diff --git a/.editorconfig b/.editorconfig index e9887fe84a..7447292eb7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,7 +1,13 @@ -# EditorConfig: https://EditorConfig.org - -# top-most EditorConfig file -root = true - -[*.cs] -trim_trailing_whitespace = true +# EditorConfig: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{cs,cpp,h}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true diff --git a/.gitattributes b/.gitattributes index 4fbfd23e2d..f427e1ff85 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ -############################################################################### -# Do not normalize any line endings. -############################################################################### +############################################################################### +# Do not normalize any line endings. +############################################################################### * -text diff --git a/.gitignore b/.gitignore index 6d36b7f882..902835ddb4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,228 +1,228 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. - -# VS 2017 user-specific files -launchSettings.json - -# User-specific files -*.suo -*.user -*.userosscache -*.sln.docstates - -# Mac -xcuserdata -.DS_Store - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -build/ -bld/ -[Bb]in/ -[Oo]bj/ - -# Visual Studio 2015 cache/options directory -.vs/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUNIT -*.VisualState.xml -TestResult*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# DNX -project.lock.json -artifacts/ - -*_i.c -*_p.c -*_i.h -*.ilk -*.meta -*.obj -*.pch -*.pdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opensdf -*.sdf -*.cachefile -*.VC.opendb -*.VC.db - -# Visual Studio profiler -*.psess -*.vsp -*.vspx - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# JustCode is a .NET coding add-in -.JustCode - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# NCrunch -_NCrunch_* -.*crunch*.local.xml - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -## TODO: Comment the next line if you want to checkin your -## web deploy settings but do note that will include unencrypted -## passwords -#*.pubxml - -*.publishproj - -# NuGet Packages -*.nupkg -# The packages folder can be ignored because of Package Restore -**/packages/* -# except build/, which is used as an MSBuild target. -!**/packages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/packages/repositories.config - -# Windows Azure Build Output -csx/ -*.build.csdef - -# Windows Store app package directory -AppPackages/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ - -# Others -ClientBin/ -[Ss]tyle[Cc]op.* -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.pfx -*.publishsettings -node_modules/ -orleans.codegen.cs - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm - -# SQL Server files -*.mdf -*.ldf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings - -# Microsoft Fakes -FakesAssemblies/ - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat - -# Visual Studio 6 build log -*.plg - -# Visual Studio 6 workspace options file -*.opt - -# LightSwitch generated files -GeneratedArtifacts/ -_Pvt_Extensions/ -ModelManifest.xml - -*.dll -*.cab -*.cer - -# VS Code private directory -.vscode/ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# VS 2017 user-specific files +launchSettings.json + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# Mac +xcuserdata +.DS_Store + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile +*.VC.opendb +*.VC.db + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +## TODO: Comment the next line if you want to checkin your +## web deploy settings but do note that will include unencrypted +## passwords +#*.pubxml + +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# LightSwitch generated files +GeneratedArtifacts/ +_Pvt_Extensions/ +ModelManifest.xml + +*.dll +*.cab +*.cer + +# VS Code private directory +.vscode/ diff --git a/AuthoringTests.md b/AuthoringTests.md index fb3728abcb..f67933a87d 100644 --- a/AuthoringTests.md +++ b/AuthoringTests.md @@ -1,122 +1,122 @@ -# Authoring Tests - -## Functional Tests - -#### Runnable functional test projects - -- `Scalar.FunctionalTests` -- `Scalar.FunctionalTests.Windows` - -`Scalar.FunctionalTests` is a .NET Core project and contains all cross-platform functional tests. `Scalar.FunctionalTests.Windows`, contains functional tests that require Windows. Additionally, `Scalar.FunctionalTests.Windows` includes all the `Scalar.FunctionalTests` allowing it to run both cross-platform and Windows-specific functional tests. - -#### Other functional test projects - -*Scalar.NativeTests* - -`Scalar.NativeTests` contains tests written in C++ that use the Windows API directly. These tests are called from the managed tests (see above) using PInvoke. - -*Scalar.FunctionalTests.LockHolder* - -The `LockHolder` is a small program that allows the functional tests to request and release the `ScalarLock`. `LockHolder` is useful for simulating different timing/race conditions. - -## Running the Functional Tests - -The functional tests are built on NUnit 3, which is available as a set of NuGet packages. - -### Windows - -1. Build Scalar: - - **Option 1:** Open Scalar.sln in Visual Studio and build everything. - - **Option 2:** Run `Scripts\BuildScalarForWindows.bat` from the command line - -2. Run the Scalar installer that was built in step 2. This will ensure that Scalar will be able to find the correct version of the pre/post-command hooks. The installer will be placed in `BuildOutput\Scalar.Installer.Windows\bin\x64\` -3. Run the tests **with elevation**. Elevation is required because the functional tests create and delete a test service. - - **Option 1:** Run the `Scalar.FunctionalTests.Windows` project from inside Visual Studio launched as Administrator. - - **Option 2:** Run `Scripts\RunFunctionalTests.bat` from CMD launched as Administrator. - -#### Selecting Which Tests are Run - -By default, the functional tests run a subset of tests as a quick smoke test for developers. There are three mutually exclusive arguments that can be passed to the functional tests to change this behavior: - -- `--full-suite`: Run all configurations of all functional tests -- `--extra-only`: Run only those tests marked as "ExtraCoverage" (i.e. the tests that are not run by default) -- `--windows-only`: Run only the tests marked as being Windows specific - -**NOTE** `Scripts\RunFunctionalTests.bat` already uses some of these arguments. If you run the tests using `RunFunctionalTests.bat` consider locally modifying the script rather than passing these flags as arguments to the script. - -### Mac - -1. Build Scalar: `Scripts/Mac/BuildScalarForMac.sh` -2. Run the tests: `Scripts/Mac/RunFunctionalTests.sh ` - -If you need the VS for Mac debugger attached for a functional test run: - -1. Make sure you've built your latest changes -2. Open Scalar.sln in VS for Mac -3. Run->Run With->Custom Configuration... -4. Select "Start external program" and specify the published functional test binary (e.g. `/Users//Repos/Scalar/Publish/Scalar.FunctionalTests`) -5. Specify any desired arguments (e.g. [a specific test](#Running-Specific-Tests) ) -6. Run Action -> "Debug - .Net Core Debugger" -7. Click "Debug" - -### Customizing the Functional Test Settings - -The functional tests take a set of parameters that indicate what paths and URLs to work with. If you want to customize those settings, they -can be found in [`Scalar.FunctionalTests\Settings.cs`](/Scalar/Scalar.FunctionalTests/Settings.cs). - - -## Running Specific Tests - -Specific tests can be run by adding the `--test=` command line argument to the functional test project/scripts. - -Note that the test name must include the class and namespace and that `Debug` or `Release` must be specified when running the functional test scripts. - -*Example* - -Windows (Script): - -`Scripts\RunFunctionalTests.bat Debug --test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` - -Windows (Visual Studio): - -1. Set `Scalar.FunctionalTests.Windows` as StartUp project -2. Project Properties->Debug->Start options->Command line arguments (all on a single line): `--test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` - -Mac: - -`Scripts/Mac/RunFunctionalTests.sh Debug --test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` - -## How to Write a Functional Test - -Each piece of functionality that we add to Scalar should have corresponding functional tests that clone a repo, mount the filesystem, and use existing tools and filesystem -APIs to interact with the virtual repo. - -Since these are functional tests that can potentially modify the state of files on disk, you need to be careful to make sure each test can run in a clean -environment. There are three base classes that you can derive from when writing your tests. It's also important to put your new class into the same namespace -as the base class, because NUnit treats namespaces like test suites, and we have logic that keys off that for deciding when to create enlistments. - -1. `TestsWithLongRunningEnlistment` - - Before any test in this namespace is executed, we create a single enlistment and mount Scalar. We then run all tests in this namespace that derive - from this base class. Only put tests in here that are purely readonly and will leave the repo in a good state for future tests. - -2. `TestsWithEnlistmentPerFixture` - - For any test fixture (a fixture is the same as a class in NUnit) that derives from this class, we create an enlistment and mount Scalar before running - any of the tests in the fixture, and then we unmount and delete the enlistment after all tests are done (but before any other fixture runs). If you need - to write a sequence of tests that manipulate the same repo, this is the right base class. - -3. `TestsWithEnlistmentPerTestCase` - - Derive from this class if you need a new enlistment created for each test case. This is the most reliable, but also most expensive option. - -## Updating the Remote Test Branch - -By default, the functional tests clone `master`, check out the branch "FunctionalTests/YYYYMMDD" (with the day the FunctionalTests branch was created), -and then remove all remote tracking information. This is done to guarantee that remote changes to tip cannot break functional tests. If you need to update -the functional tests to use a new FunctionalTests branch, you'll need to create a new "FunctionalTests/YYYYMMDD" branch and update the `Commitish` setting in `Settings.cs` to have this new branch name. -Once you have verified your scenarios locally you can push the new FunctionalTests branch and then your changes. +# Authoring Tests + +## Functional Tests + +#### Runnable functional test projects + +- `Scalar.FunctionalTests` +- `Scalar.FunctionalTests.Windows` + +`Scalar.FunctionalTests` is a .NET Core project and contains all cross-platform functional tests. `Scalar.FunctionalTests.Windows`, contains functional tests that require Windows. Additionally, `Scalar.FunctionalTests.Windows` includes all the `Scalar.FunctionalTests` allowing it to run both cross-platform and Windows-specific functional tests. + +#### Other functional test projects + +*Scalar.NativeTests* + +`Scalar.NativeTests` contains tests written in C++ that use the Windows API directly. These tests are called from the managed tests (see above) using PInvoke. + +*Scalar.FunctionalTests.LockHolder* + +The `LockHolder` is a small program that allows the functional tests to request and release the `ScalarLock`. `LockHolder` is useful for simulating different timing/race conditions. + +## Running the Functional Tests + +The functional tests are built on NUnit 3, which is available as a set of NuGet packages. + +### Windows + +1. Build Scalar: + + **Option 1:** Open Scalar.sln in Visual Studio and build everything. + + **Option 2:** Run `Scripts\BuildScalarForWindows.bat` from the command line + +2. Run the Scalar installer that was built in step 2. This will ensure that Scalar will be able to find the correct version of the pre/post-command hooks. The installer will be placed in `BuildOutput\Scalar.Installer.Windows\bin\x64\` +3. Run the tests **with elevation**. Elevation is required because the functional tests create and delete a test service. + + **Option 1:** Run the `Scalar.FunctionalTests.Windows` project from inside Visual Studio launched as Administrator. + + **Option 2:** Run `Scripts\RunFunctionalTests.bat` from CMD launched as Administrator. + +#### Selecting Which Tests are Run + +By default, the functional tests run a subset of tests as a quick smoke test for developers. There are three mutually exclusive arguments that can be passed to the functional tests to change this behavior: + +- `--full-suite`: Run all configurations of all functional tests +- `--extra-only`: Run only those tests marked as "ExtraCoverage" (i.e. the tests that are not run by default) +- `--windows-only`: Run only the tests marked as being Windows specific + +**NOTE** `Scripts\RunFunctionalTests.bat` already uses some of these arguments. If you run the tests using `RunFunctionalTests.bat` consider locally modifying the script rather than passing these flags as arguments to the script. + +### Mac + +1. Build Scalar: `Scripts/Mac/BuildScalarForMac.sh` +2. Run the tests: `Scripts/Mac/RunFunctionalTests.sh ` + +If you need the VS for Mac debugger attached for a functional test run: + +1. Make sure you've built your latest changes +2. Open Scalar.sln in VS for Mac +3. Run->Run With->Custom Configuration... +4. Select "Start external program" and specify the published functional test binary (e.g. `/Users//Repos/Scalar/Publish/Scalar.FunctionalTests`) +5. Specify any desired arguments (e.g. [a specific test](#Running-Specific-Tests) ) +6. Run Action -> "Debug - .Net Core Debugger" +7. Click "Debug" + +### Customizing the Functional Test Settings + +The functional tests take a set of parameters that indicate what paths and URLs to work with. If you want to customize those settings, they +can be found in [`Scalar.FunctionalTests\Settings.cs`](/Scalar/Scalar.FunctionalTests/Settings.cs). + + +## Running Specific Tests + +Specific tests can be run by adding the `--test=` command line argument to the functional test project/scripts. + +Note that the test name must include the class and namespace and that `Debug` or `Release` must be specified when running the functional test scripts. + +*Example* + +Windows (Script): + +`Scripts\RunFunctionalTests.bat Debug --test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` + +Windows (Visual Studio): + +1. Set `Scalar.FunctionalTests.Windows` as StartUp project +2. Project Properties->Debug->Start options->Command line arguments (all on a single line): `--test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` + +Mac: + +`Scripts/Mac/RunFunctionalTests.sh Debug --test=Scalar.FunctionalTests.Tests.EnlistmentPerFixture.GitFilesTests.CreateFileTest` + +## How to Write a Functional Test + +Each piece of functionality that we add to Scalar should have corresponding functional tests that clone a repo, mount the filesystem, and use existing tools and filesystem +APIs to interact with the virtual repo. + +Since these are functional tests that can potentially modify the state of files on disk, you need to be careful to make sure each test can run in a clean +environment. There are three base classes that you can derive from when writing your tests. It's also important to put your new class into the same namespace +as the base class, because NUnit treats namespaces like test suites, and we have logic that keys off that for deciding when to create enlistments. + +1. `TestsWithLongRunningEnlistment` + + Before any test in this namespace is executed, we create a single enlistment and mount Scalar. We then run all tests in this namespace that derive + from this base class. Only put tests in here that are purely readonly and will leave the repo in a good state for future tests. + +2. `TestsWithEnlistmentPerFixture` + + For any test fixture (a fixture is the same as a class in NUnit) that derives from this class, we create an enlistment and mount Scalar before running + any of the tests in the fixture, and then we unmount and delete the enlistment after all tests are done (but before any other fixture runs). If you need + to write a sequence of tests that manipulate the same repo, this is the right base class. + +3. `TestsWithEnlistmentPerTestCase` + + Derive from this class if you need a new enlistment created for each test case. This is the most reliable, but also most expensive option. + +## Updating the Remote Test Branch + +By default, the functional tests clone `master`, check out the branch "FunctionalTests/YYYYMMDD" (with the day the FunctionalTests branch was created), +and then remove all remote tracking information. This is done to guarantee that remote changes to tip cannot break functional tests. If you need to update +the functional tests to use a new FunctionalTests branch, you'll need to create a new "FunctionalTests/YYYYMMDD" branch and update the `Commitish` setting in `Settings.cs` to have this new branch name. +Once you have verified your scenarios locally you can push the new FunctionalTests branch and then your changes. diff --git a/Directory.Build.props b/Directory.Build.props index 0138572111..cff839cec3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,9 @@ - - - - - $(MSBuildThisFileDirectory)\..\..\BuildOutput\$(MSBuildProjectName)\obj - + + + + + $(MSBuildThisFileDirectory)\..\..\BuildOutput\$(MSBuildProjectName)\obj + diff --git a/License.md b/License.md index 891f87857b..21071075c2 100644 --- a/License.md +++ b/License.md @@ -1,21 +1,21 @@ - MIT License - - Copyright (c) Microsoft Corporation. All rights reserved. - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE diff --git a/Readme.md b/Readme.md index 02f9e95e0c..1dc64c71a6 100644 --- a/Readme.md +++ b/Readme.md @@ -1,3 +1,3 @@ -More details forthcoming. - -For now, please see the [product roadmap](../../wiki/Roadmap) +More details forthcoming. + +For now, please see the [product roadmap](../../wiki/Roadmap) diff --git a/Scalar.Build/GenerateApplicationManifests.cs b/Scalar.Build/GenerateApplicationManifests.cs index c3bd502967..68588db840 100644 --- a/Scalar.Build/GenerateApplicationManifests.cs +++ b/Scalar.Build/GenerateApplicationManifests.cs @@ -1,51 +1,51 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System.IO; - -namespace Scalar.PreBuild -{ - public class GenerateApplicationManifests : Task - { - [Required] - public string Version { get; set; } - - [Required] - public string ApplicationName { get; set; } - - [Required] - public string ManifestPath { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Creating application manifest file for {0}", ApplicationName); - - string manifestDirectory = Path.GetDirectoryName(this.ManifestPath); - if (!Directory.Exists(manifestDirectory)) - { - Directory.CreateDirectory(manifestDirectory); - } - - // Any application that calls GetVersionEx must have an application manifest in order to get an accurate response. - // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details - File.WriteAllText( - this.ManifestPath, - string.Format( -@" - - - - - - - - - -", - this.Version, - this.ApplicationName)); - - return true; - } - } -} - +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; + +namespace Scalar.PreBuild +{ + public class GenerateApplicationManifests : Task + { + [Required] + public string Version { get; set; } + + [Required] + public string ApplicationName { get; set; } + + [Required] + public string ManifestPath { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Creating application manifest file for {0}", ApplicationName); + + string manifestDirectory = Path.GetDirectoryName(this.ManifestPath); + if (!Directory.Exists(manifestDirectory)) + { + Directory.CreateDirectory(manifestDirectory); + } + + // Any application that calls GetVersionEx must have an application manifest in order to get an accurate response. + // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details + File.WriteAllText( + this.ManifestPath, + string.Format( +@" + + + + + + + + + +", + this.Version, + this.ApplicationName)); + + return true; + } + } +} + diff --git a/Scalar.Build/GenerateG4WNugetReference.cs b/Scalar.Build/GenerateG4WNugetReference.cs index fd2bd402a5..89bcdcf25d 100644 --- a/Scalar.Build/GenerateG4WNugetReference.cs +++ b/Scalar.Build/GenerateG4WNugetReference.cs @@ -1,31 +1,31 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System; -using System.IO; - -namespace Scalar.PreBuild -{ - public class GenerateG4WNugetReference : Task - { - [Required] - public string GitPackageVersion { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Creating packages.config for G4W package version " + this.GitPackageVersion); - - File.WriteAllText( - Path.Combine(Environment.CurrentDirectory, "packages.config"), - string.Format( -@" - - - - -", - this.GitPackageVersion)); - - return true; - } - } -} +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System; +using System.IO; + +namespace Scalar.PreBuild +{ + public class GenerateG4WNugetReference : Task + { + [Required] + public string GitPackageVersion { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Creating packages.config for G4W package version " + this.GitPackageVersion); + + File.WriteAllText( + Path.Combine(Environment.CurrentDirectory, "packages.config"), + string.Format( +@" + + + + +", + this.GitPackageVersion)); + + return true; + } + } +} diff --git a/Scalar.Build/GenerateGitVersionConstants.cs b/Scalar.Build/GenerateGitVersionConstants.cs index aad5e796e0..692e33e9b4 100644 --- a/Scalar.Build/GenerateGitVersionConstants.cs +++ b/Scalar.Build/GenerateGitVersionConstants.cs @@ -1,190 +1,190 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System; -using System.IO; -using System.Linq; - -namespace Scalar.PreBuild -{ - public class GenerateGitVersionConstants : Task - { - [Required] - public string GitPackageVersion { get; set; } - - [Required] - public string PackagesPath { get; set; } - - [Required] - public string OutputFile {get; set; } - - [Output] - public string LatestInstaller { get; set; } - - [Output] - public string LatestInstallerFilename { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Creating constants for Git package " + this.GitPackageVersion); - - string installerDirectory = Path.Combine( - this.PackagesPath, - "GitForWindows.GVFS.Installer." + this.GitPackageVersion, - "tools"); - - if (!Directory.Exists(installerDirectory)) - { - this.Log.LogError("Git package not found. Re-run your build to allow the package to be downloaded by Visual Studio."); - return false; - } - - this.LatestInstaller = Path.GetFullPath(Directory.EnumerateFiles(installerDirectory).Single()); - this.LatestInstallerFilename = Path.GetFileName(this.LatestInstaller); - GitVersion version; - if (!GitVersion.TryParseInstallerName(this.LatestInstallerFilename, out version)) - { - this.Log.LogError("Unable to parse git version: " + this.LatestInstallerFilename); - return false; - } - - this.Log.LogMessage(MessageImportance.High, "Found Git version " + version.ToString()); - - string projectOutputPath = Path.GetDirectoryName(this.OutputFile); - if (!Directory.Exists(projectOutputPath)) - { - Directory.CreateDirectory(projectOutputPath); - } - - File.WriteAllText( - this.OutputFile, - string.Format( -@"// This file is auto-generated by Scalar.PreBuild.GenerateGitVersionConstants. Any changes made directly in this file will be lost. -using Scalar.Common.Git; - -namespace Scalar.Common -{{ - public static partial class ScalarConstants - {{ - public static readonly GitVersion SupportedGitVersion = new GitVersion({0}, {1}, {2}, ""{3}"", {4}, {5}); - }} -}}", - version.Major, - version.Minor, - version.Build, - version.Platform, - version.Revision, - version.MinorRevision)); - - return true; - } - - // This class was partially copied from Scalar.Common.Git.GitVersion for the parsing - private class GitVersion - { - private GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) - { - this.Major = major; - this.Minor = minor; - this.Build = build; - this.Platform = platform; - this.Revision = revision; - this.MinorRevision = minorRevision; - } - - public int Major { get; private set; } - public int Minor { get; private set; } - public string Platform { get; private set; } - public int Build { get; private set; } - public int Revision { get; private set; } - public int MinorRevision { get; private set; } - - public static bool TryParseInstallerName(string input, out GitVersion version) - { - // Installer name is of the form - // Git-2.14.1.scalar.1.1.gb16030b-64-bit.exe - - version = null; - - if (!input.StartsWith("Git-", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!input.EndsWith("-64-bit.exe", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - return TryParseVersion(input.Substring(4, input.Length - 15), out version); - } - - private static bool TryParseVersion(string input, out GitVersion version) - { - version = null; - int major, minor, build, revision, minorRevision; - - if (string.IsNullOrWhiteSpace(input)) - { - return false; - } - - string[] parsedComponents = input.Split(new char[] { '.' }); - int parsedComponentsLength = parsedComponents.Length; - if (parsedComponentsLength < 5) - { - return false; - } - - if (!TryParseComponent(parsedComponents[0], out major)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[1], out minor)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[2], out build)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[4], out revision)) - { - return false; - } - - if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) - { - minorRevision = 0; - } - - string platform = parsedComponents[3]; - - version = new GitVersion(major, minor, build, platform, revision, minorRevision); - return true; - } - - public override string ToString() - { - return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); - } - - private static bool TryParseComponent(string component, out int parsedComponent) - { - if (!int.TryParse(component, out parsedComponent)) - { - return false; - } - - if (parsedComponent < 0) - { - return false; - } - - return true; - } - } - } -} +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System; +using System.IO; +using System.Linq; + +namespace Scalar.PreBuild +{ + public class GenerateGitVersionConstants : Task + { + [Required] + public string GitPackageVersion { get; set; } + + [Required] + public string PackagesPath { get; set; } + + [Required] + public string OutputFile {get; set; } + + [Output] + public string LatestInstaller { get; set; } + + [Output] + public string LatestInstallerFilename { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Creating constants for Git package " + this.GitPackageVersion); + + string installerDirectory = Path.Combine( + this.PackagesPath, + "GitForWindows.GVFS.Installer." + this.GitPackageVersion, + "tools"); + + if (!Directory.Exists(installerDirectory)) + { + this.Log.LogError("Git package not found. Re-run your build to allow the package to be downloaded by Visual Studio."); + return false; + } + + this.LatestInstaller = Path.GetFullPath(Directory.EnumerateFiles(installerDirectory).Single()); + this.LatestInstallerFilename = Path.GetFileName(this.LatestInstaller); + GitVersion version; + if (!GitVersion.TryParseInstallerName(this.LatestInstallerFilename, out version)) + { + this.Log.LogError("Unable to parse git version: " + this.LatestInstallerFilename); + return false; + } + + this.Log.LogMessage(MessageImportance.High, "Found Git version " + version.ToString()); + + string projectOutputPath = Path.GetDirectoryName(this.OutputFile); + if (!Directory.Exists(projectOutputPath)) + { + Directory.CreateDirectory(projectOutputPath); + } + + File.WriteAllText( + this.OutputFile, + string.Format( +@"// This file is auto-generated by Scalar.PreBuild.GenerateGitVersionConstants. Any changes made directly in this file will be lost. +using Scalar.Common.Git; + +namespace Scalar.Common +{{ + public static partial class ScalarConstants + {{ + public static readonly GitVersion SupportedGitVersion = new GitVersion({0}, {1}, {2}, ""{3}"", {4}, {5}); + }} +}}", + version.Major, + version.Minor, + version.Build, + version.Platform, + version.Revision, + version.MinorRevision)); + + return true; + } + + // This class was partially copied from Scalar.Common.Git.GitVersion for the parsing + private class GitVersion + { + private GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) + { + this.Major = major; + this.Minor = minor; + this.Build = build; + this.Platform = platform; + this.Revision = revision; + this.MinorRevision = minorRevision; + } + + public int Major { get; private set; } + public int Minor { get; private set; } + public string Platform { get; private set; } + public int Build { get; private set; } + public int Revision { get; private set; } + public int MinorRevision { get; private set; } + + public static bool TryParseInstallerName(string input, out GitVersion version) + { + // Installer name is of the form + // Git-2.14.1.scalar.1.1.gb16030b-64-bit.exe + + version = null; + + if (!input.StartsWith("Git-", StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (!input.EndsWith("-64-bit.exe", StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + return TryParseVersion(input.Substring(4, input.Length - 15), out version); + } + + private static bool TryParseVersion(string input, out GitVersion version) + { + version = null; + int major, minor, build, revision, minorRevision; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + string[] parsedComponents = input.Split(new char[] { '.' }); + int parsedComponentsLength = parsedComponents.Length; + if (parsedComponentsLength < 5) + { + return false; + } + + if (!TryParseComponent(parsedComponents[0], out major)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[1], out minor)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[2], out build)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[4], out revision)) + { + return false; + } + + if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + { + minorRevision = 0; + } + + string platform = parsedComponents[3]; + + version = new GitVersion(major, minor, build, platform, revision, minorRevision); + return true; + } + + public override string ToString() + { + return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); + } + + private static bool TryParseComponent(string component, out int parsedComponent) + { + if (!int.TryParse(component, out parsedComponent)) + { + return false; + } + + if (parsedComponent < 0) + { + return false; + } + + return true; + } + } + } +} diff --git a/Scalar.Build/GenerateInstallScripts.cs b/Scalar.Build/GenerateInstallScripts.cs index c2a0eb5e55..968bcc6532 100644 --- a/Scalar.Build/GenerateInstallScripts.cs +++ b/Scalar.Build/GenerateInstallScripts.cs @@ -1,61 +1,61 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System.IO; -using System.Text; - -namespace Scalar.PreBuild -{ - public class GenerateInstallScripts : Task - { - [Required] - public string GitInstallerFilename { get; set; } - - [Required] - public string ScalarSetupFilename { get; set; } - - [Required] - public string GitInstallBatPath { get; set; } - - [Required] - public string ScalarInstallBatPath { get; set; } - - [Required] - public string UnifiedInstallBatPath { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Creating install scripts for " + this.GitInstallerFilename); - - File.WriteAllText( - this.GitInstallBatPath, - this.GetGitInstallCommand()); - - File.WriteAllText( - this.ScalarInstallBatPath, - this.GetScalarInstallCommand()); - - StringBuilder sb = new StringBuilder(); - sb.AppendLine("REM AUTOGENERATED DO NOT EDIT"); - sb.AppendLine(); - sb.AppendLine(this.GetGitInstallCommand()); - sb.AppendLine(); - sb.AppendLine(this.GetScalarInstallCommand()); - - File.WriteAllText( - this.UnifiedInstallBatPath, - sb.ToString()); - - return true; - } - - public string GetGitInstallCommand() - { - return "%~dp0\\" + this.GitInstallerFilename + @" /DIR=""C:\Program Files\Git"" /NOICONS /COMPONENTS=""ext,ext\shellhere,ext\guihere,assoc,assoc_sh"" /GROUP=""Git"" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; - } - - public string GetScalarInstallCommand() - { - return "%~dp0\\" + this.ScalarSetupFilename + " /VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; - } - } -} +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; +using System.Text; + +namespace Scalar.PreBuild +{ + public class GenerateInstallScripts : Task + { + [Required] + public string GitInstallerFilename { get; set; } + + [Required] + public string ScalarSetupFilename { get; set; } + + [Required] + public string GitInstallBatPath { get; set; } + + [Required] + public string ScalarInstallBatPath { get; set; } + + [Required] + public string UnifiedInstallBatPath { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Creating install scripts for " + this.GitInstallerFilename); + + File.WriteAllText( + this.GitInstallBatPath, + this.GetGitInstallCommand()); + + File.WriteAllText( + this.ScalarInstallBatPath, + this.GetScalarInstallCommand()); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("REM AUTOGENERATED DO NOT EDIT"); + sb.AppendLine(); + sb.AppendLine(this.GetGitInstallCommand()); + sb.AppendLine(); + sb.AppendLine(this.GetScalarInstallCommand()); + + File.WriteAllText( + this.UnifiedInstallBatPath, + sb.ToString()); + + return true; + } + + public string GetGitInstallCommand() + { + return "%~dp0\\" + this.GitInstallerFilename + @" /DIR=""C:\Program Files\Git"" /NOICONS /COMPONENTS=""ext,ext\shellhere,ext\guihere,assoc,assoc_sh"" /GROUP=""Git"" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; + } + + public string GetScalarInstallCommand() + { + return "%~dp0\\" + this.ScalarSetupFilename + " /VERYSILENT /SUPPRESSMSGBOXES /NORESTART"; + } + } +} diff --git a/Scalar.Build/GenerateScalarInstallersNuspec.cs b/Scalar.Build/GenerateScalarInstallersNuspec.cs index 9b8c6671da..ab3da38c56 100644 --- a/Scalar.Build/GenerateScalarInstallersNuspec.cs +++ b/Scalar.Build/GenerateScalarInstallersNuspec.cs @@ -1,53 +1,53 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System.IO; - -namespace Scalar.PreBuild -{ - public class GenerateScalarInstallersNuspec : Task - { - [Required] - public string ScalarSetupPath { get; set; } - - [Required] - public string GitPackageVersion { get; set; } - - [Required] - public string PackagesPath { get; set; } - - [Required] - public string OutputFile { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Generating Scalar.Installers.nuspec"); - - this.ScalarSetupPath = Path.GetFullPath(this.ScalarSetupPath); - this.PackagesPath = Path.GetFullPath(this.PackagesPath); - - File.WriteAllText( - this.OutputFile, - string.Format( -@" - - - Scalar.Installers - $ScalarVersion$ - Microsoft - false - Scalar and G4W installers - - - - - - -", - this.ScalarSetupPath, - this.PackagesPath, - this.GitPackageVersion)); - - return true; - } - } -} +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; + +namespace Scalar.PreBuild +{ + public class GenerateScalarInstallersNuspec : Task + { + [Required] + public string ScalarSetupPath { get; set; } + + [Required] + public string GitPackageVersion { get; set; } + + [Required] + public string PackagesPath { get; set; } + + [Required] + public string OutputFile { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Generating Scalar.Installers.nuspec"); + + this.ScalarSetupPath = Path.GetFullPath(this.ScalarSetupPath); + this.PackagesPath = Path.GetFullPath(this.PackagesPath); + + File.WriteAllText( + this.OutputFile, + string.Format( +@" + + + Scalar.Installers + $ScalarVersion$ + Microsoft + false + Scalar and G4W installers + + + + + + +", + this.ScalarSetupPath, + this.PackagesPath, + this.GitPackageVersion)); + + return true; + } + } +} diff --git a/Scalar.Build/GenerateVersionInfo.cs b/Scalar.Build/GenerateVersionInfo.cs index 215bed7e14..de0f2bbabe 100644 --- a/Scalar.Build/GenerateVersionInfo.cs +++ b/Scalar.Build/GenerateVersionInfo.cs @@ -1,63 +1,63 @@ -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; -using System.IO; - -namespace Scalar.PreBuild -{ - public class GenerateVersionInfo : Task - { - [Required] - public string Version { get; set; } - - [Required] - public string BuildOutputPath { get; set; } - - [Required] - public string AssemblyVersion { get; set; } - - [Required] - public string VersionHeader { get; set; } - - public override bool Execute() - { - this.Log.LogMessage(MessageImportance.High, "Creating version files"); - - EnsureParentDirectoryExistsFor(this.AssemblyVersion); - File.WriteAllText( - this.AssemblyVersion, - string.Format( -@"using System.Reflection; - -[assembly: AssemblyVersion(""{0}"")] -[assembly: AssemblyFileVersion(""{0}"")] -", - this.Version)); - - string commaDelimetedVersion = this.Version.Replace('.', ','); - EnsureParentDirectoryExistsFor(this.VersionHeader); - File.WriteAllText( - this.VersionHeader, - string.Format( -@" -#define GVFS_FILE_VERSION {0} -#define GVFS_FILE_VERSION_STRING ""{1}"" -#define GVFS_PRODUCT_VERSION {0} -#define GVFS_PRODUCT_VERSION_STRING ""{1}"" -", - commaDelimetedVersion, - this.Version)); - - return true; - } - - private void EnsureParentDirectoryExistsFor(string filename) - { - string directory = Path.GetDirectoryName(filename); - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - - } - } -} +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; +using System.IO; + +namespace Scalar.PreBuild +{ + public class GenerateVersionInfo : Task + { + [Required] + public string Version { get; set; } + + [Required] + public string BuildOutputPath { get; set; } + + [Required] + public string AssemblyVersion { get; set; } + + [Required] + public string VersionHeader { get; set; } + + public override bool Execute() + { + this.Log.LogMessage(MessageImportance.High, "Creating version files"); + + EnsureParentDirectoryExistsFor(this.AssemblyVersion); + File.WriteAllText( + this.AssemblyVersion, + string.Format( +@"using System.Reflection; + +[assembly: AssemblyVersion(""{0}"")] +[assembly: AssemblyFileVersion(""{0}"")] +", + this.Version)); + + string commaDelimetedVersion = this.Version.Replace('.', ','); + EnsureParentDirectoryExistsFor(this.VersionHeader); + File.WriteAllText( + this.VersionHeader, + string.Format( +@" +#define GVFS_FILE_VERSION {0} +#define GVFS_FILE_VERSION_STRING ""{1}"" +#define GVFS_PRODUCT_VERSION {0} +#define GVFS_PRODUCT_VERSION_STRING ""{1}"" +", + commaDelimetedVersion, + this.Version)); + + return true; + } + + private void EnsureParentDirectoryExistsFor(string filename) + { + string directory = Path.GetDirectoryName(filename); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + + } + } +} diff --git a/Scalar.Build/LibGit2Sharp.NativeBinaries.props b/Scalar.Build/LibGit2Sharp.NativeBinaries.props index 16b348dccc..33e3a84482 100644 --- a/Scalar.Build/LibGit2Sharp.NativeBinaries.props +++ b/Scalar.Build/LibGit2Sharp.NativeBinaries.props @@ -1,17 +1,17 @@ - - - - - git2.dll - PreserveNewest - - - git2.so - PreserveNewest - - - git2.dylib - PreserveNewest - - - + + + + + git2.dll + PreserveNewest + + + git2.so + PreserveNewest + + + git2.dylib + PreserveNewest + + + diff --git a/Scalar.Build/Scalar.PreBuild.csproj b/Scalar.Build/Scalar.PreBuild.csproj index dbb6f017c9..cebfb95367 100644 --- a/Scalar.Build/Scalar.PreBuild.csproj +++ b/Scalar.Build/Scalar.PreBuild.csproj @@ -1,213 +1,213 @@ - - - - - - {A4984251-840E-4622-AD0C-66DFCE2B2574} - Library - Properties - Scalar.PreBuild - Scalar.PreBuild - v4.6.1 - 512 - - - true - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - true - - - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - - - - - - - - - - - - Designer - - - Designer - - - Designer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - SetupScalar.$(ScalarVersion).exe - $(BuildOutputDir)\Scalar.Installer.Windows\bin\x64\$(Configuration)\SetupScalar.$(ScalarVersion).exe - $(BuildOutputDir)\Scalar.Build\ - $(OutDir)ScalarConstants.GitVersion.cs - $(OutDir)InstallG4W.bat - $(OutDir)InstallScalar.bat - $(OutDir)InstallProduct.bat - $(OutDir)Scalar.Installers.nuspec - $(BuildOutputDir)\CommonAssemblyVersion.cs - $(BuildOutputDir)\CommonVersionHeader.h - $(BuildOutputDir)\restore.timestamp - - - - - - - - - - - - - false - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {A4984251-840E-4622-AD0C-66DFCE2B2574} + Library + Properties + Scalar.PreBuild + Scalar.PreBuild + v4.6.1 + 512 + + + true + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + + + + + + + + + Designer + + + Designer + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SetupScalar.$(ScalarVersion).exe + $(BuildOutputDir)\Scalar.Installer.Windows\bin\x64\$(Configuration)\SetupScalar.$(ScalarVersion).exe + $(BuildOutputDir)\Scalar.Build\ + $(OutDir)ScalarConstants.GitVersion.cs + $(OutDir)InstallG4W.bat + $(OutDir)InstallScalar.bat + $(OutDir)InstallProduct.bat + $(OutDir)Scalar.Installers.nuspec + $(BuildOutputDir)\CommonAssemblyVersion.cs + $(BuildOutputDir)\CommonVersionHeader.h + $(BuildOutputDir)\restore.timestamp + + + + + + + + + + + + + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.Build/Scalar.cpp.props b/Scalar.Build/Scalar.cpp.props index 21aeaaba71..9b2d260549 100644 --- a/Scalar.Build/Scalar.cpp.props +++ b/Scalar.Build/Scalar.cpp.props @@ -1,11 +1,11 @@ - - - - - - - $(BuildOutputDir)\$(MSBuildProjectName)\bin\$(Platform)\$(Configuration)\ - $(BuildOutputDir)\$(MSBuildProjectName)\intermediate\$(Platform)\$(Configuration)\ - - - + + + + + + + $(BuildOutputDir)\$(MSBuildProjectName)\bin\$(Platform)\$(Configuration)\ + $(BuildOutputDir)\$(MSBuildProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + diff --git a/Scalar.Build/Scalar.cs.props b/Scalar.Build/Scalar.cs.props index 30329b6831..08537feae6 100644 --- a/Scalar.Build/Scalar.cs.props +++ b/Scalar.Build/Scalar.cs.props @@ -1,28 +1,28 @@ - - - - - - - $(BuildOutputDir)\$(MSBuildProjectName)\bin\$(Platform)\$(Configuration)\ - $(BuildOutputDir)\$(MSBuildProjectName)\obj\$(Platform)\$(Configuration)\ - $(BuildOutputDir)\$(MSBuildProjectName)\intermediate\$(Platform)\$(Configuration)\ - - - - true - $(MSBuildThisFileDirectory)\Scalar.ruleset - false - - - - - CommonAssemblyVersion.cs - - - - - $(DefaultItemExcludes);StyleCop.Cache;TestResults.xml - - - + + + + + + + $(BuildOutputDir)\$(MSBuildProjectName)\bin\$(Platform)\$(Configuration)\ + $(BuildOutputDir)\$(MSBuildProjectName)\obj\$(Platform)\$(Configuration)\ + $(BuildOutputDir)\$(MSBuildProjectName)\intermediate\$(Platform)\$(Configuration)\ + + + + true + $(MSBuildThisFileDirectory)\Scalar.ruleset + false + + + + + CommonAssemblyVersion.cs + + + + + $(DefaultItemExcludes);StyleCop.Cache;TestResults.xml + + + diff --git a/Scalar.Build/Scalar.props b/Scalar.Build/Scalar.props index e25affd41f..83a53afb10 100644 --- a/Scalar.Build/Scalar.props +++ b/Scalar.Build/Scalar.props @@ -1,17 +1,17 @@ - - - - - 0.2.173.2 - 2.20190806.1 - - - - Debug - x64 - ..\ - $(SolutionDir)..\BuildOutput - $(SolutionDir)..\packages - - - + + + + + 0.2.173.2 + 2.20190806.1 + + + + Debug + x64 + ..\ + $(SolutionDir)..\BuildOutput + $(SolutionDir)..\packages + + + diff --git a/Scalar.Build/Scalar.ruleset b/Scalar.Build/Scalar.ruleset index ac3be97a0c..cc2030332c 100644 --- a/Scalar.Build/Scalar.ruleset +++ b/Scalar.Build/Scalar.ruleset @@ -1,67 +1,67 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.Common/AssemblyInfo.cs b/Scalar.Common/AssemblyInfo.cs index 1dc99a8cee..514010382c 100644 --- a/Scalar.Common/AssemblyInfo.cs +++ b/Scalar.Common/AssemblyInfo.cs @@ -1,4 +1,4 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Scalar.UnitTests")] -[assembly: InternalsVisibleTo("Scalar.UnitTests.Windows")] +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Scalar.UnitTests")] +[assembly: InternalsVisibleTo("Scalar.UnitTests.Windows")] diff --git a/Scalar.Common/AzDevOpsOrgFromNuGetFeed.cs b/Scalar.Common/AzDevOpsOrgFromNuGetFeed.cs index 36cc80b55e..ffbb042a89 100644 --- a/Scalar.Common/AzDevOpsOrgFromNuGetFeed.cs +++ b/Scalar.Common/AzDevOpsOrgFromNuGetFeed.cs @@ -1,51 +1,51 @@ -using System.Text.RegularExpressions; - -namespace Scalar.Common -{ - public class AzDevOpsOrgFromNuGetFeed - { - /// - /// Given a URL for a NuGet feed hosted on Azure DevOps, - /// return the organization that hosts the feed. - /// - public static bool TryParseOrg(string packageFeedUrl, out string orgName) - { - // We expect a URL of the form https://pkgs.dev.azure.com/{org} - // and want to convert it to a URL of the form https://{org}.visualstudio.com - Regex packageUrlRegex = new Regex( - @"^https://pkgs.dev.azure.com/(?.+?)/", - RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); - Match urlMatch = packageUrlRegex.Match(packageFeedUrl); - - if (!urlMatch.Success) - { - orgName = null; - return false; - } - - orgName = urlMatch.Groups["org"].Value; - return true; - } - - /// - /// Given a URL for a NuGet feed hosted on Azure DevOps, - /// return a URL that Git Credential Manager can use to - /// query for a credential that is valid for use with the - /// NuGet feed. - /// - public static bool TryCreateCredentialQueryUrl(string packageFeedUrl, out string azureDevOpsUrl, out string error) - { - if (!TryParseOrg(packageFeedUrl, out string org)) - { - azureDevOpsUrl = null; - error = $"Input URL {packageFeedUrl} did not match expected format for an Azure DevOps Package Feed URL"; - return false; - } - - azureDevOpsUrl = $"https://{org}.visualstudio.com"; - error = null; - - return true; - } - } -} +using System.Text.RegularExpressions; + +namespace Scalar.Common +{ + public class AzDevOpsOrgFromNuGetFeed + { + /// + /// Given a URL for a NuGet feed hosted on Azure DevOps, + /// return the organization that hosts the feed. + /// + public static bool TryParseOrg(string packageFeedUrl, out string orgName) + { + // We expect a URL of the form https://pkgs.dev.azure.com/{org} + // and want to convert it to a URL of the form https://{org}.visualstudio.com + Regex packageUrlRegex = new Regex( + @"^https://pkgs.dev.azure.com/(?.+?)/", + RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + Match urlMatch = packageUrlRegex.Match(packageFeedUrl); + + if (!urlMatch.Success) + { + orgName = null; + return false; + } + + orgName = urlMatch.Groups["org"].Value; + return true; + } + + /// + /// Given a URL for a NuGet feed hosted on Azure DevOps, + /// return a URL that Git Credential Manager can use to + /// query for a credential that is valid for use with the + /// NuGet feed. + /// + public static bool TryCreateCredentialQueryUrl(string packageFeedUrl, out string azureDevOpsUrl, out string error) + { + if (!TryParseOrg(packageFeedUrl, out string org)) + { + azureDevOpsUrl = null; + error = $"Input URL {packageFeedUrl} did not match expected format for an Azure DevOps Package Feed URL"; + return false; + } + + azureDevOpsUrl = $"https://{org}.visualstudio.com"; + error = null; + + return true; + } + } +} diff --git a/Scalar.Common/ConcurrentHashSet.cs b/Scalar.Common/ConcurrentHashSet.cs index 33488347a4..e886bcb6d3 100644 --- a/Scalar.Common/ConcurrentHashSet.cs +++ b/Scalar.Common/ConcurrentHashSet.cs @@ -1,57 +1,57 @@ -using System.Collections; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Scalar.Common -{ - public class ConcurrentHashSet : IEnumerable - { - private ConcurrentDictionary dictionary; - - public ConcurrentHashSet() - { - this.dictionary = new ConcurrentDictionary(); - } - - public ConcurrentHashSet(IEqualityComparer comparer) - { - this.dictionary = new ConcurrentDictionary(comparer); - } - - public int Count - { - get { return this.dictionary.Count; } - } - - public bool Add(T entry) - { - return this.dictionary.TryAdd(entry, true); - } - - public bool Contains(T item) - { - return this.dictionary.ContainsKey(item); - } - - public void Clear() - { - this.dictionary.Clear(); - } - - public IEnumerator GetEnumerator() - { - return this.dictionary.Keys.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return this.GetEnumerator(); - } - - public bool TryRemove(T key) - { - bool value; - return this.dictionary.TryRemove(key, out value); - } - } -} +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Scalar.Common +{ + public class ConcurrentHashSet : IEnumerable + { + private ConcurrentDictionary dictionary; + + public ConcurrentHashSet() + { + this.dictionary = new ConcurrentDictionary(); + } + + public ConcurrentHashSet(IEqualityComparer comparer) + { + this.dictionary = new ConcurrentDictionary(comparer); + } + + public int Count + { + get { return this.dictionary.Count; } + } + + public bool Add(T entry) + { + return this.dictionary.TryAdd(entry, true); + } + + public bool Contains(T item) + { + return this.dictionary.ContainsKey(item); + } + + public void Clear() + { + this.dictionary.Clear(); + } + + public IEnumerator GetEnumerator() + { + return this.dictionary.Keys.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.GetEnumerator(); + } + + public bool TryRemove(T key) + { + bool value; + return this.dictionary.TryRemove(key, out value); + } + } +} diff --git a/Scalar.Common/ConsoleHelper.cs b/Scalar.Common/ConsoleHelper.cs index 077b4c3c71..19a4027605 100644 --- a/Scalar.Common/ConsoleHelper.cs +++ b/Scalar.Common/ConsoleHelper.cs @@ -1,148 +1,148 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Scalar.Common -{ - public static class ConsoleHelper - { - public enum ActionResult - { - Success, - CompletedWithErrors, - Failure, - } - - public static bool ShowStatusWhileRunning( - Func action, - string message, - TextWriter output, - bool showSpinner, - string scalarLogEnlistmentRoot, - int initialDelayMs = 0) - { - Func actionResultAction = - () => - { - return action() ? ActionResult.Success : ActionResult.Failure; - }; - - ActionResult result = ShowStatusWhileRunning( - actionResultAction, - message, - output, - showSpinner, - scalarLogEnlistmentRoot, - initialDelayMs: initialDelayMs); - - return result == ActionResult.Success; - } - - public static ActionResult ShowStatusWhileRunning( - Func action, - string message, - TextWriter output, - bool showSpinner, - string scalarLogEnlistmentRoot, - int initialDelayMs) - { - ActionResult result = ActionResult.Failure; - bool initialMessageWritten = false; - - try - { - if (!showSpinner) - { - output.Write(message + "..."); - initialMessageWritten = true; - result = action(); - } - else - { - ManualResetEvent actionIsDone = new ManualResetEvent(false); - bool isComplete = false; - Thread spinnerThread = new Thread( - () => - { - int retries = 0; - char[] waiting = { '\u2014', '\\', '|', '/' }; - - while (!isComplete) - { - if (retries == 0) - { - actionIsDone.WaitOne(initialDelayMs); - } - else - { - output.Write("\r{0}...{1}", message, waiting[(retries / 2) % waiting.Length]); - initialMessageWritten = true; - actionIsDone.WaitOne(100); - } - - retries++; - } - - if (initialMessageWritten) - { - // Clear out any trailing waiting character - output.Write("\r{0}...", message); - } - }); - spinnerThread.Start(); - - try - { - result = action(); - } - finally - { - isComplete = true; - - actionIsDone.Set(); - spinnerThread.Join(); - } - } - } - finally - { - switch (result) - { - case ActionResult.Success: - if (initialMessageWritten) - { - output.WriteLine("Succeeded"); - } - - break; - - case ActionResult.CompletedWithErrors: - if (!initialMessageWritten) - { - output.Write("\r{0}...", message); - } - - output.WriteLine("Completed with errors."); - break; - - case ActionResult.Failure: - if (!initialMessageWritten) - { - output.Write("\r{0}...", message); - } - - output.WriteLine("Failed" + (scalarLogEnlistmentRoot == null ? string.Empty : ". " + GetScalarLogMessage(scalarLogEnlistmentRoot))); - break; - } - } - - return result; - } - - public static string GetScalarLogMessage(string enlistmentRoot) - { - return "Run 'scalar log " + enlistmentRoot + "' for more info."; - } - } -} +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Scalar.Common +{ + public static class ConsoleHelper + { + public enum ActionResult + { + Success, + CompletedWithErrors, + Failure, + } + + public static bool ShowStatusWhileRunning( + Func action, + string message, + TextWriter output, + bool showSpinner, + string scalarLogEnlistmentRoot, + int initialDelayMs = 0) + { + Func actionResultAction = + () => + { + return action() ? ActionResult.Success : ActionResult.Failure; + }; + + ActionResult result = ShowStatusWhileRunning( + actionResultAction, + message, + output, + showSpinner, + scalarLogEnlistmentRoot, + initialDelayMs: initialDelayMs); + + return result == ActionResult.Success; + } + + public static ActionResult ShowStatusWhileRunning( + Func action, + string message, + TextWriter output, + bool showSpinner, + string scalarLogEnlistmentRoot, + int initialDelayMs) + { + ActionResult result = ActionResult.Failure; + bool initialMessageWritten = false; + + try + { + if (!showSpinner) + { + output.Write(message + "..."); + initialMessageWritten = true; + result = action(); + } + else + { + ManualResetEvent actionIsDone = new ManualResetEvent(false); + bool isComplete = false; + Thread spinnerThread = new Thread( + () => + { + int retries = 0; + char[] waiting = { '\u2014', '\\', '|', '/' }; + + while (!isComplete) + { + if (retries == 0) + { + actionIsDone.WaitOne(initialDelayMs); + } + else + { + output.Write("\r{0}...{1}", message, waiting[(retries / 2) % waiting.Length]); + initialMessageWritten = true; + actionIsDone.WaitOne(100); + } + + retries++; + } + + if (initialMessageWritten) + { + // Clear out any trailing waiting character + output.Write("\r{0}...", message); + } + }); + spinnerThread.Start(); + + try + { + result = action(); + } + finally + { + isComplete = true; + + actionIsDone.Set(); + spinnerThread.Join(); + } + } + } + finally + { + switch (result) + { + case ActionResult.Success: + if (initialMessageWritten) + { + output.WriteLine("Succeeded"); + } + + break; + + case ActionResult.CompletedWithErrors: + if (!initialMessageWritten) + { + output.Write("\r{0}...", message); + } + + output.WriteLine("Completed with errors."); + break; + + case ActionResult.Failure: + if (!initialMessageWritten) + { + output.Write("\r{0}...", message); + } + + output.WriteLine("Failed" + (scalarLogEnlistmentRoot == null ? string.Empty : ". " + GetScalarLogMessage(scalarLogEnlistmentRoot))); + break; + } + } + + return result; + } + + public static string GetScalarLogMessage(string enlistmentRoot) + { + return "Run 'scalar log " + enlistmentRoot + "' for more info."; + } + } +} diff --git a/Scalar.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs b/Scalar.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs index c1005745f8..b7b642ca57 100644 --- a/Scalar.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs +++ b/Scalar.Common/DiskLayoutUpgrades/DiskLayoutUpgrade.cs @@ -1,403 +1,403 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.DiskLayoutUpgrades -{ - public abstract class DiskLayoutUpgrade - { - private static Dictionary majorVersionUpgrades; - private static Dictionary> minorVersionUpgrades; - protected abstract int SourceMajorVersion { get; } - protected abstract int SourceMinorVersion { get; } - protected abstract bool IsMajorUpgrade { get; } - - public static bool TryRunAllUpgrades(string enlistmentRoot) - { - majorVersionUpgrades = new Dictionary(); - minorVersionUpgrades = new Dictionary>(); - - foreach (DiskLayoutUpgrade upgrade in ScalarPlatform.Instance.DiskLayoutUpgrade.Upgrades) - { - RegisterUpgrade(upgrade); - } - - using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "DiskLayoutUpgrade")) - { - try - { - DiskLayoutUpgrade upgrade = null; - while (TryFindUpgrade(tracer, enlistmentRoot, out upgrade)) - { - if (upgrade == null) - { - return true; - } - - if (!upgrade.TryUpgrade(tracer, enlistmentRoot)) - { - return false; - } - - if (!CheckLayoutVersionWasIncremented(tracer, enlistmentRoot, upgrade)) - { - return false; - } - } - - return false; - } - catch (Exception e) - { - StartLogFile(enlistmentRoot, tracer); - tracer.RelatedError(e.ToString()); - return false; - } - finally - { - RepoMetadata.Shutdown(); - } - } - } - - public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRoot, out string error) - { - error = string.Empty; - int majorVersion; - int minorVersion; - try - { - if (TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) - { - if (majorVersion < ScalarPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion) - { - error = string.Format( - "Breaking change to Scalar disk layout has been made since cloning. \r\nEnlistment disk layout version: {0} \r\nScalar disk layout version: {1} \r\nMinimum supported version: {2}", - majorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion); - - return false; - } - else if (majorVersion > ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) - { - error = string.Format( - "Changes to Scalar disk layout do not allow mounting after downgrade. Try mounting again using a more recent version of Scalar. \r\nEnlistment disk layout version: {0} \r\nScalar disk layout version: {1}", - majorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion); - - return false; - } - else if (majorVersion != ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) - { - error = string.Format( - "Scalar disk layout version doesn't match current version. Try running 'scalar mount' to upgrade. \r\nEnlistment disk layout version: {0}.{1} \r\nScalar disk layout version: {2}.{3}", - majorVersion, - minorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion); - - return false; - } - - return true; - } - } - finally - { - RepoMetadata.Shutdown(); - } - - error = "Failed to read disk layout version. " + ConsoleHelper.GetScalarLogMessage(enlistmentRoot); - return false; - } - - public abstract bool TryUpgrade(ITracer tracer, string enlistmentRoot); - - protected bool TryDeleteFolder(ITracer tracer, string folderName) - { - try - { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - fileSystem.DeleteDirectory(folderName); - } - catch (Exception e) - { - tracer.RelatedError("Failed to delete folder {0}: {1}", folderName, e.ToString()); - return true; - } - - return true; - } - - protected bool TryDeleteFile(ITracer tracer, string fileName) - { - try - { - File.Delete(fileName); - } - catch (Exception e) - { - tracer.RelatedError("Failed to delete file {0}: {1}", fileName, e.ToString()); - return true; - } - - return true; - } - - protected bool TryRenameFolderForDelete(ITracer tracer, string folderName, out string backupFolder) - { - backupFolder = folderName + ".deleteme"; - - tracer.RelatedInfo("Moving " + folderName + " to " + backupFolder); - - try - { - Directory.Move(folderName, backupFolder); - } - catch (Exception e) - { - tracer.RelatedError("Failed to move {0} to {1}: {2}", folderName, backupFolder, e.ToString()); - return false; - } - - return true; - } - - protected bool TrySetGitConfig(ITracer tracer, string enlistmentRoot, Dictionary configSettings) - { - ScalarEnlistment enlistment; - - try - { - enlistment = ScalarEnlistment.CreateFromDirectory( - enlistmentRoot, - ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), - authentication: null); - } - catch (InvalidRepoException e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", e.ToString()); - metadata.Add(nameof(enlistmentRoot), enlistmentRoot); - tracer.RelatedError(metadata, $"{nameof(this.TrySetGitConfig)}: Failed to create ScalarEnlistment from directory"); - return false; - } - - GitProcess git = enlistment.CreateGitProcess(); - - foreach (string key in configSettings.Keys) - { - GitProcess.Result result = git.SetInLocalConfig(key, configSettings[key]); - if (result.ExitCodeIsFailure) - { - tracer.RelatedError("Could not set git config setting {0}. Error: {1}", key, result.Errors); - return false; - } - } - - return true; - } - - private static void RegisterUpgrade(DiskLayoutUpgrade upgrade) - { - if (upgrade.IsMajorUpgrade) - { - majorVersionUpgrades.Add(upgrade.SourceMajorVersion, (MajorUpgrade)upgrade); - } - else - { - if (minorVersionUpgrades.ContainsKey(upgrade.SourceMajorVersion)) - { - minorVersionUpgrades[upgrade.SourceMajorVersion].Add(upgrade.SourceMinorVersion, (MinorUpgrade)upgrade); - } - else - { - minorVersionUpgrades.Add(upgrade.SourceMajorVersion, new Dictionary { { upgrade.SourceMinorVersion, (MinorUpgrade)upgrade } }); - } - } - } - - private static bool CheckLayoutVersionWasIncremented(JsonTracer tracer, string enlistmentRoot, DiskLayoutUpgrade upgrade) - { - string error; - int actualMajorVersion; - int actualMinorVersion; - if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out actualMajorVersion, out actualMinorVersion, out error)) - { - tracer.RelatedError(error); - return false; - } - - int expectedMajorVersion = - upgrade.IsMajorUpgrade - ? upgrade.SourceMajorVersion + 1 - : upgrade.SourceMajorVersion; - int expectedMinorVersion = - upgrade.IsMajorUpgrade - ? 0 - : upgrade.SourceMinorVersion + 1; - - if (actualMajorVersion != expectedMajorVersion || - actualMinorVersion != expectedMinorVersion) - { - throw new InvalidDataException(string.Format( - "Disk layout upgrade did not increment layout version. Expected: {0}.{1}, Actual: {2}.{3}", - expectedMajorVersion, - expectedMinorVersion, - actualMajorVersion, - actualMinorVersion)); - } - - return true; - } - - private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out DiskLayoutUpgrade upgrade) - { - int majorVersion; - int minorVersion; - - string error; - if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) - { - StartLogFile(enlistmentRoot, tracer); - tracer.RelatedError(error); - upgrade = null; - return false; - } - - Dictionary minorVersionUpgradesForCurrentMajorVersion; - if (minorVersionUpgrades.TryGetValue(majorVersion, out minorVersionUpgradesForCurrentMajorVersion)) - { - MinorUpgrade minorUpgrade; - if (minorVersionUpgradesForCurrentMajorVersion.TryGetValue(minorVersion, out minorUpgrade)) - { - StartLogFile(enlistmentRoot, tracer); - tracer.RelatedInfo( - "Upgrading from disk layout {0}.{1} to {0}.{2}", - majorVersion, - minorVersion, - minorVersion + 1); - - upgrade = minorUpgrade; - return true; - } - } - - MajorUpgrade majorUpgrade; - if (majorVersionUpgrades.TryGetValue(majorVersion, out majorUpgrade)) - { - StartLogFile(enlistmentRoot, tracer); - tracer.RelatedInfo("Upgrading from disk layout {0} to {1}", majorVersion, majorVersion + 1); - - upgrade = majorUpgrade; - return true; - } - - // return true to indicate that we succeeded, and no upgrader was found - upgrade = null; - return true; - } - - private static bool TryGetDiskLayoutVersion( - ITracer tracer, - string enlistmentRoot, - out int majorVersion, - out int minorVersion, - out string error) - { - minorVersion = 0; - - string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); - - if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) - { - majorVersion = 0; - return false; - } - - if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) - { - return false; - } - - error = null; - return true; - } - - private static void StartLogFile(string enlistmentRoot, JsonTracer tracer) - { - if (!tracer.HasLogFileEventListener) - { - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName( - Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.LogName), - ScalarConstants.LogFileTypes.MountUpgrade), - EventLevel.Informational, - Keywords.Any); - - tracer.WriteStartEvent(enlistmentRoot, repoUrl: "N/A", cacheServerUrl: "N/A"); - } - } - - public abstract class MajorUpgrade : DiskLayoutUpgrade - { - protected sealed override bool IsMajorUpgrade - { - get { return true; } - } - - protected sealed override int SourceMinorVersion - { - get { throw new NotSupportedException(); } - } - - protected bool TryIncrementMajorVersion(ITracer tracer, string enlistmentRoot) - { - string newMajorVersion = (this.SourceMajorVersion + 1).ToString(); - string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); - string error; - if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) - { - tracer.RelatedError("Could not initialize repo metadata: " + error); - return false; - } - - RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMajorVersion, newMajorVersion); - RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, "0"); - - tracer.RelatedInfo("Disk layout version is now: " + newMajorVersion); - return true; - } - } - - public abstract class MinorUpgrade : DiskLayoutUpgrade - { - protected sealed override bool IsMajorUpgrade - { - get { return false; } - } - - protected bool TryIncrementMinorVersion(ITracer tracer, string enlistmentRoot) - { - string newMinorVersion = (this.SourceMinorVersion + 1).ToString(); - string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); - string error; - if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) - { - tracer.RelatedError("Could not initialize repo metadata: " + error); - return false; - } - - RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, newMinorVersion); - - tracer.RelatedInfo("Disk layout version is now: {0}.{1}", this.SourceMajorVersion, newMinorVersion); - return true; - } - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.DiskLayoutUpgrades +{ + public abstract class DiskLayoutUpgrade + { + private static Dictionary majorVersionUpgrades; + private static Dictionary> minorVersionUpgrades; + protected abstract int SourceMajorVersion { get; } + protected abstract int SourceMinorVersion { get; } + protected abstract bool IsMajorUpgrade { get; } + + public static bool TryRunAllUpgrades(string enlistmentRoot) + { + majorVersionUpgrades = new Dictionary(); + minorVersionUpgrades = new Dictionary>(); + + foreach (DiskLayoutUpgrade upgrade in ScalarPlatform.Instance.DiskLayoutUpgrade.Upgrades) + { + RegisterUpgrade(upgrade); + } + + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "DiskLayoutUpgrade")) + { + try + { + DiskLayoutUpgrade upgrade = null; + while (TryFindUpgrade(tracer, enlistmentRoot, out upgrade)) + { + if (upgrade == null) + { + return true; + } + + if (!upgrade.TryUpgrade(tracer, enlistmentRoot)) + { + return false; + } + + if (!CheckLayoutVersionWasIncremented(tracer, enlistmentRoot, upgrade)) + { + return false; + } + } + + return false; + } + catch (Exception e) + { + StartLogFile(enlistmentRoot, tracer); + tracer.RelatedError(e.ToString()); + return false; + } + finally + { + RepoMetadata.Shutdown(); + } + } + } + + public static bool TryCheckDiskLayoutVersion(ITracer tracer, string enlistmentRoot, out string error) + { + error = string.Empty; + int majorVersion; + int minorVersion; + try + { + if (TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) + { + if (majorVersion < ScalarPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion) + { + error = string.Format( + "Breaking change to Scalar disk layout has been made since cloning. \r\nEnlistment disk layout version: {0} \r\nScalar disk layout version: {1} \r\nMinimum supported version: {2}", + majorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.MinimumSupportedMajorVersion); + + return false; + } + else if (majorVersion > ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) + { + error = string.Format( + "Changes to Scalar disk layout do not allow mounting after downgrade. Try mounting again using a more recent version of Scalar. \r\nEnlistment disk layout version: {0} \r\nScalar disk layout version: {1}", + majorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion); + + return false; + } + else if (majorVersion != ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) + { + error = string.Format( + "Scalar disk layout version doesn't match current version. Try running 'scalar mount' to upgrade. \r\nEnlistment disk layout version: {0}.{1} \r\nScalar disk layout version: {2}.{3}", + majorVersion, + minorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion); + + return false; + } + + return true; + } + } + finally + { + RepoMetadata.Shutdown(); + } + + error = "Failed to read disk layout version. " + ConsoleHelper.GetScalarLogMessage(enlistmentRoot); + return false; + } + + public abstract bool TryUpgrade(ITracer tracer, string enlistmentRoot); + + protected bool TryDeleteFolder(ITracer tracer, string folderName) + { + try + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + fileSystem.DeleteDirectory(folderName); + } + catch (Exception e) + { + tracer.RelatedError("Failed to delete folder {0}: {1}", folderName, e.ToString()); + return true; + } + + return true; + } + + protected bool TryDeleteFile(ITracer tracer, string fileName) + { + try + { + File.Delete(fileName); + } + catch (Exception e) + { + tracer.RelatedError("Failed to delete file {0}: {1}", fileName, e.ToString()); + return true; + } + + return true; + } + + protected bool TryRenameFolderForDelete(ITracer tracer, string folderName, out string backupFolder) + { + backupFolder = folderName + ".deleteme"; + + tracer.RelatedInfo("Moving " + folderName + " to " + backupFolder); + + try + { + Directory.Move(folderName, backupFolder); + } + catch (Exception e) + { + tracer.RelatedError("Failed to move {0} to {1}: {2}", folderName, backupFolder, e.ToString()); + return false; + } + + return true; + } + + protected bool TrySetGitConfig(ITracer tracer, string enlistmentRoot, Dictionary configSettings) + { + ScalarEnlistment enlistment; + + try + { + enlistment = ScalarEnlistment.CreateFromDirectory( + enlistmentRoot, + ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), + authentication: null); + } + catch (InvalidRepoException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + metadata.Add(nameof(enlistmentRoot), enlistmentRoot); + tracer.RelatedError(metadata, $"{nameof(this.TrySetGitConfig)}: Failed to create ScalarEnlistment from directory"); + return false; + } + + GitProcess git = enlistment.CreateGitProcess(); + + foreach (string key in configSettings.Keys) + { + GitProcess.Result result = git.SetInLocalConfig(key, configSettings[key]); + if (result.ExitCodeIsFailure) + { + tracer.RelatedError("Could not set git config setting {0}. Error: {1}", key, result.Errors); + return false; + } + } + + return true; + } + + private static void RegisterUpgrade(DiskLayoutUpgrade upgrade) + { + if (upgrade.IsMajorUpgrade) + { + majorVersionUpgrades.Add(upgrade.SourceMajorVersion, (MajorUpgrade)upgrade); + } + else + { + if (minorVersionUpgrades.ContainsKey(upgrade.SourceMajorVersion)) + { + minorVersionUpgrades[upgrade.SourceMajorVersion].Add(upgrade.SourceMinorVersion, (MinorUpgrade)upgrade); + } + else + { + minorVersionUpgrades.Add(upgrade.SourceMajorVersion, new Dictionary { { upgrade.SourceMinorVersion, (MinorUpgrade)upgrade } }); + } + } + } + + private static bool CheckLayoutVersionWasIncremented(JsonTracer tracer, string enlistmentRoot, DiskLayoutUpgrade upgrade) + { + string error; + int actualMajorVersion; + int actualMinorVersion; + if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out actualMajorVersion, out actualMinorVersion, out error)) + { + tracer.RelatedError(error); + return false; + } + + int expectedMajorVersion = + upgrade.IsMajorUpgrade + ? upgrade.SourceMajorVersion + 1 + : upgrade.SourceMajorVersion; + int expectedMinorVersion = + upgrade.IsMajorUpgrade + ? 0 + : upgrade.SourceMinorVersion + 1; + + if (actualMajorVersion != expectedMajorVersion || + actualMinorVersion != expectedMinorVersion) + { + throw new InvalidDataException(string.Format( + "Disk layout upgrade did not increment layout version. Expected: {0}.{1}, Actual: {2}.{3}", + expectedMajorVersion, + expectedMinorVersion, + actualMajorVersion, + actualMinorVersion)); + } + + return true; + } + + private static bool TryFindUpgrade(JsonTracer tracer, string enlistmentRoot, out DiskLayoutUpgrade upgrade) + { + int majorVersion; + int minorVersion; + + string error; + if (!TryGetDiskLayoutVersion(tracer, enlistmentRoot, out majorVersion, out minorVersion, out error)) + { + StartLogFile(enlistmentRoot, tracer); + tracer.RelatedError(error); + upgrade = null; + return false; + } + + Dictionary minorVersionUpgradesForCurrentMajorVersion; + if (minorVersionUpgrades.TryGetValue(majorVersion, out minorVersionUpgradesForCurrentMajorVersion)) + { + MinorUpgrade minorUpgrade; + if (minorVersionUpgradesForCurrentMajorVersion.TryGetValue(minorVersion, out minorUpgrade)) + { + StartLogFile(enlistmentRoot, tracer); + tracer.RelatedInfo( + "Upgrading from disk layout {0}.{1} to {0}.{2}", + majorVersion, + minorVersion, + minorVersion + 1); + + upgrade = minorUpgrade; + return true; + } + } + + MajorUpgrade majorUpgrade; + if (majorVersionUpgrades.TryGetValue(majorVersion, out majorUpgrade)) + { + StartLogFile(enlistmentRoot, tracer); + tracer.RelatedInfo("Upgrading from disk layout {0} to {1}", majorVersion, majorVersion + 1); + + upgrade = majorUpgrade; + return true; + } + + // return true to indicate that we succeeded, and no upgrader was found + upgrade = null; + return true; + } + + private static bool TryGetDiskLayoutVersion( + ITracer tracer, + string enlistmentRoot, + out int majorVersion, + out int minorVersion, + out string error) + { + minorVersion = 0; + + string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); + + if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) + { + majorVersion = 0; + return false; + } + + if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) + { + return false; + } + + error = null; + return true; + } + + private static void StartLogFile(string enlistmentRoot, JsonTracer tracer) + { + if (!tracer.HasLogFileEventListener) + { + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName( + Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.LogName), + ScalarConstants.LogFileTypes.MountUpgrade), + EventLevel.Informational, + Keywords.Any); + + tracer.WriteStartEvent(enlistmentRoot, repoUrl: "N/A", cacheServerUrl: "N/A"); + } + } + + public abstract class MajorUpgrade : DiskLayoutUpgrade + { + protected sealed override bool IsMajorUpgrade + { + get { return true; } + } + + protected sealed override int SourceMinorVersion + { + get { throw new NotSupportedException(); } + } + + protected bool TryIncrementMajorVersion(ITracer tracer, string enlistmentRoot) + { + string newMajorVersion = (this.SourceMajorVersion + 1).ToString(); + string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); + string error; + if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) + { + tracer.RelatedError("Could not initialize repo metadata: " + error); + return false; + } + + RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMajorVersion, newMajorVersion); + RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, "0"); + + tracer.RelatedInfo("Disk layout version is now: " + newMajorVersion); + return true; + } + } + + public abstract class MinorUpgrade : DiskLayoutUpgrade + { + protected sealed override bool IsMajorUpgrade + { + get { return false; } + } + + protected bool TryIncrementMinorVersion(ITracer tracer, string enlistmentRoot) + { + string newMinorVersion = (this.SourceMinorVersion + 1).ToString(); + string dotScalarPath = Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); + string error; + if (!RepoMetadata.TryInitialize(tracer, dotScalarPath, out error)) + { + tracer.RelatedError("Could not initialize repo metadata: " + error); + return false; + } + + RepoMetadata.Instance.SetEntry(RepoMetadata.Keys.DiskLayoutMinorVersion, newMinorVersion); + + tracer.RelatedInfo("Disk layout version is now: {0}.{1}", this.SourceMajorVersion, newMinorVersion); + return true; + } + } + } +} diff --git a/Scalar.Common/DiskLayoutUpgrades/DiskLayoutVersion.cs b/Scalar.Common/DiskLayoutUpgrades/DiskLayoutVersion.cs index 287a8e6c33..b47c4dcc15 100644 --- a/Scalar.Common/DiskLayoutUpgrades/DiskLayoutVersion.cs +++ b/Scalar.Common/DiskLayoutUpgrades/DiskLayoutVersion.cs @@ -1,26 +1,26 @@ -namespace Scalar.Common -{ - public class DiskLayoutVersion - { - public DiskLayoutVersion(int currentMajorVersion, int currentMinorVersion, int minimumSupportedMajorVersion) - { - this.CurrentMajorVersion = currentMajorVersion; - this.CurrentMinorVersion = currentMinorVersion; - this.MinimumSupportedMajorVersion = minimumSupportedMajorVersion; - } - - // The major version should be bumped whenever there is an on-disk format change that requires a one-way upgrade. - // Increasing this version will make older versions of Scalar unable to mount a repo that has been mounted by a newer - // version of Scalar. - public int CurrentMajorVersion { get; } - - // The minor version should be bumped whenever there is an upgrade that can be safely ignored by older versions of Scalar. - // For example, this allows an upgrade step that sets a default value for some new config setting. - public int CurrentMinorVersion { get; } - - // This is the last time Scalar made a breaking change that required a reclone. This should not - // be incremented on platforms that have released a v1.0 as all their format changes should be - // supported with an upgrade step. - public int MinimumSupportedMajorVersion { get; } - } -} +namespace Scalar.Common +{ + public class DiskLayoutVersion + { + public DiskLayoutVersion(int currentMajorVersion, int currentMinorVersion, int minimumSupportedMajorVersion) + { + this.CurrentMajorVersion = currentMajorVersion; + this.CurrentMinorVersion = currentMinorVersion; + this.MinimumSupportedMajorVersion = minimumSupportedMajorVersion; + } + + // The major version should be bumped whenever there is an on-disk format change that requires a one-way upgrade. + // Increasing this version will make older versions of Scalar unable to mount a repo that has been mounted by a newer + // version of Scalar. + public int CurrentMajorVersion { get; } + + // The minor version should be bumped whenever there is an upgrade that can be safely ignored by older versions of Scalar. + // For example, this allows an upgrade step that sets a default value for some new config setting. + public int CurrentMinorVersion { get; } + + // This is the last time Scalar made a breaking change that required a reclone. This should not + // be incremented on platforms that have released a v1.0 as all their format changes should be + // supported with an upgrade step. + public int MinimumSupportedMajorVersion { get; } + } +} diff --git a/Scalar.Common/Enlistment.cs b/Scalar.Common/Enlistment.cs index d2c47ced98..8423040689 100644 --- a/Scalar.Common/Enlistment.cs +++ b/Scalar.Common/Enlistment.cs @@ -1,113 +1,113 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using System; -using System.IO; - -namespace Scalar.Common -{ - public abstract class Enlistment - { - protected Enlistment( - string enlistmentRoot, - string workingDirectoryRoot, - string workingDirectoryBackingRoot, - string repoUrl, - string gitBinPath, - bool flushFileBuffersForPacks, - GitAuthentication authentication) - { - if (string.IsNullOrWhiteSpace(gitBinPath)) - { - throw new ArgumentException("Path to git.exe must be set"); - } - - this.EnlistmentRoot = enlistmentRoot; - this.WorkingDirectoryRoot = workingDirectoryRoot; - this.WorkingDirectoryBackingRoot = workingDirectoryBackingRoot; - this.DotGitRoot = Path.Combine(this.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Root); - this.GitBinPath = gitBinPath; - this.FlushFileBuffersForPacks = flushFileBuffersForPacks; - - GitProcess gitProcess = new GitProcess(this); - if (repoUrl != null) - { - this.RepoUrl = repoUrl; - } - else - { - GitProcess.ConfigResult originResult = gitProcess.GetOriginUrl(); - if (!originResult.TryParseAsString(out string originUrl, out string error)) - { - throw new InvalidRepoException("Could not get origin url. git error: " + error); - } - - if (originUrl == null) - { - throw new InvalidRepoException("Could not get origin url. remote 'origin' is not configured for this repo.'"); - } - - this.RepoUrl = originUrl.Trim(); - } - - this.Authentication = authentication ?? new GitAuthentication(gitProcess, this.RepoUrl); - } - - public string EnlistmentRoot { get; } - - // Path to the root of the working (i.e. "src") directory. - // On platforms where the contents of the working directory are stored - // at a different location (e.g. Linux), WorkingDirectoryBackingRoot is the path of that backing - // storage location. On all other platforms WorkingDirectoryRoot and WorkingDirectoryBackingRoot - // are the same. - public string WorkingDirectoryRoot { get; } - public string WorkingDirectoryBackingRoot { get; } - - public string DotGitRoot { get; private set; } - public abstract string GitObjectsRoot { get; protected set; } - public abstract string LocalObjectsRoot { get; protected set; } - public abstract string GitPackRoot { get; protected set; } - public string RepoUrl { get; } - public bool FlushFileBuffersForPacks { get; } - - public string GitBinPath { get; } - - public GitAuthentication Authentication { get; } - - public static string GetNewLogFileName( - string logsRoot, - string prefix, - string logId = null, - PhysicalFileSystem fileSystem = null) - { - fileSystem = fileSystem ?? new PhysicalFileSystem(); - - // TODO: Remove Directory.CreateDirectory() code from here - // Don't change the state from an accessor. - if (!fileSystem.DirectoryExists(logsRoot)) - { - fileSystem.CreateDirectory(logsRoot); - } - - logId = logId ?? DateTime.Now.ToString("yyyyMMdd_HHmmss"); - - string name = prefix + "_" + logId; - string fullPath = Path.Combine( - logsRoot, - name + ".log"); - - if (fileSystem.FileExists(fullPath)) - { - fullPath = Path.Combine( - logsRoot, - name + "_" + Guid.NewGuid().ToString("N") + ".log"); - } - - return fullPath; - } - - public virtual GitProcess CreateGitProcess() - { - return new GitProcess(this); - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using System; +using System.IO; + +namespace Scalar.Common +{ + public abstract class Enlistment + { + protected Enlistment( + string enlistmentRoot, + string workingDirectoryRoot, + string workingDirectoryBackingRoot, + string repoUrl, + string gitBinPath, + bool flushFileBuffersForPacks, + GitAuthentication authentication) + { + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + throw new ArgumentException("Path to git.exe must be set"); + } + + this.EnlistmentRoot = enlistmentRoot; + this.WorkingDirectoryRoot = workingDirectoryRoot; + this.WorkingDirectoryBackingRoot = workingDirectoryBackingRoot; + this.DotGitRoot = Path.Combine(this.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Root); + this.GitBinPath = gitBinPath; + this.FlushFileBuffersForPacks = flushFileBuffersForPacks; + + GitProcess gitProcess = new GitProcess(this); + if (repoUrl != null) + { + this.RepoUrl = repoUrl; + } + else + { + GitProcess.ConfigResult originResult = gitProcess.GetOriginUrl(); + if (!originResult.TryParseAsString(out string originUrl, out string error)) + { + throw new InvalidRepoException("Could not get origin url. git error: " + error); + } + + if (originUrl == null) + { + throw new InvalidRepoException("Could not get origin url. remote 'origin' is not configured for this repo.'"); + } + + this.RepoUrl = originUrl.Trim(); + } + + this.Authentication = authentication ?? new GitAuthentication(gitProcess, this.RepoUrl); + } + + public string EnlistmentRoot { get; } + + // Path to the root of the working (i.e. "src") directory. + // On platforms where the contents of the working directory are stored + // at a different location (e.g. Linux), WorkingDirectoryBackingRoot is the path of that backing + // storage location. On all other platforms WorkingDirectoryRoot and WorkingDirectoryBackingRoot + // are the same. + public string WorkingDirectoryRoot { get; } + public string WorkingDirectoryBackingRoot { get; } + + public string DotGitRoot { get; private set; } + public abstract string GitObjectsRoot { get; protected set; } + public abstract string LocalObjectsRoot { get; protected set; } + public abstract string GitPackRoot { get; protected set; } + public string RepoUrl { get; } + public bool FlushFileBuffersForPacks { get; } + + public string GitBinPath { get; } + + public GitAuthentication Authentication { get; } + + public static string GetNewLogFileName( + string logsRoot, + string prefix, + string logId = null, + PhysicalFileSystem fileSystem = null) + { + fileSystem = fileSystem ?? new PhysicalFileSystem(); + + // TODO: Remove Directory.CreateDirectory() code from here + // Don't change the state from an accessor. + if (!fileSystem.DirectoryExists(logsRoot)) + { + fileSystem.CreateDirectory(logsRoot); + } + + logId = logId ?? DateTime.Now.ToString("yyyyMMdd_HHmmss"); + + string name = prefix + "_" + logId; + string fullPath = Path.Combine( + logsRoot, + name + ".log"); + + if (fileSystem.FileExists(fullPath)) + { + fullPath = Path.Combine( + logsRoot, + name + "_" + Guid.NewGuid().ToString("N") + ".log"); + } + + return fullPath; + } + + public virtual GitProcess CreateGitProcess() + { + return new GitProcess(this); + } + } +} diff --git a/Scalar.Common/EpochConverter.cs b/Scalar.Common/EpochConverter.cs index ef6537b74f..8a649ac779 100644 --- a/Scalar.Common/EpochConverter.cs +++ b/Scalar.Common/EpochConverter.cs @@ -1,19 +1,19 @@ -using System; - -namespace Scalar.Common -{ - public static class EpochConverter - { - private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - - public static long ToUnixEpochSeconds(DateTime datetime) - { - return Convert.ToInt64(Math.Truncate((datetime - UnixEpoch).TotalSeconds)); - } - - public static DateTime FromUnixEpochSeconds(long secondsSinceEpoch) - { - return UnixEpoch.AddSeconds(secondsSinceEpoch); - } - } +using System; + +namespace Scalar.Common +{ + public static class EpochConverter + { + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + public static long ToUnixEpochSeconds(DateTime datetime) + { + return Convert.ToInt64(Math.Truncate((datetime - UnixEpoch).TotalSeconds)); + } + + public static DateTime FromUnixEpochSeconds(long secondsSinceEpoch) + { + return UnixEpoch.AddSeconds(secondsSinceEpoch); + } + } } diff --git a/Scalar.Common/FileBasedCollection.cs b/Scalar.Common/FileBasedCollection.cs index 6f9f8d91c6..ce6d16d30b 100644 --- a/Scalar.Common/FileBasedCollection.cs +++ b/Scalar.Common/FileBasedCollection.cs @@ -1,468 +1,468 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Text; -using System.Threading; - -namespace Scalar.Common -{ - public abstract class FileBasedCollection : IDisposable - { - private const string EtwArea = nameof(FileBasedCollection); - - private const string AddEntryPrefix = "A "; - private const string RemoveEntryPrefix = "D "; - - // Use the same newline separator regardless of platform - private const string NewLine = "\r\n"; - private const int IoFailureRetryDelayMS = 50; - private const int IoFailureLoggingThreshold = 500; - - /// - /// If true, this FileBasedCollection appends directly to dataFileHandle stream - /// If false, this FileBasedCollection only using .tmp + rename to update data on disk - /// - private readonly bool collectionAppendsDirectlyToFile; - - private readonly object fileLock = new object(); - - private readonly PhysicalFileSystem fileSystem; - private readonly string dataDirectoryPath; - private readonly string tempFilePath; - - private Stream dataFileHandle; - - protected FileBasedCollection(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath, bool collectionAppendsDirectlyToFile) - { - this.Tracer = tracer; - this.fileSystem = fileSystem; - this.DataFilePath = dataFilePath; - this.tempFilePath = this.DataFilePath + ".tmp"; - this.dataDirectoryPath = Path.GetDirectoryName(this.DataFilePath); - this.collectionAppendsDirectlyToFile = collectionAppendsDirectlyToFile; - } - - protected delegate bool TryParseAdd(string line, out TKey key, out TValue value, out string error); - protected delegate bool TryParseRemove(string line, out TKey key, out string error); - - public string DataFilePath { get; } - - protected ITracer Tracer { get; } - - public void Dispose() - { - lock (this.fileLock) - { - this.CloseDataFile(); - } - } - - public void ForceFlush() - { - if (this.dataFileHandle != null) - { - FileStream fs = this.dataFileHandle as FileStream; - if (fs != null) - { - fs.Flush(flushToDisk: true); - } - } - } - - protected void WriteAndReplaceDataFile(Func> getDataLines) - { - lock (this.fileLock) - { - try - { - this.CloseDataFile(); - - bool tmpFileCreated = false; - int tmpFileCreateAttempts = 0; - - bool tmpFileMoved = false; - int tmpFileMoveAttempts = 0; - - Exception lastException = null; - - while (!tmpFileCreated || !tmpFileMoved) - { - if (!tmpFileCreated) - { - tmpFileCreated = this.TryWriteTempFile(getDataLines, out lastException); - if (!tmpFileCreated) - { - if (this.Tracer != null && tmpFileCreateAttempts % IoFailureLoggingThreshold == 0) - { - EventMetadata metadata = CreateEventMetadata(lastException); - metadata.Add("tmpFileCreateAttempts", tmpFileCreateAttempts); - this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to create tmp file ... retrying"); - } - - ++tmpFileCreateAttempts; - Thread.Sleep(IoFailureRetryDelayMS); - } - } - - if (tmpFileCreated) - { - try - { - if (this.fileSystem.FileExists(this.tempFilePath)) - { - this.fileSystem.MoveAndOverwriteFile(this.tempFilePath, this.DataFilePath); - tmpFileMoved = true; - } - else - { - if (this.Tracer != null) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); - this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": tmp file is missing. Recreating tmp file."); - } - - tmpFileCreated = false; - } - } - catch (Win32Exception e) - { - if (this.Tracer != null && tmpFileMoveAttempts % IoFailureLoggingThreshold == 0) - { - EventMetadata metadata = CreateEventMetadata(e); - metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); - this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to overwrite data file ... retrying"); - } - - ++tmpFileMoveAttempts; - Thread.Sleep(IoFailureRetryDelayMS); - } - } - } - - if (this.collectionAppendsDirectlyToFile) - { - this.OpenOrCreateDataFile(retryUntilSuccess: true); - } - } - catch (Exception e) - { - throw new FileBasedCollectionException(e); - } - } - } - - protected string FormatAddLine(string line) - { - return AddEntryPrefix + line; - } - - protected string FormatRemoveLine(string line) - { - return RemoveEntryPrefix + line; - } - - /// An optional callback to be run as soon as the fileLock is taken. - protected void WriteAddEntry(string value, Action synchronizedAction = null) - { - lock (this.fileLock) - { - string line = this.FormatAddLine(value); - if (synchronizedAction != null) - { - synchronizedAction(); - } - - this.WriteToDisk(line); - } - } - - /// An optional callback to be run as soon as the fileLock is taken. - protected void WriteRemoveEntry(string key, Action synchronizedAction = null) - { - lock (this.fileLock) - { - string line = this.FormatRemoveLine(key); - if (synchronizedAction != null) - { - synchronizedAction(); - } - - this.WriteToDisk(line); - } - } - - protected void DeleteDataFileIfCondition(Func condition) - { - if (!this.collectionAppendsDirectlyToFile) - { - throw new InvalidOperationException(nameof(this.DeleteDataFileIfCondition) + " requires that collectionAppendsDirectlyToFile be true"); - } - - lock (this.fileLock) - { - if (condition()) - { - this.dataFileHandle.SetLength(0); - } - } - } - - /// An optional callback to be run as soon as the fileLock is taken - protected bool TryLoadFromDisk( - TryParseAdd tryParseAdd, - TryParseRemove tryParseRemove, - Action add, - out string error, - Action synchronizedAction = null) - { - lock (this.fileLock) - { - try - { - if (synchronizedAction != null) - { - synchronizedAction(); - } - - this.fileSystem.CreateDirectory(this.dataDirectoryPath); - - this.OpenOrCreateDataFile(retryUntilSuccess: false); - - if (this.collectionAppendsDirectlyToFile) - { - this.RemoveLastEntryIfInvalid(); - } - - long lineCount = 0; - - this.dataFileHandle.Seek(0, SeekOrigin.Begin); - StreamReader reader = new StreamReader(this.dataFileHandle); - Dictionary parsedEntries = new Dictionary(); - while (!reader.EndOfStream) - { - lineCount++; - - // StreamReader strips the trailing /r/n - string line = reader.ReadLine(); - if (line.StartsWith(RemoveEntryPrefix)) - { - TKey key; - if (!tryParseRemove(line.Substring(RemoveEntryPrefix.Length), out key, out error)) - { - error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); - return false; - } - - parsedEntries.Remove(key); - } - else if (line.StartsWith(AddEntryPrefix)) - { - TKey key; - TValue value; - if (!tryParseAdd(line.Substring(AddEntryPrefix.Length), out key, out value, out error)) - { - error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); - return false; - } - - parsedEntries[key] = value; - } - else - { - error = string.Format("{0} is corrupt on line {1}: Invalid Prefix '{2}'", this.GetType().Name, lineCount, line[0]); - return false; - } - } - - foreach (KeyValuePair kvp in parsedEntries) - { - add(kvp.Key, kvp.Value); - } - - if (!this.collectionAppendsDirectlyToFile) - { - this.CloseDataFile(); - } - } - catch (IOException ex) - { - error = ex.ToString(); - this.CloseDataFile(); - return false; - } - catch (Exception e) - { - this.CloseDataFile(); - throw new FileBasedCollectionException(e); - } - - error = null; - return true; - } - } - - 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; - } - - /// - /// Closes dataFileHandle. Requires fileLock. - /// - private void CloseDataFile() - { - if (this.dataFileHandle != null) - { - this.dataFileHandle.Dispose(); - this.dataFileHandle = null; - } - } - - /// - /// Opens dataFileHandle for ReadWrite. Requires fileLock. - /// - /// If true, OpenOrCreateDataFile will continue to retry until it succeeds - /// If retryUntilSuccess is true, OpenOrCreateDataFile will only attempt to retry when the error is non-fatal - private void OpenOrCreateDataFile(bool retryUntilSuccess) - { - int attempts = 0; - Exception lastException = null; - while (true) - { - try - { - if (this.dataFileHandle == null) - { - this.dataFileHandle = this.fileSystem.OpenFileStream( - this.DataFilePath, - FileMode.OpenOrCreate, - this.collectionAppendsDirectlyToFile ? FileAccess.ReadWrite : FileAccess.Read, - FileShare.Read, - callFlushFileBuffers: false); - } - - this.dataFileHandle.Seek(0, SeekOrigin.End); - return; - } - catch (IOException e) - { - lastException = e; - } - catch (UnauthorizedAccessException e) - { - lastException = e; - } - - if (retryUntilSuccess) - { - if (this.Tracer != null && attempts % IoFailureLoggingThreshold == 0) - { - EventMetadata metadata = CreateEventMetadata(lastException); - metadata.Add("attempts", attempts); - this.Tracer.RelatedWarning(metadata, nameof(this.OpenOrCreateDataFile) + ": Failed to open data file stream ... retrying"); - } - - ++attempts; - Thread.Sleep(IoFailureRetryDelayMS); - } - else - { - throw lastException; - } - } - } - - /// - /// Writes data as UTF8 to dataFileHandle. fileLock will be acquired. - /// - private void WriteToDisk(string value) - { - if (!this.collectionAppendsDirectlyToFile) - { - throw new InvalidOperationException(nameof(this.WriteToDisk) + " requires that collectionAppendsDirectlyToFile be true"); - } - - byte[] bytes = Encoding.UTF8.GetBytes(value + NewLine); - lock (this.fileLock) - { - this.dataFileHandle.Write(bytes, 0, bytes.Length); - this.dataFileHandle.Flush(); - } - } - - /// - /// Reads entries from dataFileHandle, removing any data after the last NewLine ("\r\n"). Requires fileLock. - /// - private void RemoveLastEntryIfInvalid() - { - if (this.dataFileHandle.Length > 2) - { - this.dataFileHandle.Seek(-2, SeekOrigin.End); - if (this.dataFileHandle.ReadByte() != '\r' || - this.dataFileHandle.ReadByte() != '\n') - { - this.dataFileHandle.Seek(0, SeekOrigin.Begin); - long lastLineEnding = 0; - while (this.dataFileHandle.Position < this.dataFileHandle.Length) - { - if (this.dataFileHandle.ReadByte() == '\r' && this.dataFileHandle.ReadByte() == '\n') - { - lastLineEnding = this.dataFileHandle.Position; - } - } - - this.dataFileHandle.SetLength(lastLineEnding); - } - } - } - - /// - /// Attempts to write all data lines to tmp file - /// - /// Method that returns the dataLines to write as an IEnumerable - /// Output parameter that's set when TryWriteTempFile catches a non-fatal exception - /// True if the write succeeded and false otherwise - /// If a fatal exception is encountered while trying to write the temp file, this method will not catch it. - private bool TryWriteTempFile(Func> getDataLines, out Exception handledException) - { - handledException = null; - - try - { - using (Stream tempFile = this.fileSystem.OpenFileStream(this.tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) - using (StreamWriter writer = new StreamWriter(tempFile)) - { - foreach (string line in getDataLines()) - { - writer.Write(line + NewLine); - } - - tempFile.Flush(); - } - - return true; - } - catch (IOException e) - { - handledException = e; - return false; - } - catch (UnauthorizedAccessException e) - { - handledException = e; - return false; - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Threading; + +namespace Scalar.Common +{ + public abstract class FileBasedCollection : IDisposable + { + private const string EtwArea = nameof(FileBasedCollection); + + private const string AddEntryPrefix = "A "; + private const string RemoveEntryPrefix = "D "; + + // Use the same newline separator regardless of platform + private const string NewLine = "\r\n"; + private const int IoFailureRetryDelayMS = 50; + private const int IoFailureLoggingThreshold = 500; + + /// + /// If true, this FileBasedCollection appends directly to dataFileHandle stream + /// If false, this FileBasedCollection only using .tmp + rename to update data on disk + /// + private readonly bool collectionAppendsDirectlyToFile; + + private readonly object fileLock = new object(); + + private readonly PhysicalFileSystem fileSystem; + private readonly string dataDirectoryPath; + private readonly string tempFilePath; + + private Stream dataFileHandle; + + protected FileBasedCollection(ITracer tracer, PhysicalFileSystem fileSystem, string dataFilePath, bool collectionAppendsDirectlyToFile) + { + this.Tracer = tracer; + this.fileSystem = fileSystem; + this.DataFilePath = dataFilePath; + this.tempFilePath = this.DataFilePath + ".tmp"; + this.dataDirectoryPath = Path.GetDirectoryName(this.DataFilePath); + this.collectionAppendsDirectlyToFile = collectionAppendsDirectlyToFile; + } + + protected delegate bool TryParseAdd(string line, out TKey key, out TValue value, out string error); + protected delegate bool TryParseRemove(string line, out TKey key, out string error); + + public string DataFilePath { get; } + + protected ITracer Tracer { get; } + + public void Dispose() + { + lock (this.fileLock) + { + this.CloseDataFile(); + } + } + + public void ForceFlush() + { + if (this.dataFileHandle != null) + { + FileStream fs = this.dataFileHandle as FileStream; + if (fs != null) + { + fs.Flush(flushToDisk: true); + } + } + } + + protected void WriteAndReplaceDataFile(Func> getDataLines) + { + lock (this.fileLock) + { + try + { + this.CloseDataFile(); + + bool tmpFileCreated = false; + int tmpFileCreateAttempts = 0; + + bool tmpFileMoved = false; + int tmpFileMoveAttempts = 0; + + Exception lastException = null; + + while (!tmpFileCreated || !tmpFileMoved) + { + if (!tmpFileCreated) + { + tmpFileCreated = this.TryWriteTempFile(getDataLines, out lastException); + if (!tmpFileCreated) + { + if (this.Tracer != null && tmpFileCreateAttempts % IoFailureLoggingThreshold == 0) + { + EventMetadata metadata = CreateEventMetadata(lastException); + metadata.Add("tmpFileCreateAttempts", tmpFileCreateAttempts); + this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to create tmp file ... retrying"); + } + + ++tmpFileCreateAttempts; + Thread.Sleep(IoFailureRetryDelayMS); + } + } + + if (tmpFileCreated) + { + try + { + if (this.fileSystem.FileExists(this.tempFilePath)) + { + this.fileSystem.MoveAndOverwriteFile(this.tempFilePath, this.DataFilePath); + tmpFileMoved = true; + } + else + { + if (this.Tracer != null) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); + this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": tmp file is missing. Recreating tmp file."); + } + + tmpFileCreated = false; + } + } + catch (Win32Exception e) + { + if (this.Tracer != null && tmpFileMoveAttempts % IoFailureLoggingThreshold == 0) + { + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add("tmpFileMoveAttempts", tmpFileMoveAttempts); + this.Tracer.RelatedWarning(metadata, nameof(this.WriteAndReplaceDataFile) + ": Failed to overwrite data file ... retrying"); + } + + ++tmpFileMoveAttempts; + Thread.Sleep(IoFailureRetryDelayMS); + } + } + } + + if (this.collectionAppendsDirectlyToFile) + { + this.OpenOrCreateDataFile(retryUntilSuccess: true); + } + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + } + + protected string FormatAddLine(string line) + { + return AddEntryPrefix + line; + } + + protected string FormatRemoveLine(string line) + { + return RemoveEntryPrefix + line; + } + + /// An optional callback to be run as soon as the fileLock is taken. + protected void WriteAddEntry(string value, Action synchronizedAction = null) + { + lock (this.fileLock) + { + string line = this.FormatAddLine(value); + if (synchronizedAction != null) + { + synchronizedAction(); + } + + this.WriteToDisk(line); + } + } + + /// An optional callback to be run as soon as the fileLock is taken. + protected void WriteRemoveEntry(string key, Action synchronizedAction = null) + { + lock (this.fileLock) + { + string line = this.FormatRemoveLine(key); + if (synchronizedAction != null) + { + synchronizedAction(); + } + + this.WriteToDisk(line); + } + } + + protected void DeleteDataFileIfCondition(Func condition) + { + if (!this.collectionAppendsDirectlyToFile) + { + throw new InvalidOperationException(nameof(this.DeleteDataFileIfCondition) + " requires that collectionAppendsDirectlyToFile be true"); + } + + lock (this.fileLock) + { + if (condition()) + { + this.dataFileHandle.SetLength(0); + } + } + } + + /// An optional callback to be run as soon as the fileLock is taken + protected bool TryLoadFromDisk( + TryParseAdd tryParseAdd, + TryParseRemove tryParseRemove, + Action add, + out string error, + Action synchronizedAction = null) + { + lock (this.fileLock) + { + try + { + if (synchronizedAction != null) + { + synchronizedAction(); + } + + this.fileSystem.CreateDirectory(this.dataDirectoryPath); + + this.OpenOrCreateDataFile(retryUntilSuccess: false); + + if (this.collectionAppendsDirectlyToFile) + { + this.RemoveLastEntryIfInvalid(); + } + + long lineCount = 0; + + this.dataFileHandle.Seek(0, SeekOrigin.Begin); + StreamReader reader = new StreamReader(this.dataFileHandle); + Dictionary parsedEntries = new Dictionary(); + while (!reader.EndOfStream) + { + lineCount++; + + // StreamReader strips the trailing /r/n + string line = reader.ReadLine(); + if (line.StartsWith(RemoveEntryPrefix)) + { + TKey key; + if (!tryParseRemove(line.Substring(RemoveEntryPrefix.Length), out key, out error)) + { + error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); + return false; + } + + parsedEntries.Remove(key); + } + else if (line.StartsWith(AddEntryPrefix)) + { + TKey key; + TValue value; + if (!tryParseAdd(line.Substring(AddEntryPrefix.Length), out key, out value, out error)) + { + error = string.Format("{0} is corrupt on line {1}: {2}", this.GetType().Name, lineCount, error); + return false; + } + + parsedEntries[key] = value; + } + else + { + error = string.Format("{0} is corrupt on line {1}: Invalid Prefix '{2}'", this.GetType().Name, lineCount, line[0]); + return false; + } + } + + foreach (KeyValuePair kvp in parsedEntries) + { + add(kvp.Key, kvp.Value); + } + + if (!this.collectionAppendsDirectlyToFile) + { + this.CloseDataFile(); + } + } + catch (IOException ex) + { + error = ex.ToString(); + this.CloseDataFile(); + return false; + } + catch (Exception e) + { + this.CloseDataFile(); + throw new FileBasedCollectionException(e); + } + + error = null; + return true; + } + } + + 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; + } + + /// + /// Closes dataFileHandle. Requires fileLock. + /// + private void CloseDataFile() + { + if (this.dataFileHandle != null) + { + this.dataFileHandle.Dispose(); + this.dataFileHandle = null; + } + } + + /// + /// Opens dataFileHandle for ReadWrite. Requires fileLock. + /// + /// If true, OpenOrCreateDataFile will continue to retry until it succeeds + /// If retryUntilSuccess is true, OpenOrCreateDataFile will only attempt to retry when the error is non-fatal + private void OpenOrCreateDataFile(bool retryUntilSuccess) + { + int attempts = 0; + Exception lastException = null; + while (true) + { + try + { + if (this.dataFileHandle == null) + { + this.dataFileHandle = this.fileSystem.OpenFileStream( + this.DataFilePath, + FileMode.OpenOrCreate, + this.collectionAppendsDirectlyToFile ? FileAccess.ReadWrite : FileAccess.Read, + FileShare.Read, + callFlushFileBuffers: false); + } + + this.dataFileHandle.Seek(0, SeekOrigin.End); + return; + } + catch (IOException e) + { + lastException = e; + } + catch (UnauthorizedAccessException e) + { + lastException = e; + } + + if (retryUntilSuccess) + { + if (this.Tracer != null && attempts % IoFailureLoggingThreshold == 0) + { + EventMetadata metadata = CreateEventMetadata(lastException); + metadata.Add("attempts", attempts); + this.Tracer.RelatedWarning(metadata, nameof(this.OpenOrCreateDataFile) + ": Failed to open data file stream ... retrying"); + } + + ++attempts; + Thread.Sleep(IoFailureRetryDelayMS); + } + else + { + throw lastException; + } + } + } + + /// + /// Writes data as UTF8 to dataFileHandle. fileLock will be acquired. + /// + private void WriteToDisk(string value) + { + if (!this.collectionAppendsDirectlyToFile) + { + throw new InvalidOperationException(nameof(this.WriteToDisk) + " requires that collectionAppendsDirectlyToFile be true"); + } + + byte[] bytes = Encoding.UTF8.GetBytes(value + NewLine); + lock (this.fileLock) + { + this.dataFileHandle.Write(bytes, 0, bytes.Length); + this.dataFileHandle.Flush(); + } + } + + /// + /// Reads entries from dataFileHandle, removing any data after the last NewLine ("\r\n"). Requires fileLock. + /// + private void RemoveLastEntryIfInvalid() + { + if (this.dataFileHandle.Length > 2) + { + this.dataFileHandle.Seek(-2, SeekOrigin.End); + if (this.dataFileHandle.ReadByte() != '\r' || + this.dataFileHandle.ReadByte() != '\n') + { + this.dataFileHandle.Seek(0, SeekOrigin.Begin); + long lastLineEnding = 0; + while (this.dataFileHandle.Position < this.dataFileHandle.Length) + { + if (this.dataFileHandle.ReadByte() == '\r' && this.dataFileHandle.ReadByte() == '\n') + { + lastLineEnding = this.dataFileHandle.Position; + } + } + + this.dataFileHandle.SetLength(lastLineEnding); + } + } + } + + /// + /// Attempts to write all data lines to tmp file + /// + /// Method that returns the dataLines to write as an IEnumerable + /// Output parameter that's set when TryWriteTempFile catches a non-fatal exception + /// True if the write succeeded and false otherwise + /// If a fatal exception is encountered while trying to write the temp file, this method will not catch it. + private bool TryWriteTempFile(Func> getDataLines, out Exception handledException) + { + handledException = null; + + try + { + using (Stream tempFile = this.fileSystem.OpenFileStream(this.tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) + using (StreamWriter writer = new StreamWriter(tempFile)) + { + foreach (string line in getDataLines()) + { + writer.Write(line + NewLine); + } + + tempFile.Flush(); + } + + return true; + } + catch (IOException e) + { + handledException = e; + return false; + } + catch (UnauthorizedAccessException e) + { + handledException = e; + return false; + } + } + } +} diff --git a/Scalar.Common/FileBasedCollectionException.cs b/Scalar.Common/FileBasedCollectionException.cs index 3582791b0f..5a6fe5dd24 100644 --- a/Scalar.Common/FileBasedCollectionException.cs +++ b/Scalar.Common/FileBasedCollectionException.cs @@ -1,12 +1,12 @@ -using System; - -namespace Scalar.Common -{ - public class FileBasedCollectionException : Exception - { - public FileBasedCollectionException(Exception innerException) - : base(innerException.Message, innerException) - { - } - } -} +using System; + +namespace Scalar.Common +{ + public class FileBasedCollectionException : Exception + { + public FileBasedCollectionException(Exception innerException) + : base(innerException.Message, innerException) + { + } + } +} diff --git a/Scalar.Common/FileBasedDictionary.cs b/Scalar.Common/FileBasedDictionary.cs index e379dd809f..d56441d013 100644 --- a/Scalar.Common/FileBasedDictionary.cs +++ b/Scalar.Common/FileBasedDictionary.cs @@ -1,169 +1,169 @@ -using Newtonsoft.Json; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; - -namespace Scalar.Common -{ - public class FileBasedDictionary : FileBasedCollection - { - private ConcurrentDictionary data; - - private FileBasedDictionary( - ITracer tracer, - PhysicalFileSystem fileSystem, - string dataFilePath, - IEqualityComparer keyComparer) - : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: false) - { - this.data = new ConcurrentDictionary(keyComparer); - } - - public static bool TryCreate( - ITracer tracer, - string dictionaryPath, - PhysicalFileSystem fileSystem, - out FileBasedDictionary output, - out string error, - IEqualityComparer keyComparer = null) - { - output = new FileBasedDictionary( - tracer, - fileSystem, - dictionaryPath, - keyComparer ?? EqualityComparer.Default); - - if (!output.TryLoadFromDisk( - output.TryParseAddLine, - output.TryParseRemoveLine, - output.HandleAddLine, - out error)) - { - output = null; - return false; - } - - return true; - } - - public void SetValuesAndFlush(IEnumerable> values) - { - try - { - foreach (KeyValuePair kvp in values) - { - this.data[kvp.Key] = kvp.Value; - } - - this.Flush(); - } - catch (Exception e) - { - throw new FileBasedCollectionException(e); - } - } - - public void SetValueAndFlush(TKey key, TValue value) - { - try - { - this.data[key] = value; - this.Flush(); - } - catch (Exception e) - { - throw new FileBasedCollectionException(e); - } - } - - public bool TryGetValue(TKey key, out TValue value) - { - try - { - return this.data.TryGetValue(key, out value); - } - catch (Exception e) - { - throw new FileBasedCollectionException(e); - } - } - - public void RemoveAndFlush(TKey key) - { - try - { - TValue value; - if (this.data.TryRemove(key, out value)) - { - this.Flush(); - } - } - catch (Exception e) - { - throw new FileBasedCollectionException(e); - } - } - - public Dictionary GetAllKeysAndValues() - { - return new Dictionary(this.data); - } - - private void Flush() - { - this.WriteAndReplaceDataFile(this.GenerateDataLines); - } - - private bool TryParseAddLine(string line, out TKey key, out TValue value, out string error) - { - try - { - KeyValuePair kvp = JsonConvert.DeserializeObject>(line); - key = kvp.Key; - value = kvp.Value; - } - catch (JsonException ex) - { - key = default(TKey); - value = default(TValue); - error = "Could not deserialize JSON for add line: " + ex.Message; - return false; - } - - error = null; - return true; - } - - private bool TryParseRemoveLine(string line, out TKey key, out string error) - { - try - { - key = JsonConvert.DeserializeObject(line); - } - catch (JsonException ex) - { - key = default(TKey); - error = "Could not deserialize JSON for delete line: " + ex.Message; - return false; - } - - error = null; - return true; - } - - private void HandleAddLine(TKey key, TValue value) - { - this.data.TryAdd(key, value); - } - - private IEnumerable GenerateDataLines() - { - foreach (KeyValuePair kvp in this.data) - { - yield return this.FormatAddLine(JsonConvert.SerializeObject(kvp).Trim()); - } - } - } -} +using Newtonsoft.Json; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; + +namespace Scalar.Common +{ + public class FileBasedDictionary : FileBasedCollection + { + private ConcurrentDictionary data; + + private FileBasedDictionary( + ITracer tracer, + PhysicalFileSystem fileSystem, + string dataFilePath, + IEqualityComparer keyComparer) + : base(tracer, fileSystem, dataFilePath, collectionAppendsDirectlyToFile: false) + { + this.data = new ConcurrentDictionary(keyComparer); + } + + public static bool TryCreate( + ITracer tracer, + string dictionaryPath, + PhysicalFileSystem fileSystem, + out FileBasedDictionary output, + out string error, + IEqualityComparer keyComparer = null) + { + output = new FileBasedDictionary( + tracer, + fileSystem, + dictionaryPath, + keyComparer ?? EqualityComparer.Default); + + if (!output.TryLoadFromDisk( + output.TryParseAddLine, + output.TryParseRemoveLine, + output.HandleAddLine, + out error)) + { + output = null; + return false; + } + + return true; + } + + public void SetValuesAndFlush(IEnumerable> values) + { + try + { + foreach (KeyValuePair kvp in values) + { + this.data[kvp.Key] = kvp.Value; + } + + this.Flush(); + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + + public void SetValueAndFlush(TKey key, TValue value) + { + try + { + this.data[key] = value; + this.Flush(); + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + + public bool TryGetValue(TKey key, out TValue value) + { + try + { + return this.data.TryGetValue(key, out value); + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + + public void RemoveAndFlush(TKey key) + { + try + { + TValue value; + if (this.data.TryRemove(key, out value)) + { + this.Flush(); + } + } + catch (Exception e) + { + throw new FileBasedCollectionException(e); + } + } + + public Dictionary GetAllKeysAndValues() + { + return new Dictionary(this.data); + } + + private void Flush() + { + this.WriteAndReplaceDataFile(this.GenerateDataLines); + } + + private bool TryParseAddLine(string line, out TKey key, out TValue value, out string error) + { + try + { + KeyValuePair kvp = JsonConvert.DeserializeObject>(line); + key = kvp.Key; + value = kvp.Value; + } + catch (JsonException ex) + { + key = default(TKey); + value = default(TValue); + error = "Could not deserialize JSON for add line: " + ex.Message; + return false; + } + + error = null; + return true; + } + + private bool TryParseRemoveLine(string line, out TKey key, out string error) + { + try + { + key = JsonConvert.DeserializeObject(line); + } + catch (JsonException ex) + { + key = default(TKey); + error = "Could not deserialize JSON for delete line: " + ex.Message; + return false; + } + + error = null; + return true; + } + + private void HandleAddLine(TKey key, TValue value) + { + this.data.TryAdd(key, value); + } + + private IEnumerable GenerateDataLines() + { + foreach (KeyValuePair kvp in this.data) + { + yield return this.FormatAddLine(JsonConvert.SerializeObject(kvp).Trim()); + } + } + } +} diff --git a/Scalar.Common/FileBasedLock.cs b/Scalar.Common/FileBasedLock.cs index d139097687..e888df5780 100644 --- a/Scalar.Common/FileBasedLock.cs +++ b/Scalar.Common/FileBasedLock.cs @@ -1,27 +1,27 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; - -namespace Scalar.Common -{ - public abstract class FileBasedLock : IDisposable - { - public FileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - { - this.FileSystem = fileSystem; - this.Tracer = tracer; - this.LockPath = lockPath; - } - - protected PhysicalFileSystem FileSystem { get; } - protected string LockPath { get; } - protected ITracer Tracer { get; } - - public abstract bool TryAcquireLock(); - - public abstract void Dispose(); - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; + +namespace Scalar.Common +{ + public abstract class FileBasedLock : IDisposable + { + public FileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + { + this.FileSystem = fileSystem; + this.Tracer = tracer; + this.LockPath = lockPath; + } + + protected PhysicalFileSystem FileSystem { get; } + protected string LockPath { get; } + protected ITracer Tracer { get; } + + public abstract bool TryAcquireLock(); + + public abstract void Dispose(); + } +} diff --git a/Scalar.Common/FileSystem/DirectoryItemInfo.cs b/Scalar.Common/FileSystem/DirectoryItemInfo.cs index d8c16c9eba..1a391a7367 100644 --- a/Scalar.Common/FileSystem/DirectoryItemInfo.cs +++ b/Scalar.Common/FileSystem/DirectoryItemInfo.cs @@ -1,10 +1,10 @@ -namespace Scalar.Common.FileSystem -{ - public class DirectoryItemInfo - { - public string Name { get; set; } - public string FullName { get; set; } - public long Length { get; set; } - public bool IsDirectory { get; set; } - } -} +namespace Scalar.Common.FileSystem +{ + public class DirectoryItemInfo + { + public string Name { get; set; } + public string FullName { get; set; } + public long Length { get; set; } + public bool IsDirectory { get; set; } + } +} diff --git a/Scalar.Common/FileSystem/FileProperties.cs b/Scalar.Common/FileSystem/FileProperties.cs index 7f99d06bc7..934758226e 100644 --- a/Scalar.Common/FileSystem/FileProperties.cs +++ b/Scalar.Common/FileSystem/FileProperties.cs @@ -1,26 +1,26 @@ -using System; -using System.IO; - -namespace Scalar.Common.FileSystem -{ - public class FileProperties - { - public static readonly FileProperties DefaultFile = new FileProperties(FileAttributes.Normal, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); - public static readonly FileProperties DefaultDirectory = new FileProperties(FileAttributes.Directory, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); - - public FileProperties(FileAttributes attributes, DateTime creationTimeUTC, DateTime lastAccessTimeUTC, DateTime lastWriteTimeUTC, long length) - { - this.FileAttributes = attributes; - this.CreationTimeUTC = creationTimeUTC; - this.LastAccessTimeUTC = lastAccessTimeUTC; - this.LastWriteTimeUTC = lastWriteTimeUTC; - this.Length = length; - } - - public FileAttributes FileAttributes { get; private set; } - public DateTime CreationTimeUTC { get; private set; } - public DateTime LastAccessTimeUTC { get; private set; } - public DateTime LastWriteTimeUTC { get; private set; } - public long Length { get; private set; } - } -} +using System; +using System.IO; + +namespace Scalar.Common.FileSystem +{ + public class FileProperties + { + public static readonly FileProperties DefaultFile = new FileProperties(FileAttributes.Normal, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); + public static readonly FileProperties DefaultDirectory = new FileProperties(FileAttributes.Directory, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, 0); + + public FileProperties(FileAttributes attributes, DateTime creationTimeUTC, DateTime lastAccessTimeUTC, DateTime lastWriteTimeUTC, long length) + { + this.FileAttributes = attributes; + this.CreationTimeUTC = creationTimeUTC; + this.LastAccessTimeUTC = lastAccessTimeUTC; + this.LastWriteTimeUTC = lastWriteTimeUTC; + this.Length = length; + } + + public FileAttributes FileAttributes { get; private set; } + public DateTime CreationTimeUTC { get; private set; } + public DateTime LastAccessTimeUTC { get; private set; } + public DateTime LastWriteTimeUTC { get; private set; } + public long Length { get; private set; } + } +} diff --git a/Scalar.Common/FileSystem/FlushToDiskFileStream.cs b/Scalar.Common/FileSystem/FlushToDiskFileStream.cs index b9053ffbe3..b585712785 100644 --- a/Scalar.Common/FileSystem/FlushToDiskFileStream.cs +++ b/Scalar.Common/FileSystem/FlushToDiskFileStream.cs @@ -1,29 +1,29 @@ -using System.IO; - -namespace Scalar.Common.FileSystem -{ - public class FlushToDiskFileStream : FileStream - { - public FlushToDiskFileStream(string path, FileMode mode) - : base(path, mode) - { - } - - public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share) - : base(path, mode, access, share) - { - } - - public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) - : base(path, mode, access, share, bufferSize, options) - { - } - - public override void Flush() - { - // Ensure that all buffered data in intermediate file buffers is written to disk - // Passing in true below results in a call to FlushFileBuffers - base.Flush(true); - } - } -} +using System.IO; + +namespace Scalar.Common.FileSystem +{ + public class FlushToDiskFileStream : FileStream + { + public FlushToDiskFileStream(string path, FileMode mode) + : base(path, mode) + { + } + + public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share) + : base(path, mode, access, share) + { + } + + public FlushToDiskFileStream(string path, FileMode mode, FileAccess access, FileShare share, int bufferSize, FileOptions options) + : base(path, mode, access, share, bufferSize, options) + { + } + + public override void Flush() + { + // Ensure that all buffered data in intermediate file buffers is written to disk + // Passing in true below results in a call to FlushFileBuffers + base.Flush(true); + } + } +} diff --git a/Scalar.Common/FileSystem/HooksInstaller.cs b/Scalar.Common/FileSystem/HooksInstaller.cs index d991691115..56a0f82a7a 100644 --- a/Scalar.Common/FileSystem/HooksInstaller.cs +++ b/Scalar.Common/FileSystem/HooksInstaller.cs @@ -1,199 +1,199 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; - -namespace Scalar.Common.FileSystem -{ - public static class HooksInstaller - { - private static readonly string ExecutingDirectory; - private static readonly HookData[] NativeHooks = new[] - { - new HookData(ScalarConstants.DotGit.Hooks.ReadObjectName, ScalarConstants.DotGit.Hooks.ReadObjectPath, ScalarPlatform.Instance.Constants.ScalarReadObjectHookExecutableName), - }; - - static HooksInstaller() - { - ExecutingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - } - - public static bool InstallHooks(ScalarContext context, out string error) - { - error = string.Empty; - try - { - foreach (HookData hook in NativeHooks) - { - string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); - string targetHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + ScalarPlatform.Instance.Constants.ExecutableExtension); - if (!TryHooksInstallationAction(() => CopyHook(context, installedHookPath, targetHookPath), out error)) - { - error = "Failed to copy " + installedHookPath + "\n" + error; - return false; - } - } - } - catch (Exception e) - { - error = e.ToString(); - return false; - } - - return true; - } - - public static bool TryUpdateHooks(ScalarContext context, out string errorMessage) - { - errorMessage = string.Empty; - foreach (HookData hook in NativeHooks) - { - if (!TryUpdateHook(context, hook, out errorMessage)) - { - return false; - } - } - - return true; - } - - public static void CopyHook(ScalarContext context, string sourcePath, string destinationPath) - { - Exception ex; - if (!context.FileSystem.TryCopyToTempFileAndRename(sourcePath, destinationPath, out ex)) - { - throw new RetryableException($"Error installing {sourcePath} to {destinationPath}", ex); - } - } - - /// - /// Try to perform the specified action. The action will be retried (with backoff) up to 3 times. - /// - /// Action to perform - /// Error message - /// True if the action succeeded and false otherwise - /// This method is optimized for the hooks installation process and should not be used - /// as a generic retry mechanism. See RetryWrapper for a general purpose retry mechanism - public static bool TryHooksInstallationAction(Action action, out string errorMessage) - { - int retriesLeft = 3; - int retryWaitMillis = 500; // Will grow exponentially on each retry attempt - errorMessage = null; - - while (true) - { - try - { - action(); - return true; - } - catch (RetryableException re) - { - if (retriesLeft == 0) - { - errorMessage = re.InnerException.ToString(); - return false; - } - - Thread.Sleep(retryWaitMillis); - retriesLeft -= 1; - retryWaitMillis *= 2; - } - catch (Exception e) - { - errorMessage = e.ToString(); - return false; - } - } - } - - private static bool TryUpdateHook( - ScalarContext context, - HookData hook, - out string errorMessage) - { - bool copyHook = false; - string enlistmentHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + ScalarPlatform.Instance.Constants.ExecutableExtension); - string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); - - if (!context.FileSystem.FileExists(installedHookPath)) - { - errorMessage = hook.ExecutableName + " cannot be found at " + installedHookPath; - return false; - } - - if (!context.FileSystem.FileExists(enlistmentHookPath)) - { - copyHook = true; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "Mount"); - metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); - metadata.Add(nameof(installedHookPath), installedHookPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, hook.Name + " not found in enlistment, copying from installation folder"); - context.Tracer.RelatedWarning(hook.Name + " MissingFromEnlistment", metadata); - } - else - { - try - { - FileVersionInfo enlistmentVersion = FileVersionInfo.GetVersionInfo(enlistmentHookPath); - FileVersionInfo installedVersion = FileVersionInfo.GetVersionInfo(installedHookPath); - copyHook = enlistmentVersion.FileVersion != installedVersion.FileVersion; - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "Mount"); - metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); - metadata.Add(nameof(installedHookPath), installedHookPath); - metadata.Add("Exception", e.ToString()); - context.Tracer.RelatedError(metadata, "Failed to compare " + hook.Name + " version"); - errorMessage = "Error comparing " + hook.Name + " versions. " + ConsoleHelper.GetScalarLogMessage(context.Enlistment.EnlistmentRoot); - return false; - } - } - - if (copyHook) - { - try - { - CopyHook(context, installedHookPath, enlistmentHookPath); - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "Mount"); - metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); - metadata.Add(nameof(installedHookPath), installedHookPath); - metadata.Add("Exception", e.ToString()); - context.Tracer.RelatedError(metadata, "Failed to copy " + hook.Name + " to enlistment"); - errorMessage = "Error copying " + hook.Name + " to enlistment. " + ConsoleHelper.GetScalarLogMessage(context.Enlistment.EnlistmentRoot); - return false; - } - } - - errorMessage = null; - return true; - } - - private class HookData - { - public HookData(string name, string path, string executableName) - { - this.Name = name; - this.Path = path; - this.ExecutableName = executableName; - } - - public string Name { get; } - public string Path { get; } - public string ExecutableName { get; } - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; + +namespace Scalar.Common.FileSystem +{ + public static class HooksInstaller + { + private static readonly string ExecutingDirectory; + private static readonly HookData[] NativeHooks = new[] + { + new HookData(ScalarConstants.DotGit.Hooks.ReadObjectName, ScalarConstants.DotGit.Hooks.ReadObjectPath, ScalarPlatform.Instance.Constants.ScalarReadObjectHookExecutableName), + }; + + static HooksInstaller() + { + ExecutingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + } + + public static bool InstallHooks(ScalarContext context, out string error) + { + error = string.Empty; + try + { + foreach (HookData hook in NativeHooks) + { + string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); + string targetHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + ScalarPlatform.Instance.Constants.ExecutableExtension); + if (!TryHooksInstallationAction(() => CopyHook(context, installedHookPath, targetHookPath), out error)) + { + error = "Failed to copy " + installedHookPath + "\n" + error; + return false; + } + } + } + catch (Exception e) + { + error = e.ToString(); + return false; + } + + return true; + } + + public static bool TryUpdateHooks(ScalarContext context, out string errorMessage) + { + errorMessage = string.Empty; + foreach (HookData hook in NativeHooks) + { + if (!TryUpdateHook(context, hook, out errorMessage)) + { + return false; + } + } + + return true; + } + + public static void CopyHook(ScalarContext context, string sourcePath, string destinationPath) + { + Exception ex; + if (!context.FileSystem.TryCopyToTempFileAndRename(sourcePath, destinationPath, out ex)) + { + throw new RetryableException($"Error installing {sourcePath} to {destinationPath}", ex); + } + } + + /// + /// Try to perform the specified action. The action will be retried (with backoff) up to 3 times. + /// + /// Action to perform + /// Error message + /// True if the action succeeded and false otherwise + /// This method is optimized for the hooks installation process and should not be used + /// as a generic retry mechanism. See RetryWrapper for a general purpose retry mechanism + public static bool TryHooksInstallationAction(Action action, out string errorMessage) + { + int retriesLeft = 3; + int retryWaitMillis = 500; // Will grow exponentially on each retry attempt + errorMessage = null; + + while (true) + { + try + { + action(); + return true; + } + catch (RetryableException re) + { + if (retriesLeft == 0) + { + errorMessage = re.InnerException.ToString(); + return false; + } + + Thread.Sleep(retryWaitMillis); + retriesLeft -= 1; + retryWaitMillis *= 2; + } + catch (Exception e) + { + errorMessage = e.ToString(); + return false; + } + } + } + + private static bool TryUpdateHook( + ScalarContext context, + HookData hook, + out string errorMessage) + { + bool copyHook = false; + string enlistmentHookPath = Path.Combine(context.Enlistment.WorkingDirectoryBackingRoot, hook.Path + ScalarPlatform.Instance.Constants.ExecutableExtension); + string installedHookPath = Path.Combine(ExecutingDirectory, hook.ExecutableName); + + if (!context.FileSystem.FileExists(installedHookPath)) + { + errorMessage = hook.ExecutableName + " cannot be found at " + installedHookPath; + return false; + } + + if (!context.FileSystem.FileExists(enlistmentHookPath)) + { + copyHook = true; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); + metadata.Add(nameof(installedHookPath), installedHookPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, hook.Name + " not found in enlistment, copying from installation folder"); + context.Tracer.RelatedWarning(hook.Name + " MissingFromEnlistment", metadata); + } + else + { + try + { + FileVersionInfo enlistmentVersion = FileVersionInfo.GetVersionInfo(enlistmentHookPath); + FileVersionInfo installedVersion = FileVersionInfo.GetVersionInfo(installedHookPath); + copyHook = enlistmentVersion.FileVersion != installedVersion.FileVersion; + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); + metadata.Add(nameof(installedHookPath), installedHookPath); + metadata.Add("Exception", e.ToString()); + context.Tracer.RelatedError(metadata, "Failed to compare " + hook.Name + " version"); + errorMessage = "Error comparing " + hook.Name + " versions. " + ConsoleHelper.GetScalarLogMessage(context.Enlistment.EnlistmentRoot); + return false; + } + } + + if (copyHook) + { + try + { + CopyHook(context, installedHookPath, enlistmentHookPath); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add(nameof(enlistmentHookPath), enlistmentHookPath); + metadata.Add(nameof(installedHookPath), installedHookPath); + metadata.Add("Exception", e.ToString()); + context.Tracer.RelatedError(metadata, "Failed to copy " + hook.Name + " to enlistment"); + errorMessage = "Error copying " + hook.Name + " to enlistment. " + ConsoleHelper.GetScalarLogMessage(context.Enlistment.EnlistmentRoot); + return false; + } + } + + errorMessage = null; + return true; + } + + private class HookData + { + public HookData(string name, string path, string executableName) + { + this.Name = name; + this.Path = path; + this.ExecutableName = executableName; + } + + public string Name { get; } + public string Path { get; } + public string ExecutableName { get; } + } + } +} diff --git a/Scalar.Common/FileSystem/IPlatformFileSystem.cs b/Scalar.Common/FileSystem/IPlatformFileSystem.cs index a71686a4e6..fc9823bbe6 100644 --- a/Scalar.Common/FileSystem/IPlatformFileSystem.cs +++ b/Scalar.Common/FileSystem/IPlatformFileSystem.cs @@ -1,19 +1,19 @@ -using Scalar.Common.Tracing; - -namespace Scalar.Common.FileSystem -{ - public interface IPlatformFileSystem - { - bool SupportsFileMode { get; } - void FlushFileBuffers(string path); - void MoveAndOverwriteFile(string sourceFileName, string destinationFilename); - bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage); - void ChangeMode(string path, ushort mode); - bool HydrateFile(string fileName, byte[] buffer); - bool IsExecutable(string filePath); - bool IsSocket(string filePath); - bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error); - bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error); - bool IsFileSystemSupported(string path, out string error); - } -} +using Scalar.Common.Tracing; + +namespace Scalar.Common.FileSystem +{ + public interface IPlatformFileSystem + { + bool SupportsFileMode { get; } + void FlushFileBuffers(string path); + void MoveAndOverwriteFile(string sourceFileName, string destinationFilename); + bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage); + void ChangeMode(string path, ushort mode); + bool HydrateFile(string fileName, byte[] buffer); + bool IsExecutable(string filePath); + bool IsSocket(string filePath); + bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error); + bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error); + bool IsFileSystemSupported(string path, out string error); + } +} diff --git a/Scalar.Common/FileSystem/PhysicalFileSystem.cs b/Scalar.Common/FileSystem/PhysicalFileSystem.cs index e7b4decefd..7740de4356 100644 --- a/Scalar.Common/FileSystem/PhysicalFileSystem.cs +++ b/Scalar.Common/FileSystem/PhysicalFileSystem.cs @@ -1,506 +1,506 @@ -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Security; -using System.Threading; - -namespace Scalar.Common.FileSystem -{ - public class PhysicalFileSystem - { - public const int DefaultStreamBufferSize = 8192; - - public virtual void DeleteDirectory(string path, bool recursive = true) - { - if (!Directory.Exists(path)) - { - return; - } - - DirectoryInfo directory = new DirectoryInfo(path); - - if (recursive) - { - foreach (FileInfo file in directory.GetFiles()) - { - file.Attributes = FileAttributes.Normal; - file.Delete(); - } - - foreach (DirectoryInfo subDirectory in directory.GetDirectories()) - { - this.DeleteDirectory(subDirectory.FullName); - } - } - - directory.Delete(); - } - - public virtual void CopyDirectoryRecursive(string srcDirectoryPath, string dstDirectoryPath) - { - DirectoryInfo srcDirectory = new DirectoryInfo(srcDirectoryPath); - - if (!this.DirectoryExists(dstDirectoryPath)) - { - this.CreateDirectory(dstDirectoryPath); - } - - foreach (FileInfo file in srcDirectory.EnumerateFiles()) - { - this.CopyFile(file.FullName, Path.Combine(dstDirectoryPath, file.Name), overwrite: true); - } - - foreach (DirectoryInfo subDirectory in srcDirectory.EnumerateDirectories()) - { - this.CopyDirectoryRecursive(subDirectory.FullName, Path.Combine(dstDirectoryPath, subDirectory.Name)); - } - } - - public virtual bool FileExists(string path) - { - return File.Exists(path); - } - - public virtual bool DirectoryExists(string path) - { - return Directory.Exists(path); - } - - public virtual void CopyFile(string sourcePath, string destinationPath, bool overwrite) - { - File.Copy(sourcePath, destinationPath, overwrite); - } - - public virtual void DeleteFile(string path) - { - File.Delete(path); - } - - public virtual string ReadAllText(string path) - { - return File.ReadAllText(path); - } - - public virtual byte[] ReadAllBytes(string path) - { - return File.ReadAllBytes(path); - } - - public virtual IEnumerable ReadLines(string path) - { - return File.ReadLines(path); - } - - public virtual void WriteAllText(string path, string contents) - { - File.WriteAllText(path, contents); - } - - public virtual bool TryWriteAllText(string path, string contents) - { - try - { - this.WriteAllText(path, contents); - return true; - } - catch (IOException) - { - return false; - } - catch (UnauthorizedAccessException) - { - return false; - } - catch (SecurityException) - { - return false; - } - } - - public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, bool callFlushFileBuffers) - { - return this.OpenFileStream(path, fileMode, fileAccess, shareMode, FileOptions.None, callFlushFileBuffers); - } - - public virtual void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - ScalarPlatform.Instance.FileSystem.MoveAndOverwriteFile(sourceFileName, destinationFilename); - } - - public virtual bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) - { - return ScalarPlatform.Instance.FileSystem.TryGetNormalizedPath(path, out normalizedPath, out errorMessage); - } - - public virtual Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool callFlushFileBuffers) - { - if (callFlushFileBuffers) - { - return new FlushToDiskFileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); - } - - return new FileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); - } - - public virtual void FlushFileBuffers(string path) - { - ScalarPlatform.Instance.FileSystem.FlushFileBuffers(path); - } - - public virtual void CreateDirectory(string path) - { - Directory.CreateDirectory(path); - } - - public virtual bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) - { - return ScalarPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(directoryPath, out error); - } - - public virtual bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) - { - return ScalarPlatform.Instance.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions(tracer, directoryPath, out error); - } - - public virtual bool IsSymLink(string path) - { - return (this.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && NativeMethods.IsSymLink(path); - } - - public virtual IEnumerable ItemsInDirectory(string path) - { - DirectoryInfo ntfsDirectory = new DirectoryInfo(path); - foreach (FileSystemInfo ntfsItem in ntfsDirectory.GetFileSystemInfos()) - { - DirectoryItemInfo itemInfo = new DirectoryItemInfo() - { - FullName = ntfsItem.FullName, - Name = ntfsItem.Name, - IsDirectory = (ntfsItem.Attributes & FileAttributes.Directory) != 0 - }; - - if (!itemInfo.IsDirectory) - { - itemInfo.Length = ((FileInfo)ntfsItem).Length; - } - - yield return itemInfo; - } - } - - public virtual IEnumerable EnumerateDirectories(string path) - { - return Directory.EnumerateDirectories(path); - } - - public virtual FileProperties GetFileProperties(string path) - { - FileInfo entry = new FileInfo(path); - if (entry.Exists) - { - return new FileProperties( - entry.Attributes, - entry.CreationTimeUtc, - entry.LastAccessTimeUtc, - entry.LastWriteTimeUtc, - entry.Length); - } - else - { - return FileProperties.DefaultFile; - } - } - - public virtual FileAttributes GetAttributes(string path) - { - return File.GetAttributes(path); - } - - public virtual void SetAttributes(string path, FileAttributes fileAttributes) - { - File.SetAttributes(path, fileAttributes); - } - - public virtual void MoveFile(string sourcePath, string targetPath) - { - File.Move(sourcePath, targetPath); - } - - public virtual string[] GetFiles(string directoryPath, string mask) - { - return Directory.GetFiles(directoryPath, mask); - } - - public virtual FileVersionInfo GetVersionInfo(string path) - { - return FileVersionInfo.GetVersionInfo(path); - } - - public virtual bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) - { - return versionInfo1.FileVersion == versionInfo2.FileVersion; - } - - public virtual bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) - { - return versionInfo1.ProductVersion == versionInfo2.ProductVersion; - } - - public bool TryWriteTempFileAndRename(string destinationPath, string contents, out Exception handledException) - { - handledException = null; - string tempFilePath = destinationPath + ".temp"; - - string parentPath = Path.GetDirectoryName(tempFilePath); - this.CreateDirectory(parentPath); - - try - { - using (Stream tempFile = this.OpenFileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) - using (StreamWriter writer = new StreamWriter(tempFile)) - { - writer.Write(contents); - tempFile.Flush(); - } - - this.MoveAndOverwriteFile(tempFilePath, destinationPath); - return true; - } - catch (Win32Exception e) - { - handledException = e; - return false; - } - catch (IOException e) - { - handledException = e; - return false; - } - catch (UnauthorizedAccessException e) - { - handledException = e; - return false; - } - } - - public bool TryCopyToTempFileAndRename(string sourcePath, string destinationPath, out Exception handledException) - { - handledException = null; - string tempFilePath = destinationPath + ".temp"; - - try - { - File.Copy(sourcePath, tempFilePath, overwrite: true); - ScalarPlatform.Instance.FileSystem.FlushFileBuffers(tempFilePath); - this.MoveAndOverwriteFile(tempFilePath, destinationPath); - return true; - } - catch (Win32Exception e) - { - handledException = e; - return false; - } - catch (IOException e) - { - handledException = e; - return false; - } - catch (UnauthorizedAccessException e) - { - handledException = e; - return false; - } - } - - public bool TryCreateDirectory(string path, out Exception exception) - { - try - { - Directory.CreateDirectory(path); - } - catch (Exception e) when (e is IOException || - e is UnauthorizedAccessException || - e is ArgumentException || - e is NotSupportedException) - { - exception = e; - return false; - } - - exception = null; - return true; - } - - /// - /// Recursively deletes a directory and all contained contents. - /// - public bool TryDeleteDirectory(string path, out Exception exception) - { - try - { - this.DeleteDirectory(path); - } - catch (DirectoryNotFoundException) - { - // The directory does not exist - follow the - // convention of this class and report success - } - catch (Exception e) when (e is IOException || - e is UnauthorizedAccessException || - e is ArgumentException) - { - exception = e; - return false; - } - - exception = null; - return true; - } - - /// - /// Attempts to delete a file - /// - /// Path of file to delete - /// True if the delete succeed, and false otherwise - /// The files attributes will be set to Normal before deleting the file - public bool TryDeleteFile(string path) - { - Exception exception; - return this.TryDeleteFile(path, out exception); - } - - /// - /// Attempts to delete a file - /// - /// Path of file to delete - /// Exception thrown, if any, while attempting to delete file (or reset file attributes) - /// True if the delete succeed, and false otherwise - /// The files attributes will be set to Normal before deleting the file - public bool TryDeleteFile(string path, out Exception exception) - { - exception = null; - try - { - if (this.FileExists(path)) - { - this.SetAttributes(path, FileAttributes.Normal); - this.DeleteFile(path); - } - - return true; - } - catch (FileNotFoundException) - { - // SetAttributes could not find the file - return true; - } - catch (IOException e) - { - exception = e; - return false; - } - catch (UnauthorizedAccessException e) - { - exception = e; - return false; - } - } - - /// - /// Attempts to delete a file - /// - /// Path of file to delete - /// Prefix to be used on keys when new entries are added to the metadata - /// Metadata for recording failed deletes - /// The files attributes will be set to Normal before deleting the file - public bool TryDeleteFile(string path, string metadataKey, EventMetadata metadata) - { - Exception deleteException = null; - if (!this.TryDeleteFile(path, out deleteException)) - { - metadata.Add($"{metadataKey}_DeleteFailed", "true"); - if (deleteException != null) - { - metadata.Add($"{metadataKey}_DeleteException", deleteException.ToString()); - } - - return false; - } - - return true; - } - - /// - /// Retry delete until it succeeds (or maximum number of retries have failed) - /// - /// ITracer for logging and telemetry, can be null - /// Path of file to delete - /// - /// Amount of time to wait between each delete attempt. If 0, there will be no delays between attempts - /// - /// Maximum number of retries (if 0, a single attempt will be made) - /// - /// Number of retries to attempt before logging a failure. First and last failure is always logged if tracer is not null. - /// - /// True if the delete succeed, and false otherwise - /// The files attributes will be set to Normal before deleting the file - public bool TryWaitForDelete( - ITracer tracer, - string path, - int retryDelayMs, - int maxRetries, - int retryLoggingThreshold) - { - int failureCount = 0; - while (this.FileExists(path)) - { - Exception exception = null; - if (!this.TryDeleteFile(path, out exception)) - { - if (failureCount == maxRetries) - { - if (tracer != null) - { - EventMetadata metadata = new EventMetadata(); - if (exception != null) - { - metadata.Add("Exception", exception.ToString()); - } - - metadata.Add("path", path); - metadata.Add("failureCount", failureCount + 1); - metadata.Add("maxRetries", maxRetries); - tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file."); - } - - return false; - } - else - { - if (tracer != null && failureCount % retryLoggingThreshold == 0) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", exception.ToString()); - metadata.Add("path", path); - metadata.Add("failureCount", failureCount + 1); - metadata.Add("maxRetries", maxRetries); - tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file, retrying ..."); - } - } - - ++failureCount; - - if (retryDelayMs > 0) - { - Thread.Sleep(retryDelayMs); - } - } - } - - return true; - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Threading; + +namespace Scalar.Common.FileSystem +{ + public class PhysicalFileSystem + { + public const int DefaultStreamBufferSize = 8192; + + public virtual void DeleteDirectory(string path, bool recursive = true) + { + if (!Directory.Exists(path)) + { + return; + } + + DirectoryInfo directory = new DirectoryInfo(path); + + if (recursive) + { + foreach (FileInfo file in directory.GetFiles()) + { + file.Attributes = FileAttributes.Normal; + file.Delete(); + } + + foreach (DirectoryInfo subDirectory in directory.GetDirectories()) + { + this.DeleteDirectory(subDirectory.FullName); + } + } + + directory.Delete(); + } + + public virtual void CopyDirectoryRecursive(string srcDirectoryPath, string dstDirectoryPath) + { + DirectoryInfo srcDirectory = new DirectoryInfo(srcDirectoryPath); + + if (!this.DirectoryExists(dstDirectoryPath)) + { + this.CreateDirectory(dstDirectoryPath); + } + + foreach (FileInfo file in srcDirectory.EnumerateFiles()) + { + this.CopyFile(file.FullName, Path.Combine(dstDirectoryPath, file.Name), overwrite: true); + } + + foreach (DirectoryInfo subDirectory in srcDirectory.EnumerateDirectories()) + { + this.CopyDirectoryRecursive(subDirectory.FullName, Path.Combine(dstDirectoryPath, subDirectory.Name)); + } + } + + public virtual bool FileExists(string path) + { + return File.Exists(path); + } + + public virtual bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + public virtual void CopyFile(string sourcePath, string destinationPath, bool overwrite) + { + File.Copy(sourcePath, destinationPath, overwrite); + } + + public virtual void DeleteFile(string path) + { + File.Delete(path); + } + + public virtual string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public virtual byte[] ReadAllBytes(string path) + { + return File.ReadAllBytes(path); + } + + public virtual IEnumerable ReadLines(string path) + { + return File.ReadLines(path); + } + + public virtual void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); + } + + public virtual bool TryWriteAllText(string path, string contents) + { + try + { + this.WriteAllText(path, contents); + return true; + } + catch (IOException) + { + return false; + } + catch (UnauthorizedAccessException) + { + return false; + } + catch (SecurityException) + { + return false; + } + } + + public Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, bool callFlushFileBuffers) + { + return this.OpenFileStream(path, fileMode, fileAccess, shareMode, FileOptions.None, callFlushFileBuffers); + } + + public virtual void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + ScalarPlatform.Instance.FileSystem.MoveAndOverwriteFile(sourceFileName, destinationFilename); + } + + public virtual bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) + { + return ScalarPlatform.Instance.FileSystem.TryGetNormalizedPath(path, out normalizedPath, out errorMessage); + } + + public virtual Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool callFlushFileBuffers) + { + if (callFlushFileBuffers) + { + return new FlushToDiskFileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); + } + + return new FileStream(path, fileMode, fileAccess, shareMode, DefaultStreamBufferSize, options); + } + + public virtual void FlushFileBuffers(string path) + { + ScalarPlatform.Instance.FileSystem.FlushFileBuffers(path); + } + + public virtual void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } + + public virtual bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) + { + return ScalarPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(directoryPath, out error); + } + + public virtual bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) + { + return ScalarPlatform.Instance.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions(tracer, directoryPath, out error); + } + + public virtual bool IsSymLink(string path) + { + return (this.GetAttributes(path) & FileAttributes.ReparsePoint) == FileAttributes.ReparsePoint && NativeMethods.IsSymLink(path); + } + + public virtual IEnumerable ItemsInDirectory(string path) + { + DirectoryInfo ntfsDirectory = new DirectoryInfo(path); + foreach (FileSystemInfo ntfsItem in ntfsDirectory.GetFileSystemInfos()) + { + DirectoryItemInfo itemInfo = new DirectoryItemInfo() + { + FullName = ntfsItem.FullName, + Name = ntfsItem.Name, + IsDirectory = (ntfsItem.Attributes & FileAttributes.Directory) != 0 + }; + + if (!itemInfo.IsDirectory) + { + itemInfo.Length = ((FileInfo)ntfsItem).Length; + } + + yield return itemInfo; + } + } + + public virtual IEnumerable EnumerateDirectories(string path) + { + return Directory.EnumerateDirectories(path); + } + + public virtual FileProperties GetFileProperties(string path) + { + FileInfo entry = new FileInfo(path); + if (entry.Exists) + { + return new FileProperties( + entry.Attributes, + entry.CreationTimeUtc, + entry.LastAccessTimeUtc, + entry.LastWriteTimeUtc, + entry.Length); + } + else + { + return FileProperties.DefaultFile; + } + } + + public virtual FileAttributes GetAttributes(string path) + { + return File.GetAttributes(path); + } + + public virtual void SetAttributes(string path, FileAttributes fileAttributes) + { + File.SetAttributes(path, fileAttributes); + } + + public virtual void MoveFile(string sourcePath, string targetPath) + { + File.Move(sourcePath, targetPath); + } + + public virtual string[] GetFiles(string directoryPath, string mask) + { + return Directory.GetFiles(directoryPath, mask); + } + + public virtual FileVersionInfo GetVersionInfo(string path) + { + return FileVersionInfo.GetVersionInfo(path); + } + + public virtual bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) + { + return versionInfo1.FileVersion == versionInfo2.FileVersion; + } + + public virtual bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) + { + return versionInfo1.ProductVersion == versionInfo2.ProductVersion; + } + + public bool TryWriteTempFileAndRename(string destinationPath, string contents, out Exception handledException) + { + handledException = null; + string tempFilePath = destinationPath + ".temp"; + + string parentPath = Path.GetDirectoryName(tempFilePath); + this.CreateDirectory(parentPath); + + try + { + using (Stream tempFile = this.OpenFileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None, callFlushFileBuffers: true)) + using (StreamWriter writer = new StreamWriter(tempFile)) + { + writer.Write(contents); + tempFile.Flush(); + } + + this.MoveAndOverwriteFile(tempFilePath, destinationPath); + return true; + } + catch (Win32Exception e) + { + handledException = e; + return false; + } + catch (IOException e) + { + handledException = e; + return false; + } + catch (UnauthorizedAccessException e) + { + handledException = e; + return false; + } + } + + public bool TryCopyToTempFileAndRename(string sourcePath, string destinationPath, out Exception handledException) + { + handledException = null; + string tempFilePath = destinationPath + ".temp"; + + try + { + File.Copy(sourcePath, tempFilePath, overwrite: true); + ScalarPlatform.Instance.FileSystem.FlushFileBuffers(tempFilePath); + this.MoveAndOverwriteFile(tempFilePath, destinationPath); + return true; + } + catch (Win32Exception e) + { + handledException = e; + return false; + } + catch (IOException e) + { + handledException = e; + return false; + } + catch (UnauthorizedAccessException e) + { + handledException = e; + return false; + } + } + + public bool TryCreateDirectory(string path, out Exception exception) + { + try + { + Directory.CreateDirectory(path); + } + catch (Exception e) when (e is IOException || + e is UnauthorizedAccessException || + e is ArgumentException || + e is NotSupportedException) + { + exception = e; + return false; + } + + exception = null; + return true; + } + + /// + /// Recursively deletes a directory and all contained contents. + /// + public bool TryDeleteDirectory(string path, out Exception exception) + { + try + { + this.DeleteDirectory(path); + } + catch (DirectoryNotFoundException) + { + // The directory does not exist - follow the + // convention of this class and report success + } + catch (Exception e) when (e is IOException || + e is UnauthorizedAccessException || + e is ArgumentException) + { + exception = e; + return false; + } + + exception = null; + return true; + } + + /// + /// Attempts to delete a file + /// + /// Path of file to delete + /// True if the delete succeed, and false otherwise + /// The files attributes will be set to Normal before deleting the file + public bool TryDeleteFile(string path) + { + Exception exception; + return this.TryDeleteFile(path, out exception); + } + + /// + /// Attempts to delete a file + /// + /// Path of file to delete + /// Exception thrown, if any, while attempting to delete file (or reset file attributes) + /// True if the delete succeed, and false otherwise + /// The files attributes will be set to Normal before deleting the file + public bool TryDeleteFile(string path, out Exception exception) + { + exception = null; + try + { + if (this.FileExists(path)) + { + this.SetAttributes(path, FileAttributes.Normal); + this.DeleteFile(path); + } + + return true; + } + catch (FileNotFoundException) + { + // SetAttributes could not find the file + return true; + } + catch (IOException e) + { + exception = e; + return false; + } + catch (UnauthorizedAccessException e) + { + exception = e; + return false; + } + } + + /// + /// Attempts to delete a file + /// + /// Path of file to delete + /// Prefix to be used on keys when new entries are added to the metadata + /// Metadata for recording failed deletes + /// The files attributes will be set to Normal before deleting the file + public bool TryDeleteFile(string path, string metadataKey, EventMetadata metadata) + { + Exception deleteException = null; + if (!this.TryDeleteFile(path, out deleteException)) + { + metadata.Add($"{metadataKey}_DeleteFailed", "true"); + if (deleteException != null) + { + metadata.Add($"{metadataKey}_DeleteException", deleteException.ToString()); + } + + return false; + } + + return true; + } + + /// + /// Retry delete until it succeeds (or maximum number of retries have failed) + /// + /// ITracer for logging and telemetry, can be null + /// Path of file to delete + /// + /// Amount of time to wait between each delete attempt. If 0, there will be no delays between attempts + /// + /// Maximum number of retries (if 0, a single attempt will be made) + /// + /// Number of retries to attempt before logging a failure. First and last failure is always logged if tracer is not null. + /// + /// True if the delete succeed, and false otherwise + /// The files attributes will be set to Normal before deleting the file + public bool TryWaitForDelete( + ITracer tracer, + string path, + int retryDelayMs, + int maxRetries, + int retryLoggingThreshold) + { + int failureCount = 0; + while (this.FileExists(path)) + { + Exception exception = null; + if (!this.TryDeleteFile(path, out exception)) + { + if (failureCount == maxRetries) + { + if (tracer != null) + { + EventMetadata metadata = new EventMetadata(); + if (exception != null) + { + metadata.Add("Exception", exception.ToString()); + } + + metadata.Add("path", path); + metadata.Add("failureCount", failureCount + 1); + metadata.Add("maxRetries", maxRetries); + tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file."); + } + + return false; + } + else + { + if (tracer != null && failureCount % retryLoggingThreshold == 0) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", exception.ToString()); + metadata.Add("path", path); + metadata.Add("failureCount", failureCount + 1); + metadata.Add("maxRetries", maxRetries); + tracer.RelatedWarning(metadata, $"{nameof(this.TryWaitForDelete)}: Failed to delete file, retrying ..."); + } + } + + ++failureCount; + + if (retryDelayMs > 0) + { + Thread.Sleep(retryDelayMs); + } + } + } + + return true; + } + } +} diff --git a/Scalar.Common/Git/DiffTreeResult.cs b/Scalar.Common/Git/DiffTreeResult.cs index 0b2a9a0921..8ec70fab91 100644 --- a/Scalar.Common/Git/DiffTreeResult.cs +++ b/Scalar.Common/Git/DiffTreeResult.cs @@ -1,220 +1,220 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.Common.Git -{ - public class DiffTreeResult - { - public const string TreeMarker = "tree "; - public const string BlobMarker = "blob "; - - public const int TypeMarkerStartIndex = 7; - - private const ushort SymLinkFileIndexEntry = 0xA000; - - private static readonly HashSet ValidTreeModes = new HashSet() { "040000" }; - - public enum Operations - { - Unknown, - CopyEdit, - RenameEdit, - Modify, - Delete, - Add, - Unmerged, - TypeChange, - } - - public Operations Operation { get; set; } - public bool SourceIsDirectory { get; set; } - public bool TargetIsDirectory { get; set; } - public bool TargetIsSymLink { get; set; } - public string TargetPath { get; set; } - public string SourceSha { get; set; } - public string TargetSha { get; set; } - public ushort SourceMode { get; set; } - public ushort TargetMode { get; set; } - - public static DiffTreeResult ParseFromDiffTreeLine(string line) - { - if (string.IsNullOrEmpty(line)) - { - throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); - } - - /* - * The lines passed to this method should be the result of a call to git diff-tree -r -t (sourceTreeish) (targetTreeish) - * - * Example output lines from git diff-tree - * :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tScalar/FastFetch/Git - * :000000 100644 0000000000000000000000000000000000000000 cdc036f9d561f14d908e0a0c337105b53c778e5e A\tScalar/FastFetch/Git/FastFetchGitObjects.cs - * :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI - * :100644 000000 1242fc97c612ff286a5f1221d569508600ca5e06 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI/Scalar.CLI.csproj - * :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tScalar/Scalar.Common - * :100644 100644 57d9c737c8a48632cfbb12cae00c97d512b9f155 524d7dbcebd33e4007c52711d3f21b17373de454 M\tScalar/Scalar.Common/Scalar.Common.csproj - * ^-[0] ^-[1] ^-[2] ^-[3] ^-[4] - * ^-tab - * ^-[5] - * - * This output will only happen if -C or -M is passed to the diff-tree command - * Since we are not passing those options we shouldn't have to handle this format. - * :100644 100644 3ac7d60a25bb772af1d5843c76e8a070c062dc5d c31a95125b8a6efd401488839a7ed1288ce01634 R094\tScalar/Scalar.CLI/CommandLine/CloneVerb.cs\tScalar/Scalar/CommandLine/CloneVerb.cs - */ - - if (!line.StartsWith(":")) - { - throw new ArgumentException($"diff-tree lines should start with a :", nameof(line)); - } - - // Skip the colon at the front - line = line.Substring(1); - - // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. - // Splitting on \t will give us the mode, sha, operation in parts[0] and that path in parts[1] and optionally in paths[2] - string[] parts = line.Split(new[] { '\t' }, count: 2); - - // Take the mode, sha, operation part and split on a space then add the paths that were split on a tab to the end - parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); - - if (parts.Length != 6 || - parts[5].Contains('\t')) - { - // Look at file history to see how -C -M with 7 parts could be handled - throw new ArgumentException($"diff-tree lines should have 6 parts unless passed -C or -M which this method doesn't handle", nameof(line)); - } - - DiffTreeResult result = new DiffTreeResult(); - result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); - result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); - result.SourceMode = Convert.ToUInt16(parts[0], 8); - result.TargetMode = Convert.ToUInt16(parts[1], 8); - - if (!result.TargetIsDirectory) - { - result.TargetIsSymLink = result.TargetMode == SymLinkFileIndexEntry; - } - - result.SourceSha = parts[2]; - result.TargetSha = parts[3]; - result.Operation = DiffTreeResult.ParseOperation(parts[4]); - result.TargetPath = ConvertPathToUtf8Path(parts[5]); - if (result.TargetIsDirectory || result.SourceIsDirectory) - { - // Since diff-tree is not doing rename detection, file->directory or directory->file transformations are always multiple lines - // with a delete line and an add line - // :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tScalar/FastFetch/Git - // :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tScalar/Scalar.Common - // :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI - result.TargetPath = AppendPathSeparatorIfNeeded(result.TargetPath); - } - - return result; - } - - /// - /// Parse the output of calling git ls-tree - /// - /// A line that was output from calling git ls-tree - /// A DiffTreeResult build from the output line - /// - /// The call to ls-tree could be any of the following - /// git ls-tree (treeish) - /// git ls-tree -r (treeish) - /// git ls-tree -t (treeish) - /// git ls-tree -r -t (treeish) - /// - public static DiffTreeResult ParseFromLsTreeLine(string line) - { - if (string.IsNullOrEmpty(line)) - { - throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); - } - - /* - * Example output lines from ls-tree - * - * 040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar - * 100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md - * 100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts/BuildScalarForMac.sh - * ^-mode ^-marker ^-tab - * ^-sha ^-path - */ - - // Everything from ls-tree is an add. - if (IsLsTreeLineOfType(line, TreeMarker)) - { - DiffTreeResult treeAdd = new DiffTreeResult(); - treeAdd.TargetIsDirectory = true; - treeAdd.TargetPath = AppendPathSeparatorIfNeeded(ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1))); - treeAdd.Operation = DiffTreeResult.Operations.Add; - - return treeAdd; - } - else - { - if (IsLsTreeLineOfType(line, BlobMarker)) - { - DiffTreeResult blobAdd = new DiffTreeResult(); - blobAdd.TargetMode = Convert.ToUInt16(line.Substring(0, 6), 8); - blobAdd.TargetIsSymLink = blobAdd.TargetMode == SymLinkFileIndexEntry; - blobAdd.TargetSha = line.Substring(TypeMarkerStartIndex + BlobMarker.Length, ScalarConstants.ShaStringLength); - blobAdd.TargetPath = ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1)); - blobAdd.Operation = DiffTreeResult.Operations.Add; - - return blobAdd; - } - else - { - return null; - } - } - } - - public static bool IsLsTreeLineOfType(string line, string typeMarker) - { - if (line.Length <= TypeMarkerStartIndex + typeMarker.Length) - { - return false; - } - - return line.IndexOf(typeMarker, TypeMarkerStartIndex, typeMarker.Length, StringComparison.OrdinalIgnoreCase) == TypeMarkerStartIndex; - } - - private static string AppendPathSeparatorIfNeeded(string path) - { - return path.Last() == Path.DirectorySeparatorChar ? path : path + Path.DirectorySeparatorChar; - } - - private static Operations ParseOperation(string gitOperationString) - { - switch (gitOperationString) - { - case "U": return Operations.Unmerged; - case "M": return Operations.Modify; - case "A": return Operations.Add; - case "D": return Operations.Delete; - case "X": return Operations.Unknown; - case "T": return Operations.TypeChange; - default: - if (gitOperationString.StartsWith("C")) - { - return Operations.CopyEdit; - } - else if (gitOperationString.StartsWith("R")) - { - return Operations.RenameEdit; - } - - throw new InvalidDataException("Unrecognized diff-tree operation: " + gitOperationString); - } - } - - private static string ConvertPathToUtf8Path(string relativePath) - { - return GitPathConverter.ConvertPathOctetsToUtf8(relativePath.Trim('"')).Replace(ScalarConstants.GitPathSeparator, Path.DirectorySeparatorChar); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.Common.Git +{ + public class DiffTreeResult + { + public const string TreeMarker = "tree "; + public const string BlobMarker = "blob "; + + public const int TypeMarkerStartIndex = 7; + + private const ushort SymLinkFileIndexEntry = 0xA000; + + private static readonly HashSet ValidTreeModes = new HashSet() { "040000" }; + + public enum Operations + { + Unknown, + CopyEdit, + RenameEdit, + Modify, + Delete, + Add, + Unmerged, + TypeChange, + } + + public Operations Operation { get; set; } + public bool SourceIsDirectory { get; set; } + public bool TargetIsDirectory { get; set; } + public bool TargetIsSymLink { get; set; } + public string TargetPath { get; set; } + public string SourceSha { get; set; } + public string TargetSha { get; set; } + public ushort SourceMode { get; set; } + public ushort TargetMode { get; set; } + + public static DiffTreeResult ParseFromDiffTreeLine(string line) + { + if (string.IsNullOrEmpty(line)) + { + throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); + } + + /* + * The lines passed to this method should be the result of a call to git diff-tree -r -t (sourceTreeish) (targetTreeish) + * + * Example output lines from git diff-tree + * :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tScalar/FastFetch/Git + * :000000 100644 0000000000000000000000000000000000000000 cdc036f9d561f14d908e0a0c337105b53c778e5e A\tScalar/FastFetch/Git/FastFetchGitObjects.cs + * :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI + * :100644 000000 1242fc97c612ff286a5f1221d569508600ca5e06 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI/Scalar.CLI.csproj + * :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tScalar/Scalar.Common + * :100644 100644 57d9c737c8a48632cfbb12cae00c97d512b9f155 524d7dbcebd33e4007c52711d3f21b17373de454 M\tScalar/Scalar.Common/Scalar.Common.csproj + * ^-[0] ^-[1] ^-[2] ^-[3] ^-[4] + * ^-tab + * ^-[5] + * + * This output will only happen if -C or -M is passed to the diff-tree command + * Since we are not passing those options we shouldn't have to handle this format. + * :100644 100644 3ac7d60a25bb772af1d5843c76e8a070c062dc5d c31a95125b8a6efd401488839a7ed1288ce01634 R094\tScalar/Scalar.CLI/CommandLine/CloneVerb.cs\tScalar/Scalar/CommandLine/CloneVerb.cs + */ + + if (!line.StartsWith(":")) + { + throw new ArgumentException($"diff-tree lines should start with a :", nameof(line)); + } + + // Skip the colon at the front + line = line.Substring(1); + + // Filenames may contain spaces, but always follow a \t. Other fields are space delimited. + // Splitting on \t will give us the mode, sha, operation in parts[0] and that path in parts[1] and optionally in paths[2] + string[] parts = line.Split(new[] { '\t' }, count: 2); + + // Take the mode, sha, operation part and split on a space then add the paths that were split on a tab to the end + parts = parts[0].Split(' ').Concat(parts.Skip(1)).ToArray(); + + if (parts.Length != 6 || + parts[5].Contains('\t')) + { + // Look at file history to see how -C -M with 7 parts could be handled + throw new ArgumentException($"diff-tree lines should have 6 parts unless passed -C or -M which this method doesn't handle", nameof(line)); + } + + DiffTreeResult result = new DiffTreeResult(); + result.SourceIsDirectory = ValidTreeModes.Contains(parts[0]); + result.TargetIsDirectory = ValidTreeModes.Contains(parts[1]); + result.SourceMode = Convert.ToUInt16(parts[0], 8); + result.TargetMode = Convert.ToUInt16(parts[1], 8); + + if (!result.TargetIsDirectory) + { + result.TargetIsSymLink = result.TargetMode == SymLinkFileIndexEntry; + } + + result.SourceSha = parts[2]; + result.TargetSha = parts[3]; + result.Operation = DiffTreeResult.ParseOperation(parts[4]); + result.TargetPath = ConvertPathToUtf8Path(parts[5]); + if (result.TargetIsDirectory || result.SourceIsDirectory) + { + // Since diff-tree is not doing rename detection, file->directory or directory->file transformations are always multiple lines + // with a delete line and an add line + // :000000 040000 0000000000000000000000000000000000000000 cee82f9d431bf610404f67bcdda3fee76f0c1dd5 A\tScalar/FastFetch/Git + // :040000 040000 3823348f91113a619eed8f48fe597cc9c7d088d8 fd56ff77b12a0b76567cb55ed4950272eac8b8f6 M\tScalar/Scalar.Common + // :040000 000000 f68b90da732791438d67c0326997a2d26e4c2de4 0000000000000000000000000000000000000000 D\tScalar/Scalar.CLI + result.TargetPath = AppendPathSeparatorIfNeeded(result.TargetPath); + } + + return result; + } + + /// + /// Parse the output of calling git ls-tree + /// + /// A line that was output from calling git ls-tree + /// A DiffTreeResult build from the output line + /// + /// The call to ls-tree could be any of the following + /// git ls-tree (treeish) + /// git ls-tree -r (treeish) + /// git ls-tree -t (treeish) + /// git ls-tree -r -t (treeish) + /// + public static DiffTreeResult ParseFromLsTreeLine(string line) + { + if (string.IsNullOrEmpty(line)) + { + throw new ArgumentException("Line to parse cannot be null or empty", nameof(line)); + } + + /* + * Example output lines from ls-tree + * + * 040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar + * 100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md + * 100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts/BuildScalarForMac.sh + * ^-mode ^-marker ^-tab + * ^-sha ^-path + */ + + // Everything from ls-tree is an add. + if (IsLsTreeLineOfType(line, TreeMarker)) + { + DiffTreeResult treeAdd = new DiffTreeResult(); + treeAdd.TargetIsDirectory = true; + treeAdd.TargetPath = AppendPathSeparatorIfNeeded(ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1))); + treeAdd.Operation = DiffTreeResult.Operations.Add; + + return treeAdd; + } + else + { + if (IsLsTreeLineOfType(line, BlobMarker)) + { + DiffTreeResult blobAdd = new DiffTreeResult(); + blobAdd.TargetMode = Convert.ToUInt16(line.Substring(0, 6), 8); + blobAdd.TargetIsSymLink = blobAdd.TargetMode == SymLinkFileIndexEntry; + blobAdd.TargetSha = line.Substring(TypeMarkerStartIndex + BlobMarker.Length, ScalarConstants.ShaStringLength); + blobAdd.TargetPath = ConvertPathToUtf8Path(line.Substring(line.LastIndexOf("\t") + 1)); + blobAdd.Operation = DiffTreeResult.Operations.Add; + + return blobAdd; + } + else + { + return null; + } + } + } + + public static bool IsLsTreeLineOfType(string line, string typeMarker) + { + if (line.Length <= TypeMarkerStartIndex + typeMarker.Length) + { + return false; + } + + return line.IndexOf(typeMarker, TypeMarkerStartIndex, typeMarker.Length, StringComparison.OrdinalIgnoreCase) == TypeMarkerStartIndex; + } + + private static string AppendPathSeparatorIfNeeded(string path) + { + return path.Last() == Path.DirectorySeparatorChar ? path : path + Path.DirectorySeparatorChar; + } + + private static Operations ParseOperation(string gitOperationString) + { + switch (gitOperationString) + { + case "U": return Operations.Unmerged; + case "M": return Operations.Modify; + case "A": return Operations.Add; + case "D": return Operations.Delete; + case "X": return Operations.Unknown; + case "T": return Operations.TypeChange; + default: + if (gitOperationString.StartsWith("C")) + { + return Operations.CopyEdit; + } + else if (gitOperationString.StartsWith("R")) + { + return Operations.RenameEdit; + } + + throw new InvalidDataException("Unrecognized diff-tree operation: " + gitOperationString); + } + } + + private static string ConvertPathToUtf8Path(string relativePath) + { + return GitPathConverter.ConvertPathOctetsToUtf8(relativePath.Trim('"')).Replace(ScalarConstants.GitPathSeparator, Path.DirectorySeparatorChar); + } + } +} diff --git a/Scalar.Common/Git/EndianHelper.cs b/Scalar.Common/Git/EndianHelper.cs index 2f528d257b..60b5de805a 100644 --- a/Scalar.Common/Git/EndianHelper.cs +++ b/Scalar.Common/Git/EndianHelper.cs @@ -1,48 +1,48 @@ -namespace Scalar.Common.Git -{ - public static class EndianHelper - { - public static short Swap(short source) - { - return (short)Swap((ushort)source); - } - - public static int Swap(int source) - { - return (int)Swap((uint)source); - } - - public static long Swap(long source) - { - return (long)((ulong)source); - } - - public static ushort Swap(ushort source) - { - return (ushort)(((source & 0x000000FF) << 8) | - ((source & 0x0000FF00) >> 8)); - } - - public static uint Swap(uint source) - { - return - ((source & 0x000000FF) << 24) | - ((source & 0x0000FF00) << 8) | - ((source & 0x00FF0000) >> 8) | - ((source & 0xFF000000) >> 24); - } - - public static ulong Swap(ulong source) - { - return - ((source & 0x00000000000000FF) << 56) | - ((source & 0x000000000000FF00) << 40) | - ((source & 0x0000000000FF0000) << 24) | - ((source & 0x00000000FF000000) << 8) | - ((source & 0x000000FF00000000) >> 8) | - ((source & 0x0000FF0000000000) >> 24) | - ((source & 0x00FF000000000000) >> 40) | - ((source & 0xFF00000000000000) >> 56); - } - } -} +namespace Scalar.Common.Git +{ + public static class EndianHelper + { + public static short Swap(short source) + { + return (short)Swap((ushort)source); + } + + public static int Swap(int source) + { + return (int)Swap((uint)source); + } + + public static long Swap(long source) + { + return (long)((ulong)source); + } + + public static ushort Swap(ushort source) + { + return (ushort)(((source & 0x000000FF) << 8) | + ((source & 0x0000FF00) >> 8)); + } + + public static uint Swap(uint source) + { + return + ((source & 0x000000FF) << 24) | + ((source & 0x0000FF00) << 8) | + ((source & 0x00FF0000) >> 8) | + ((source & 0xFF000000) >> 24); + } + + public static ulong Swap(ulong source) + { + return + ((source & 0x00000000000000FF) << 56) | + ((source & 0x000000000000FF00) << 40) | + ((source & 0x0000000000FF0000) << 24) | + ((source & 0x00000000FF000000) << 8) | + ((source & 0x000000FF00000000) >> 8) | + ((source & 0x0000FF0000000000) >> 24) | + ((source & 0x00FF000000000000) >> 40) | + ((source & 0xFF00000000000000) >> 56); + } + } +} diff --git a/Scalar.Common/Git/GitAuthentication.cs b/Scalar.Common/Git/GitAuthentication.cs index 5d113f3c81..e6b2c43da6 100644 --- a/Scalar.Common/Git/GitAuthentication.cs +++ b/Scalar.Common/Git/GitAuthentication.cs @@ -1,327 +1,327 @@ -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Security.Cryptography.X509Certificates; -using System.Text; - -namespace Scalar.Common.Git -{ - public class GitAuthentication - { - private const double MaxBackoffSeconds = 30; - - private readonly object gitAuthLock = new object(); - private readonly ICredentialStore credentialStore; - private readonly string repoUrl; - - private int numberOfAttempts = 0; - private DateTime lastAuthAttempt = DateTime.MinValue; - - private string cachedCredentialString; - private bool isCachedCredentialStringApproved = false; - - private bool isInitialized; - - public GitAuthentication(GitProcess git, string repoUrl) - { - this.credentialStore = git; - this.repoUrl = repoUrl; - - if (git.TryGetConfigUrlMatch("http", this.repoUrl, out Dictionary configSettings)) - { - this.GitSsl = new GitSsl(configSettings); - } - } - - public bool IsBackingOff - { - get - { - return this.GetNextAuthAttemptTime() > DateTime.Now; - } - } - - public bool IsAnonymous { get; private set; } = true; - - private GitSsl GitSsl { get; } - - public void ApproveCredentials(ITracer tracer, string credentialString) - { - lock (this.gitAuthLock) - { - // Don't reset the backoff if this is for a different credential than we have cached - if (credentialString == this.cachedCredentialString) - { - this.numberOfAttempts = 0; - this.lastAuthAttempt = DateTime.MinValue; - - // Tell Git to store the valid credential if we haven't already - // done so for this cached credential. - if (!this.isCachedCredentialStringApproved) - { - string username; - string password; - if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) - { - if (!this.credentialStore.TryStoreCredential(tracer, this.repoUrl, username, password, out string error)) - { - // Storing credentials is best effort attempt - log failure, but do not fail - tracer.RelatedWarning("Failed to store credential string: {0}", error); - } - - this.isCachedCredentialStringApproved = true; - } - else - { - EventMetadata metadata = new EventMetadata(new Dictionary - { - ["RepoUrl"] = this.repoUrl, - }); - tracer.RelatedError(metadata, "Failed to parse credential string for approval"); - } - } - } - } - } - - public void RejectCredentials(ITracer tracer, string credentialString) - { - lock (this.gitAuthLock) - { - // Don't stomp a different credential - if (credentialString == this.cachedCredentialString && this.cachedCredentialString != null) - { - // If we can we should pass the actual username/password values we used (and found to be invalid) - // to `git-credential reject` so the credential helpers can attempt to check if they're erasing - // the expected credentials, if they so choose to. - string username; - string password; - if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) - { - if (!this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username, password, out string error)) - { - // Deleting credentials is best effort attempt - log failure, but do not fail - tracer.RelatedWarning("Failed to delete credential string: {0}", error); - } - } - else - { - // We failed to parse the credential string so instead (as a recovery) we try to erase without - // specifying the particular username/password. - EventMetadata metadata = new EventMetadata(new Dictionary - { - ["RepoUrl"] = this.repoUrl, - }); - tracer.RelatedWarning(metadata, "Failed to parse credential string for rejection. Rejecting any credential for this repo URL."); - this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username: null, password: null, error: out string error); - } - - this.cachedCredentialString = null; - this.isCachedCredentialStringApproved = false; - this.UpdateBackoff(); - } - } - } - - public bool TryGetCredentials(ITracer tracer, out string credentialString, out string errorMessage) - { - if (!this.isInitialized) - { - throw new InvalidOperationException("This auth instance must be initialized before it can be used"); - } - - credentialString = this.cachedCredentialString; - if (credentialString == null) - { - lock (this.gitAuthLock) - { - if (this.cachedCredentialString == null) - { - if (this.IsBackingOff) - { - errorMessage = "Auth failed. No retries will be made until: " + this.GetNextAuthAttemptTime(); - return false; - } - - if (!this.TryCallGitCredential(tracer, out errorMessage)) - { - return false; - } - } - - credentialString = this.cachedCredentialString; - } - } - - errorMessage = null; - return true; - } - - public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage) - { - if (this.isInitialized) - { - throw new InvalidOperationException("Already initialized"); - } - - errorMessage = null; - - bool isAnonymous; - if (!this.TryAnonymousQuery(tracer, enlistment, out isAnonymous)) - { - errorMessage = $"Unable to determine if authentication is required"; - return false; - } - - if (!isAnonymous && - !this.TryCallGitCredential(tracer, out errorMessage)) - { - return false; - } - - this.IsAnonymous = isAnonymous; - this.isInitialized = true; - return true; - } - - public bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) - { - if (this.isInitialized) - { - throw new InvalidOperationException("Already initialized"); - } - - if (this.TryCallGitCredential(tracer, out errorMessage)) - { - this.isInitialized = true; - return true; - } - - return false; - } - - public void ConfigureHttpClientHandlerSslIfNeeded(ITracer tracer, HttpClientHandler httpClientHandler, GitProcess gitProcess) - { - X509Certificate2 cert = this.GitSsl?.GetCertificate(tracer, gitProcess); - if (cert != null) - { - if (this.GitSsl != null && !this.GitSsl.ShouldVerify) - { - httpClientHandler.ServerCertificateCustomValidationCallback = - (httpRequestMessage, c, cetChain, policyErrors) => - { - return true; - }; - } - - httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual; - httpClientHandler.ClientCertificates.Add(cert); - } - } - - private static bool TryParseCredentialString(string credentialString, out string username, out string password) - { - if (credentialString != null) - { - byte[] data = Convert.FromBase64String(credentialString); - string rawCredString = Encoding.ASCII.GetString(data); - - string[] usernamePassword = rawCredString.Split(':'); - if (usernamePassword.Length == 2) - { - username = usernamePassword[0]; - password = usernamePassword[1]; - - return true; - } - } - - username = null; - password = null; - return false; - } - - private bool TryAnonymousQuery(ITracer tracer, Enlistment enlistment, out bool isAnonymous) - { - bool querySucceeded; - using (ITracer anonymousTracer = tracer.StartActivity("AttemptAnonymousAuth", EventLevel.Informational)) - { - HttpStatusCode? httpStatus; - - using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(anonymousTracer, enlistment, new RetryConfig())) - { - ServerScalarConfig scalarConfig; - const bool LogErrors = false; - if (configRequestor.TryQueryScalarConfig(LogErrors, out scalarConfig, out httpStatus, out _)) - { - querySucceeded = true; - isAnonymous = true; - } - else if (httpStatus == HttpStatusCode.Unauthorized) - { - querySucceeded = true; - isAnonymous = false; - } - else - { - querySucceeded = false; - isAnonymous = false; - } - } - - anonymousTracer.Stop(new EventMetadata - { - { "HttpStatus", httpStatus.HasValue ? ((int)httpStatus).ToString() : "None" }, - { "QuerySucceeded", querySucceeded }, - { "IsAnonymous", isAnonymous }, - }); - } - - return querySucceeded; - } - - private DateTime GetNextAuthAttemptTime() - { - if (this.numberOfAttempts <= 1) - { - return DateTime.MinValue; - } - - double backoffSeconds = RetryBackoff.CalculateBackoffSeconds(this.numberOfAttempts, MaxBackoffSeconds); - return this.lastAuthAttempt + TimeSpan.FromSeconds(backoffSeconds); - } - - private void UpdateBackoff() - { - this.lastAuthAttempt = DateTime.Now; - this.numberOfAttempts++; - } - - private bool TryCallGitCredential(ITracer tracer, out string errorMessage) - { - string gitUsername; - string gitPassword; - if (!this.credentialStore.TryGetCredential(tracer, this.repoUrl, out gitUsername, out gitPassword, out errorMessage)) - { - this.UpdateBackoff(); - return false; - } - - if (!string.IsNullOrEmpty(gitUsername) && !string.IsNullOrEmpty(gitPassword)) - { - this.cachedCredentialString = Convert.ToBase64String(Encoding.ASCII.GetBytes(gitUsername + ":" + gitPassword)); - this.isCachedCredentialStringApproved = false; - } - else - { - errorMessage = "Got back empty credentials from git"; - return false; - } - - return true; - } - } -} +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Scalar.Common.Git +{ + public class GitAuthentication + { + private const double MaxBackoffSeconds = 30; + + private readonly object gitAuthLock = new object(); + private readonly ICredentialStore credentialStore; + private readonly string repoUrl; + + private int numberOfAttempts = 0; + private DateTime lastAuthAttempt = DateTime.MinValue; + + private string cachedCredentialString; + private bool isCachedCredentialStringApproved = false; + + private bool isInitialized; + + public GitAuthentication(GitProcess git, string repoUrl) + { + this.credentialStore = git; + this.repoUrl = repoUrl; + + if (git.TryGetConfigUrlMatch("http", this.repoUrl, out Dictionary configSettings)) + { + this.GitSsl = new GitSsl(configSettings); + } + } + + public bool IsBackingOff + { + get + { + return this.GetNextAuthAttemptTime() > DateTime.Now; + } + } + + public bool IsAnonymous { get; private set; } = true; + + private GitSsl GitSsl { get; } + + public void ApproveCredentials(ITracer tracer, string credentialString) + { + lock (this.gitAuthLock) + { + // Don't reset the backoff if this is for a different credential than we have cached + if (credentialString == this.cachedCredentialString) + { + this.numberOfAttempts = 0; + this.lastAuthAttempt = DateTime.MinValue; + + // Tell Git to store the valid credential if we haven't already + // done so for this cached credential. + if (!this.isCachedCredentialStringApproved) + { + string username; + string password; + if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) + { + if (!this.credentialStore.TryStoreCredential(tracer, this.repoUrl, username, password, out string error)) + { + // Storing credentials is best effort attempt - log failure, but do not fail + tracer.RelatedWarning("Failed to store credential string: {0}", error); + } + + this.isCachedCredentialStringApproved = true; + } + else + { + EventMetadata metadata = new EventMetadata(new Dictionary + { + ["RepoUrl"] = this.repoUrl, + }); + tracer.RelatedError(metadata, "Failed to parse credential string for approval"); + } + } + } + } + } + + public void RejectCredentials(ITracer tracer, string credentialString) + { + lock (this.gitAuthLock) + { + // Don't stomp a different credential + if (credentialString == this.cachedCredentialString && this.cachedCredentialString != null) + { + // If we can we should pass the actual username/password values we used (and found to be invalid) + // to `git-credential reject` so the credential helpers can attempt to check if they're erasing + // the expected credentials, if they so choose to. + string username; + string password; + if (TryParseCredentialString(this.cachedCredentialString, out username, out password)) + { + if (!this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username, password, out string error)) + { + // Deleting credentials is best effort attempt - log failure, but do not fail + tracer.RelatedWarning("Failed to delete credential string: {0}", error); + } + } + else + { + // We failed to parse the credential string so instead (as a recovery) we try to erase without + // specifying the particular username/password. + EventMetadata metadata = new EventMetadata(new Dictionary + { + ["RepoUrl"] = this.repoUrl, + }); + tracer.RelatedWarning(metadata, "Failed to parse credential string for rejection. Rejecting any credential for this repo URL."); + this.credentialStore.TryDeleteCredential(tracer, this.repoUrl, username: null, password: null, error: out string error); + } + + this.cachedCredentialString = null; + this.isCachedCredentialStringApproved = false; + this.UpdateBackoff(); + } + } + } + + public bool TryGetCredentials(ITracer tracer, out string credentialString, out string errorMessage) + { + if (!this.isInitialized) + { + throw new InvalidOperationException("This auth instance must be initialized before it can be used"); + } + + credentialString = this.cachedCredentialString; + if (credentialString == null) + { + lock (this.gitAuthLock) + { + if (this.cachedCredentialString == null) + { + if (this.IsBackingOff) + { + errorMessage = "Auth failed. No retries will be made until: " + this.GetNextAuthAttemptTime(); + return false; + } + + if (!this.TryCallGitCredential(tracer, out errorMessage)) + { + return false; + } + } + + credentialString = this.cachedCredentialString; + } + } + + errorMessage = null; + return true; + } + + public bool TryInitialize(ITracer tracer, Enlistment enlistment, out string errorMessage) + { + if (this.isInitialized) + { + throw new InvalidOperationException("Already initialized"); + } + + errorMessage = null; + + bool isAnonymous; + if (!this.TryAnonymousQuery(tracer, enlistment, out isAnonymous)) + { + errorMessage = $"Unable to determine if authentication is required"; + return false; + } + + if (!isAnonymous && + !this.TryCallGitCredential(tracer, out errorMessage)) + { + return false; + } + + this.IsAnonymous = isAnonymous; + this.isInitialized = true; + return true; + } + + public bool TryInitializeAndRequireAuth(ITracer tracer, out string errorMessage) + { + if (this.isInitialized) + { + throw new InvalidOperationException("Already initialized"); + } + + if (this.TryCallGitCredential(tracer, out errorMessage)) + { + this.isInitialized = true; + return true; + } + + return false; + } + + public void ConfigureHttpClientHandlerSslIfNeeded(ITracer tracer, HttpClientHandler httpClientHandler, GitProcess gitProcess) + { + X509Certificate2 cert = this.GitSsl?.GetCertificate(tracer, gitProcess); + if (cert != null) + { + if (this.GitSsl != null && !this.GitSsl.ShouldVerify) + { + httpClientHandler.ServerCertificateCustomValidationCallback = + (httpRequestMessage, c, cetChain, policyErrors) => + { + return true; + }; + } + + httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual; + httpClientHandler.ClientCertificates.Add(cert); + } + } + + private static bool TryParseCredentialString(string credentialString, out string username, out string password) + { + if (credentialString != null) + { + byte[] data = Convert.FromBase64String(credentialString); + string rawCredString = Encoding.ASCII.GetString(data); + + string[] usernamePassword = rawCredString.Split(':'); + if (usernamePassword.Length == 2) + { + username = usernamePassword[0]; + password = usernamePassword[1]; + + return true; + } + } + + username = null; + password = null; + return false; + } + + private bool TryAnonymousQuery(ITracer tracer, Enlistment enlistment, out bool isAnonymous) + { + bool querySucceeded; + using (ITracer anonymousTracer = tracer.StartActivity("AttemptAnonymousAuth", EventLevel.Informational)) + { + HttpStatusCode? httpStatus; + + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(anonymousTracer, enlistment, new RetryConfig())) + { + ServerScalarConfig scalarConfig; + const bool LogErrors = false; + if (configRequestor.TryQueryScalarConfig(LogErrors, out scalarConfig, out httpStatus, out _)) + { + querySucceeded = true; + isAnonymous = true; + } + else if (httpStatus == HttpStatusCode.Unauthorized) + { + querySucceeded = true; + isAnonymous = false; + } + else + { + querySucceeded = false; + isAnonymous = false; + } + } + + anonymousTracer.Stop(new EventMetadata + { + { "HttpStatus", httpStatus.HasValue ? ((int)httpStatus).ToString() : "None" }, + { "QuerySucceeded", querySucceeded }, + { "IsAnonymous", isAnonymous }, + }); + } + + return querySucceeded; + } + + private DateTime GetNextAuthAttemptTime() + { + if (this.numberOfAttempts <= 1) + { + return DateTime.MinValue; + } + + double backoffSeconds = RetryBackoff.CalculateBackoffSeconds(this.numberOfAttempts, MaxBackoffSeconds); + return this.lastAuthAttempt + TimeSpan.FromSeconds(backoffSeconds); + } + + private void UpdateBackoff() + { + this.lastAuthAttempt = DateTime.Now; + this.numberOfAttempts++; + } + + private bool TryCallGitCredential(ITracer tracer, out string errorMessage) + { + string gitUsername; + string gitPassword; + if (!this.credentialStore.TryGetCredential(tracer, this.repoUrl, out gitUsername, out gitPassword, out errorMessage)) + { + this.UpdateBackoff(); + return false; + } + + if (!string.IsNullOrEmpty(gitUsername) && !string.IsNullOrEmpty(gitPassword)) + { + this.cachedCredentialString = Convert.ToBase64String(Encoding.ASCII.GetBytes(gitUsername + ":" + gitPassword)); + this.isCachedCredentialStringApproved = false; + } + else + { + errorMessage = "Got back empty credentials from git"; + return false; + } + + return true; + } + } +} diff --git a/Scalar.Common/Git/GitConfigHelper.cs b/Scalar.Common/Git/GitConfigHelper.cs index b32b0df78e..65f8aa3111 100644 --- a/Scalar.Common/Git/GitConfigHelper.cs +++ b/Scalar.Common/Git/GitConfigHelper.cs @@ -1,139 +1,139 @@ -using System; -using System.Collections.Generic; - -namespace Scalar.Common.Git -{ - /// - /// Helper methods for git config-style file reading and parsing. - /// - public static class GitConfigHelper - { - /// - /// Sanitizes lines read from Git config files: - /// - Removes leading and trailing whitespace - /// - Removes comments - /// - /// Input line from config file - /// Sanitized config file line - /// true if sanitizedLine has content, false if there is no content left after sanitizing - public static bool TrySanitizeConfigFileLine(string fileLine, out string sanitizedLine) - { - sanitizedLine = fileLine; - int commentIndex = sanitizedLine.IndexOf(ScalarConstants.GitCommentSign); - if (commentIndex >= 0) - { - sanitizedLine = sanitizedLine.Substring(0, commentIndex); - } - - sanitizedLine = sanitizedLine.Trim(); - - return !string.IsNullOrWhiteSpace(sanitizedLine); - } - - /// - /// Get the settings for a section in a given config file. - /// - /// The contents of a config file, one line per entry. - /// The name of the section to grab the settings from. - /// A dictionary of settings, keyed off the setting name. - public static Dictionary GetSettings(string[] configLines, string sectionName) - { - List linesToParse = new List(); - - int currentLineIndex = 0; - string sectionTag = "[" + sectionName + "]"; - - // There can be multiple occurrences of the same section in a config file. - while (currentLineIndex < configLines.Length) - { - while (currentLineIndex < configLines.Length && !string.Equals(configLines[currentLineIndex].Trim(), sectionTag, StringComparison.OrdinalIgnoreCase)) - { - currentLineIndex++; - } - - if (currentLineIndex < configLines.Length) - { - // skip [sectionName] line - currentLineIndex++; - - while (currentLineIndex < configLines.Length && !configLines[currentLineIndex].StartsWith("[")) - { - string currentLineValue = configLines[currentLineIndex].Trim(); - if (!string.IsNullOrEmpty(currentLineValue)) - { - linesToParse.Add(currentLineValue); - } - - currentLineIndex++; - } - } - } - - return ParseKeyValues(linesToParse); - } - - /// - /// Returns a list of settings based on a collection of lines of text in the form: - /// settingName = settingValue - /// or - /// section.settingName=settingValue - /// - /// The lines of text with the settings to parse. - /// The delimiter char, separating key from value - /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. - public static Dictionary ParseKeyValues(IEnumerable input, char delimiter = '=') - { - Dictionary configSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string line in input) - { - string[] fields = line.Split(new[] { delimiter }, 2, StringSplitOptions.None); - - if (fields.Length > 0) - { - string key = fields[0].Trim(); - string value = string.Empty; - - if (fields.Length > 1) - { - value = fields[1].Trim(); - } - - if (!string.IsNullOrEmpty(key)) - { - if (!configSettings.ContainsKey(key) && fields.Length == 2) - { - GitConfigSetting setting = new GitConfigSetting(key, value); - configSettings.Add(key, setting); - } - else if (fields.Length == 2) - { - configSettings[key].Add(value); - } - } - } - } - - return configSettings; - } - - /// - /// Returns a list of settings based on input of the form: - /// settingName1 = settingValue1 - /// settingName2 = settingValue2 - /// settingName3 = settingValue3 - /// settingNameN = settingValueN - /// or - /// section.settingName1=settingValue1 - /// section.settingName2=settingValue2 - /// section.settingName3=settingValue3 - /// section.settingNameN=settingValueN - /// - /// The settings as text. - /// The delimiter char, separating key from value - /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. - public static Dictionary ParseKeyValues(string input, char delimiter = '=') - { - return ParseKeyValues(input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), delimiter); - } - } -} +using System; +using System.Collections.Generic; + +namespace Scalar.Common.Git +{ + /// + /// Helper methods for git config-style file reading and parsing. + /// + public static class GitConfigHelper + { + /// + /// Sanitizes lines read from Git config files: + /// - Removes leading and trailing whitespace + /// - Removes comments + /// + /// Input line from config file + /// Sanitized config file line + /// true if sanitizedLine has content, false if there is no content left after sanitizing + public static bool TrySanitizeConfigFileLine(string fileLine, out string sanitizedLine) + { + sanitizedLine = fileLine; + int commentIndex = sanitizedLine.IndexOf(ScalarConstants.GitCommentSign); + if (commentIndex >= 0) + { + sanitizedLine = sanitizedLine.Substring(0, commentIndex); + } + + sanitizedLine = sanitizedLine.Trim(); + + return !string.IsNullOrWhiteSpace(sanitizedLine); + } + + /// + /// Get the settings for a section in a given config file. + /// + /// The contents of a config file, one line per entry. + /// The name of the section to grab the settings from. + /// A dictionary of settings, keyed off the setting name. + public static Dictionary GetSettings(string[] configLines, string sectionName) + { + List linesToParse = new List(); + + int currentLineIndex = 0; + string sectionTag = "[" + sectionName + "]"; + + // There can be multiple occurrences of the same section in a config file. + while (currentLineIndex < configLines.Length) + { + while (currentLineIndex < configLines.Length && !string.Equals(configLines[currentLineIndex].Trim(), sectionTag, StringComparison.OrdinalIgnoreCase)) + { + currentLineIndex++; + } + + if (currentLineIndex < configLines.Length) + { + // skip [sectionName] line + currentLineIndex++; + + while (currentLineIndex < configLines.Length && !configLines[currentLineIndex].StartsWith("[")) + { + string currentLineValue = configLines[currentLineIndex].Trim(); + if (!string.IsNullOrEmpty(currentLineValue)) + { + linesToParse.Add(currentLineValue); + } + + currentLineIndex++; + } + } + } + + return ParseKeyValues(linesToParse); + } + + /// + /// Returns a list of settings based on a collection of lines of text in the form: + /// settingName = settingValue + /// or + /// section.settingName=settingValue + /// + /// The lines of text with the settings to parse. + /// The delimiter char, separating key from value + /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. + public static Dictionary ParseKeyValues(IEnumerable input, char delimiter = '=') + { + Dictionary configSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string line in input) + { + string[] fields = line.Split(new[] { delimiter }, 2, StringSplitOptions.None); + + if (fields.Length > 0) + { + string key = fields[0].Trim(); + string value = string.Empty; + + if (fields.Length > 1) + { + value = fields[1].Trim(); + } + + if (!string.IsNullOrEmpty(key)) + { + if (!configSettings.ContainsKey(key) && fields.Length == 2) + { + GitConfigSetting setting = new GitConfigSetting(key, value); + configSettings.Add(key, setting); + } + else if (fields.Length == 2) + { + configSettings[key].Add(value); + } + } + } + } + + return configSettings; + } + + /// + /// Returns a list of settings based on input of the form: + /// settingName1 = settingValue1 + /// settingName2 = settingValue2 + /// settingName3 = settingValue3 + /// settingNameN = settingValueN + /// or + /// section.settingName1=settingValue1 + /// section.settingName2=settingValue2 + /// section.settingName3=settingValue3 + /// section.settingNameN=settingValueN + /// + /// The settings as text. + /// The delimiter char, separating key from value + /// A dictionary of settings, keyed off the setting name representing the settings parsed from input. + public static Dictionary ParseKeyValues(string input, char delimiter = '=') + { + return ParseKeyValues(input.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), delimiter); + } + } +} diff --git a/Scalar.Common/Git/GitConfigSetting.cs b/Scalar.Common/Git/GitConfigSetting.cs index 141235a8e0..b9094a7ec3 100644 --- a/Scalar.Common/Git/GitConfigSetting.cs +++ b/Scalar.Common/Git/GitConfigSetting.cs @@ -1,34 +1,34 @@ -using System.Collections.Generic; - -namespace Scalar.Common.Git -{ - public class GitConfigSetting - { - public const string CoreVirtualizeObjectsName = "core.virtualizeobjects"; - public const string CoreVirtualFileSystemName = "core.virtualfilesystem"; - public const string CredentialUseHttpPath = "credential.\"https://dev.azure.com\".useHttpPath"; - - public const string HttpSslCert = "http.sslcert"; - public const string HttpSslVerify = "http.sslverify"; - public const string HttpSslCertPasswordProtected = "http.sslcertpasswordprotected"; - - public GitConfigSetting(string name, params string[] values) - { - this.Name = name; - this.Values = new HashSet(values); - } - - public string Name { get; } - public HashSet Values { get; } - - public bool HasValue(string value) - { - return this.Values.Contains(value); - } - - public void Add(string value) - { - this.Values.Add(value); - } - } -} +using System.Collections.Generic; + +namespace Scalar.Common.Git +{ + public class GitConfigSetting + { + public const string CoreVirtualizeObjectsName = "core.virtualizeobjects"; + public const string CoreVirtualFileSystemName = "core.virtualfilesystem"; + public const string CredentialUseHttpPath = "credential.\"https://dev.azure.com\".useHttpPath"; + + public const string HttpSslCert = "http.sslcert"; + public const string HttpSslVerify = "http.sslverify"; + public const string HttpSslCertPasswordProtected = "http.sslcertpasswordprotected"; + + public GitConfigSetting(string name, params string[] values) + { + this.Name = name; + this.Values = new HashSet(values); + } + + public string Name { get; } + public HashSet Values { get; } + + public bool HasValue(string value) + { + return this.Values.Contains(value); + } + + public void Add(string value) + { + this.Values.Add(value); + } + } +} diff --git a/Scalar.Common/Git/GitIndexGenerator.cs b/Scalar.Common/Git/GitIndexGenerator.cs index d5de0aca0b..7fcf4d8094 100644 --- a/Scalar.Common/Git/GitIndexGenerator.cs +++ b/Scalar.Common/Git/GitIndexGenerator.cs @@ -1,266 +1,266 @@ -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Threading; - -namespace Scalar.Common.Git -{ - public class GitIndexGenerator - { - private const long EntryCountOffset = 8; - - private const ushort ExtendedBit = 0x4000; - private const ushort SkipWorktreeBit = 0x4000; - - private static readonly byte[] PaddingBytes = new byte[8]; - - private static readonly byte[] IndexHeader = new byte[] - { - (byte)'D', (byte)'I', (byte)'R', (byte)'C', // Magic Signature - }; - - // We can't accurated fill times and length in realtime, so we block write the zeroes and probably save time. - private static readonly byte[] EntryHeader = new byte[] - { - 0, 0, 0, 0, - 0, 0, 0, 0, // ctime - 0, 0, 0, 0, - 0, 0, 0, 0, // mtime - 0, 0, 0, 0, // stat(2) dev - 0, 0, 0, 0, // stat(2) ino - 0, 0, 0x81, 0xA4, // filemode (0x81A4 in little endian) - 0, 0, 0, 0, // stat(2) uid - 0, 0, 0, 0, // stat(2) gid - 0, 0, 0, 0 // file length - }; - - private readonly string indexLockPath; - - private Enlistment enlistment; - private ITracer tracer; - private bool shouldHashIndex; - - private uint entryCount = 0; - - private BlockingCollection entryQueue = new BlockingCollection(); - - public GitIndexGenerator(ITracer tracer, Enlistment enlistment, bool shouldHashIndex) - { - this.tracer = tracer; - this.enlistment = enlistment; - this.shouldHashIndex = shouldHashIndex; - - this.indexLockPath = Path.Combine(enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName + ScalarConstants.DotGit.LockExtension); - } - - public bool HasFailures { get; private set; } - - public void CreateFromHeadTree(uint indexVersion) - { - using (ITracer updateIndexActivity = this.tracer.StartActivity("CreateFromHeadTree", EventLevel.Informational)) - { - Thread entryWritingThread = new Thread(() => this.WriteAllEntries(indexVersion)); - entryWritingThread.Start(); - - GitProcess git = new GitProcess(this.enlistment); - GitProcess.Result result = git.LsTree( - ScalarConstants.DotGit.HeadName, - this.EnqueueEntriesFromLsTree, - recursive: true, - showAllTrees: false); - - if (result.ExitCodeIsFailure) - { - this.tracer.RelatedError("LsTree failed during index generation: {0}", result.Errors); - this.HasFailures = true; - } - - this.entryQueue.CompleteAdding(); - entryWritingThread.Join(); - } - } - - private void EnqueueEntriesFromLsTree(string line) - { - LsTreeEntry entry = LsTreeEntry.ParseFromLsTreeLine(line); - if (entry != null) - { - this.entryQueue.Add(entry); - } - } - - private void WriteAllEntries(uint version) - { - try - { - using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Create, FileAccess.Write, FileShare.None)) - using (BinaryWriter writer = new BinaryWriter(indexStream)) - { - writer.Write(IndexHeader); - writer.Write(EndianHelper.Swap(version)); - writer.Write((uint)0); // Number of entries placeholder - - uint lastStringLength = 0; - LsTreeEntry entry; - while (this.entryQueue.TryTake(out entry, Timeout.Infinite)) - { - this.WriteEntry(writer, version, entry.Sha, entry.Filename, ref lastStringLength); - } - - // Update entry count - writer.BaseStream.Position = EntryCountOffset; - writer.Write(EndianHelper.Swap(this.entryCount)); - writer.Flush(); - } - - this.AppendIndexSha(); - this.ReplaceExistingIndex(); - } - catch (Exception e) - { - this.tracer.RelatedError("Failed to generate index: {0}", e.ToString()); - this.HasFailures = true; - } - } - - private string GetDirectoryNameForGitPath(string filename) - { - int idx = filename.LastIndexOf('/'); - if (idx < 0) - { - return "/"; - } - - return filename.Substring(0, idx + 1); - } - - private void WriteEntry(BinaryWriter writer, uint version, string sha, string filename, ref uint lastStringLength) - { - long startPosition = writer.BaseStream.Position; - - this.entryCount++; - - writer.Write(EntryHeader, 0, EntryHeader.Length); - - writer.Write(SHA1Util.BytesFromHexString(sha)); - - byte[] filenameBytes = Encoding.UTF8.GetBytes(filename); - - ushort flags = (ushort)(filenameBytes.Length & 0xFFF); - writer.Write(EndianHelper.Swap(flags)); - - if (version >= 4) - { - this.WriteReplaceLength(writer, lastStringLength); - lastStringLength = (uint)filenameBytes.Length; - } - - writer.Write(filenameBytes); - - writer.Flush(); - long endPosition = writer.BaseStream.Position; - - // Version 4 requires a nul-terminated string. - int numPaddingBytes = 1; - if (version < 4) - { - // Version 2-3 has between 1 and 8 padding bytes including nul-terminator. - numPaddingBytes = 8 - ((int)(endPosition - startPosition) % 8); - if (numPaddingBytes == 0) - { - numPaddingBytes = 8; - } - } - - writer.Write(PaddingBytes, 0, numPaddingBytes); - - writer.Flush(); - } - - private void WriteReplaceLength(BinaryWriter writer, uint value) - { - List bytes = new List(); - do - { - byte nextByte = (byte)(value & 0x7F); - value = value >> 7; - bytes.Add(nextByte); - } - while (value != 0); - - bytes.Reverse(); - for (int i = 0; i < bytes.Count; ++i) - { - byte toWrite = bytes[i]; - if (i < bytes.Count - 1) - { - toWrite -= 1; - toWrite |= 0x80; - } - - writer.Write(toWrite); - } - } - - private void AppendIndexSha() - { - byte[] sha = this.GetIndexHash(); - - using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Write, FileShare.None)) - { - indexStream.Seek(0, SeekOrigin.End); - indexStream.Write(sha, 0, sha.Length); - } - } - - private byte[] GetIndexHash() - { - if (this.shouldHashIndex) - { - using (Stream fileStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Read, FileShare.Write)) - using (HashingStream hasher = new HashingStream(fileStream)) - { - hasher.CopyTo(Stream.Null); - return hasher.Hash; - } - } - - return new byte[20]; - } - - private void ReplaceExistingIndex() - { - string indexPath = Path.Combine(this.enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName); - File.Delete(indexPath); - File.Move(this.indexLockPath, indexPath); - } - - private class LsTreeEntry - { - public LsTreeEntry() - { - this.Filename = string.Empty; - } - - public string Filename { get; private set; } - public string Sha { get; private set; } - - public static LsTreeEntry ParseFromLsTreeLine(string line) - { - if (DiffTreeResult.IsLsTreeLineOfType(line, DiffTreeResult.BlobMarker)) - { - LsTreeEntry blobEntry = new LsTreeEntry(); - blobEntry.Sha = line.Substring(DiffTreeResult.TypeMarkerStartIndex + DiffTreeResult.BlobMarker.Length, ScalarConstants.ShaStringLength); - blobEntry.Filename = GitPathConverter.ConvertPathOctetsToUtf8(line.Substring(line.LastIndexOf("\t") + 1).Trim('"')); - - return blobEntry; - } - - return null; - } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; + +namespace Scalar.Common.Git +{ + public class GitIndexGenerator + { + private const long EntryCountOffset = 8; + + private const ushort ExtendedBit = 0x4000; + private const ushort SkipWorktreeBit = 0x4000; + + private static readonly byte[] PaddingBytes = new byte[8]; + + private static readonly byte[] IndexHeader = new byte[] + { + (byte)'D', (byte)'I', (byte)'R', (byte)'C', // Magic Signature + }; + + // We can't accurated fill times and length in realtime, so we block write the zeroes and probably save time. + private static readonly byte[] EntryHeader = new byte[] + { + 0, 0, 0, 0, + 0, 0, 0, 0, // ctime + 0, 0, 0, 0, + 0, 0, 0, 0, // mtime + 0, 0, 0, 0, // stat(2) dev + 0, 0, 0, 0, // stat(2) ino + 0, 0, 0x81, 0xA4, // filemode (0x81A4 in little endian) + 0, 0, 0, 0, // stat(2) uid + 0, 0, 0, 0, // stat(2) gid + 0, 0, 0, 0 // file length + }; + + private readonly string indexLockPath; + + private Enlistment enlistment; + private ITracer tracer; + private bool shouldHashIndex; + + private uint entryCount = 0; + + private BlockingCollection entryQueue = new BlockingCollection(); + + public GitIndexGenerator(ITracer tracer, Enlistment enlistment, bool shouldHashIndex) + { + this.tracer = tracer; + this.enlistment = enlistment; + this.shouldHashIndex = shouldHashIndex; + + this.indexLockPath = Path.Combine(enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName + ScalarConstants.DotGit.LockExtension); + } + + public bool HasFailures { get; private set; } + + public void CreateFromHeadTree(uint indexVersion) + { + using (ITracer updateIndexActivity = this.tracer.StartActivity("CreateFromHeadTree", EventLevel.Informational)) + { + Thread entryWritingThread = new Thread(() => this.WriteAllEntries(indexVersion)); + entryWritingThread.Start(); + + GitProcess git = new GitProcess(this.enlistment); + GitProcess.Result result = git.LsTree( + ScalarConstants.DotGit.HeadName, + this.EnqueueEntriesFromLsTree, + recursive: true, + showAllTrees: false); + + if (result.ExitCodeIsFailure) + { + this.tracer.RelatedError("LsTree failed during index generation: {0}", result.Errors); + this.HasFailures = true; + } + + this.entryQueue.CompleteAdding(); + entryWritingThread.Join(); + } + } + + private void EnqueueEntriesFromLsTree(string line) + { + LsTreeEntry entry = LsTreeEntry.ParseFromLsTreeLine(line); + if (entry != null) + { + this.entryQueue.Add(entry); + } + } + + private void WriteAllEntries(uint version) + { + try + { + using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Create, FileAccess.Write, FileShare.None)) + using (BinaryWriter writer = new BinaryWriter(indexStream)) + { + writer.Write(IndexHeader); + writer.Write(EndianHelper.Swap(version)); + writer.Write((uint)0); // Number of entries placeholder + + uint lastStringLength = 0; + LsTreeEntry entry; + while (this.entryQueue.TryTake(out entry, Timeout.Infinite)) + { + this.WriteEntry(writer, version, entry.Sha, entry.Filename, ref lastStringLength); + } + + // Update entry count + writer.BaseStream.Position = EntryCountOffset; + writer.Write(EndianHelper.Swap(this.entryCount)); + writer.Flush(); + } + + this.AppendIndexSha(); + this.ReplaceExistingIndex(); + } + catch (Exception e) + { + this.tracer.RelatedError("Failed to generate index: {0}", e.ToString()); + this.HasFailures = true; + } + } + + private string GetDirectoryNameForGitPath(string filename) + { + int idx = filename.LastIndexOf('/'); + if (idx < 0) + { + return "/"; + } + + return filename.Substring(0, idx + 1); + } + + private void WriteEntry(BinaryWriter writer, uint version, string sha, string filename, ref uint lastStringLength) + { + long startPosition = writer.BaseStream.Position; + + this.entryCount++; + + writer.Write(EntryHeader, 0, EntryHeader.Length); + + writer.Write(SHA1Util.BytesFromHexString(sha)); + + byte[] filenameBytes = Encoding.UTF8.GetBytes(filename); + + ushort flags = (ushort)(filenameBytes.Length & 0xFFF); + writer.Write(EndianHelper.Swap(flags)); + + if (version >= 4) + { + this.WriteReplaceLength(writer, lastStringLength); + lastStringLength = (uint)filenameBytes.Length; + } + + writer.Write(filenameBytes); + + writer.Flush(); + long endPosition = writer.BaseStream.Position; + + // Version 4 requires a nul-terminated string. + int numPaddingBytes = 1; + if (version < 4) + { + // Version 2-3 has between 1 and 8 padding bytes including nul-terminator. + numPaddingBytes = 8 - ((int)(endPosition - startPosition) % 8); + if (numPaddingBytes == 0) + { + numPaddingBytes = 8; + } + } + + writer.Write(PaddingBytes, 0, numPaddingBytes); + + writer.Flush(); + } + + private void WriteReplaceLength(BinaryWriter writer, uint value) + { + List bytes = new List(); + do + { + byte nextByte = (byte)(value & 0x7F); + value = value >> 7; + bytes.Add(nextByte); + } + while (value != 0); + + bytes.Reverse(); + for (int i = 0; i < bytes.Count; ++i) + { + byte toWrite = bytes[i]; + if (i < bytes.Count - 1) + { + toWrite -= 1; + toWrite |= 0x80; + } + + writer.Write(toWrite); + } + } + + private void AppendIndexSha() + { + byte[] sha = this.GetIndexHash(); + + using (Stream indexStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Write, FileShare.None)) + { + indexStream.Seek(0, SeekOrigin.End); + indexStream.Write(sha, 0, sha.Length); + } + } + + private byte[] GetIndexHash() + { + if (this.shouldHashIndex) + { + using (Stream fileStream = new FileStream(this.indexLockPath, FileMode.Open, FileAccess.Read, FileShare.Write)) + using (HashingStream hasher = new HashingStream(fileStream)) + { + hasher.CopyTo(Stream.Null); + return hasher.Hash; + } + } + + return new byte[20]; + } + + private void ReplaceExistingIndex() + { + string indexPath = Path.Combine(this.enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName); + File.Delete(indexPath); + File.Move(this.indexLockPath, indexPath); + } + + private class LsTreeEntry + { + public LsTreeEntry() + { + this.Filename = string.Empty; + } + + public string Filename { get; private set; } + public string Sha { get; private set; } + + public static LsTreeEntry ParseFromLsTreeLine(string line) + { + if (DiffTreeResult.IsLsTreeLineOfType(line, DiffTreeResult.BlobMarker)) + { + LsTreeEntry blobEntry = new LsTreeEntry(); + blobEntry.Sha = line.Substring(DiffTreeResult.TypeMarkerStartIndex + DiffTreeResult.BlobMarker.Length, ScalarConstants.ShaStringLength); + blobEntry.Filename = GitPathConverter.ConvertPathOctetsToUtf8(line.Substring(line.LastIndexOf("\t") + 1).Trim('"')); + + return blobEntry; + } + + return null; + } + } + } +} diff --git a/Scalar.Common/Git/GitObjectContentType.cs b/Scalar.Common/Git/GitObjectContentType.cs index af4931200e..d0b1ad57a5 100644 --- a/Scalar.Common/Git/GitObjectContentType.cs +++ b/Scalar.Common/Git/GitObjectContentType.cs @@ -1,10 +1,10 @@ -namespace Scalar.Common.Git -{ - public enum GitObjectContentType - { - None, - LooseObject, - BatchedLooseObjects, - PackFile - } -} +namespace Scalar.Common.Git +{ + public enum GitObjectContentType + { + None, + LooseObject, + BatchedLooseObjects, + PackFile + } +} diff --git a/Scalar.Common/Git/GitObjects.cs b/Scalar.Common/Git/GitObjects.cs index 4b4380cd81..455faa192c 100644 --- a/Scalar.Common/Git/GitObjects.cs +++ b/Scalar.Common/Git/GitObjects.cs @@ -1,987 +1,987 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Http; -using Scalar.Common.NetworkStreams; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.Common.Git -{ - public abstract class GitObjects - { - protected readonly ITracer Tracer; - protected readonly GitObjectsHttpRequestor GitObjectRequestor; - protected readonly Enlistment Enlistment; - - private const string EtwArea = nameof(GitObjects); - private const string TempPackFolder = "tempPacks"; - private const string TempIdxExtension = ".tempidx"; - - private readonly PhysicalFileSystem fileSystem; - - public GitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) - { - this.Tracer = tracer; - this.Enlistment = enlistment; - this.GitObjectRequestor = objectRequestor; - this.fileSystem = fileSystem ?? new PhysicalFileSystem(); - } - - public enum DownloadAndSaveObjectResult - { - Success, - ObjectNotOnServer, - Error - } - - public static bool IsLooseObjectsDirectory(string value) - { - return value.Length == 2 && value.All(c => Uri.IsHexDigit(c)); - } - - public virtual bool TryDownloadCommit(string commitSha) - { - const bool PreferLooseObjects = false; - IEnumerable objectIds = new[] { commitSha }; - - GitProcess gitProcess = new GitProcess(this.Enlistment); - RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadObjects( - objectIds, - onSuccess: (tryCount, response) => this.TrySavePackOrLooseObject(objectIds, PreferLooseObjects, response, gitProcess), - onFailure: (eArgs) => - { - EventMetadata metadata = CreateEventMetadata(eArgs.Error); - metadata.Add("Operation", "DownloadAndSaveObjects"); - metadata.Add("WillRetry", eArgs.WillRetry); - - if (eArgs.WillRetry) - { - this.Tracer.RelatedWarning(metadata, eArgs.Error.ToString(), Keywords.Network | Keywords.Telemetry); - } - else - { - this.Tracer.RelatedError(metadata, eArgs.Error.ToString(), Keywords.Network); - } - }, - preferBatchedLooseObjects: PreferLooseObjects); - - return output.Succeeded && output.Result.Success; - } - - public virtual void DeleteStaleTempPrefetchPackAndIdxs() - { - string[] staleTempPacks = this.ReadPackFileNames(Path.Combine(this.Enlistment.GitPackRoot, GitObjects.TempPackFolder), ScalarConstants.PrefetchPackPrefix); - foreach (string stalePackPath in staleTempPacks) - { - string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx"); - string staleTempIdxPath = Path.ChangeExtension(stalePackPath, TempIdxExtension); - - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("stalePackPath", stalePackPath); - metadata.Add("staleIdxPath", staleIdxPath); - metadata.Add("staleTempIdxPath", staleTempIdxPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting stale temp pack and/or idx file"); - - this.fileSystem.TryDeleteFile(staleTempIdxPath, metadataKey: nameof(staleTempIdxPath), metadata: metadata); - this.fileSystem.TryDeleteFile(staleIdxPath, metadataKey: nameof(staleIdxPath), metadata: metadata); - this.fileSystem.TryDeleteFile(stalePackPath, metadataKey: nameof(stalePackPath), metadata: metadata); - - this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteStaleTempPrefetchPackAndIdxs), metadata); - } - } - - public virtual void DeleteTemporaryFiles() - { - string[] temporaryFiles = this.fileSystem.GetFiles(this.Enlistment.GitPackRoot, "tmp_*"); - foreach (string temporaryFilePath in temporaryFiles) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add(nameof(temporaryFilePath), temporaryFilePath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting temporary file"); - - this.fileSystem.TryDeleteFile(temporaryFilePath, metadataKey: nameof(temporaryFilePath), metadata: metadata); - - this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteTemporaryFiles), metadata); - } - } - - public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, out List packIndexes) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("latestTimestamp", latestTimestamp); - - using (ITracer activity = this.Tracer.StartActivity("TryDownloadPrefetchPacks", EventLevel.Informational, Keywords.Telemetry, metadata)) - { - long bytesDownloaded = 0; - - long requestId = HttpRequestor.GetNewRequestId(); - List innerPackIndexes = null; - RetryWrapper.InvocationResult result = this.GitObjectRequestor.TrySendProtocolRequest( - requestId: requestId, - onSuccess: (tryCount, response) => this.DeserializePrefetchPacks(response, ref latestTimestamp, ref bytesDownloaded, ref innerPackIndexes, gitProcess), - onFailure: RetryWrapper.StandardErrorHandler(activity, requestId, "TryDownloadPrefetchPacks"), - method: HttpMethod.Get, - endPointGenerator: () => new Uri( - string.Format( - "{0}?lastPackTimestamp={1}", - this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl, - latestTimestamp)), - requestBodyGenerator: () => null, - cancellationToken: CancellationToken.None, - acceptType: new MediaTypeWithQualityHeaderValue(ScalarConstants.MediaTypes.PrefetchPackFilesAndIndexesMediaType)); - - packIndexes = innerPackIndexes; - - if (!result.Succeeded) - { - if (result.Result != null && result.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) - { - EventMetadata warning = CreateEventMetadata(); - warning.Add(TracingConstants.MessageKey.WarningMessage, "The server does not support " + ScalarConstants.Endpoints.ScalarPrefetch); - warning.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); - activity.RelatedEvent(EventLevel.Warning, "CommandNotSupported", warning); - } - else - { - EventMetadata error = CreateEventMetadata(result.Error); - error.Add("latestTimestamp", latestTimestamp); - error.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); - activity.RelatedWarning(error, "DownloadPrefetchPacks failed.", Keywords.Telemetry); - } - } - - activity.Stop(new EventMetadata - { - { "Area", EtwArea }, - { "Success", result.Succeeded }, - { "Attempts", result.Attempts }, - { "BytesDownloaded", bytesDownloaded }, - }); - - return result.Succeeded; - } - } - - public virtual string WriteLooseObject(Stream responseStream, string sha, bool overwriteExistingObject, byte[] bufToCopyWith) - { - try - { - LooseObjectToWrite toWrite = this.GetLooseObjectDestination(sha); - - using (Stream fileStream = this.OpenTempLooseObjectStream(toWrite.TempFile)) - { - StreamUtil.CopyToWithBuffer(responseStream, fileStream, bufToCopyWith); - } - - this.FinalizeTempFile(sha, toWrite, overwriteExistingObject); - - return toWrite.ActualFile; - } - catch (IOException e) - { - throw new RetryableException("IOException while writing loose object. See inner exception for details.", e); - } - catch (UnauthorizedAccessException e) - { - throw new RetryableException("UnauthorizedAccessException while writing loose object. See inner exception for details.", e); - } - catch (Win32Exception e) - { - throw new RetryableException("Win32Exception while writing loose object. See inner exception for details.", e); - } - } - - public virtual string WriteTempPackFile(Stream stream) - { - string fileName = Path.GetRandomFileName(); - string fullPath = Path.Combine(this.Enlistment.GitPackRoot, fileName); - - Task flushTask; - long fileLength; - this.TryWriteTempFile( - tracer: null, - source: stream, - tempFilePath: fullPath, - fileLength: out fileLength, - flushTask: out flushTask, - throwOnError: true); - - flushTask?.Wait(); - - return fullPath; - } - - public virtual bool TryWriteTempFile( - ITracer tracer, - Stream source, - string tempFilePath, - out long fileLength, - out Task flushTask, - bool throwOnError = false) - { - fileLength = 0; - flushTask = null; - try - { - Stream fileStream = null; - - try - { - fileStream = this.fileSystem.OpenFileStream( - tempFilePath, - FileMode.OpenOrCreate, - FileAccess.Write, - FileShare.Read, - callFlushFileBuffers: false); // Any flushing to disk will be done asynchronously - - StreamUtil.CopyToWithBuffer(source, fileStream); - fileLength = fileStream.Length; - - if (this.Enlistment.FlushFileBuffersForPacks) - { - // Flush any data buffered in FileStream to the file system - fileStream.Flush(); - - // FlushFileBuffers using FlushAsync - // Do this last to ensure that the stream is not being accessed after it's been disposed - flushTask = fileStream.FlushAsync().ContinueWith((result) => fileStream.Dispose()); - } - } - finally - { - if (flushTask == null && fileStream != null) - { - fileStream.Dispose(); - } - } - - this.ValidateTempFile(tempFilePath, tempFilePath); - } - catch (Exception ex) - { - if (flushTask != null) - { - flushTask.Wait(); - flushTask = null; - } - - this.CleanupTempFile(this.Tracer, tempFilePath); - - if (tracer != null) - { - EventMetadata metadata = CreateEventMetadata(ex); - metadata.Add("tempFilePath", tempFilePath); - tracer.RelatedWarning(metadata, $"{nameof(this.TryWriteTempFile)}: Exception caught while writing temp file", Keywords.Telemetry); - } - - if (throwOnError) - { - throw; - } - else - { - return false; - } - } - - return true; - } - - public virtual GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null) - { - string packfilePath = GetRandomPackName(this.Enlistment.GitPackRoot); - - Exception moveFileException = null; - try - { - // We're indexing a pack file that was saved to a temp file name, and so it must be renamed - // to its final name before indexing ('git index-pack' requires that the pack file name end with .pack) - this.fileSystem.MoveFile(tempPackPath, packfilePath); - } - catch (IOException e) - { - moveFileException = e; - } - catch (UnauthorizedAccessException e) - { - moveFileException = e; - } - - if (moveFileException != null) - { - EventMetadata failureMetadata = CreateEventMetadata(moveFileException); - failureMetadata.Add("tempPackPath", tempPackPath); - failureMetadata.Add("packfilePath", packfilePath); - - this.fileSystem.TryDeleteFile(tempPackPath, metadataKey: nameof(tempPackPath), metadata: failureMetadata); - - this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexTempPackFile): Exception caught while trying to move temp pack file}"); - - return new GitProcess.Result( - string.Empty, - moveFileException != null ? moveFileException.Message : "Failed to move temp pack file to final path", - GitProcess.Result.GenericFailureCode); - } - - // TryBuildIndex will delete the pack file if indexing fails - GitProcess.Result result; - this.TryBuildIndex(this.Tracer, packfilePath, out result, gitProcess); - return result; - } - - public virtual GitProcess.Result IndexPackFile(string packfilePath, GitProcess gitProcess) - { - string tempIdxPath = Path.ChangeExtension(packfilePath, TempIdxExtension); - string idxPath = Path.ChangeExtension(packfilePath, ".idx"); - - Exception indexPackException = null; - try - { - if (gitProcess == null) - { - gitProcess = new GitProcess(this.Enlistment); - } - - GitProcess.Result result = gitProcess.IndexPack(packfilePath, tempIdxPath); - if (result.ExitCodeIsFailure) - { - Exception exception; - if (!this.fileSystem.TryDeleteFile(tempIdxPath, exception: out exception)) - { - EventMetadata metadata = CreateEventMetadata(exception); - metadata.Add("tempIdxPath", tempIdxPath); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to cleanup temp idx file after index pack failure"); - } - } - else - { - if (this.Enlistment.FlushFileBuffersForPacks) - { - Exception exception; - string error; - if (!this.TryFlushFileBuffers(tempIdxPath, out exception, out error)) - { - EventMetadata metadata = CreateEventMetadata(exception); - metadata.Add("packfilePath", packfilePath); - metadata.Add("tempIndexPath", tempIdxPath); - metadata.Add("error", error); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to flush temp idx file buffers"); - } - } - - this.fileSystem.MoveAndOverwriteFile(tempIdxPath, idxPath); - } - - return result; - } - catch (Win32Exception e) - { - indexPackException = e; - } - catch (IOException e) - { - indexPackException = e; - } - catch (UnauthorizedAccessException e) - { - indexPackException = e; - } - - EventMetadata failureMetadata = CreateEventMetadata(indexPackException); - failureMetadata.Add("packfilePath", packfilePath); - failureMetadata.Add("tempIdxPath", tempIdxPath); - failureMetadata.Add("idxPath", idxPath); - - this.fileSystem.TryDeleteFile(tempIdxPath, metadataKey: nameof(tempIdxPath), metadata: failureMetadata); - this.fileSystem.TryDeleteFile(idxPath, metadataKey: nameof(idxPath), metadata: failureMetadata); - - this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexPackFile): Exception caught while trying to index pack file}"); - - return new GitProcess.Result( - string.Empty, - indexPackException != null ? indexPackException.Message : "Failed to index pack file", - GitProcess.Result.GenericFailureCode); - } - - public virtual string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "") - { - if (this.fileSystem.DirectoryExists(packFolderPath)) - { - try - { - return this.fileSystem.GetFiles(packFolderPath, prefixFilter + "*.pack"); - } - catch (DirectoryNotFoundException e) - { - EventMetadata metadata = CreateEventMetadata(e); - metadata.Add("packFolderPath", packFolderPath); - metadata.Add("prefixFilter", prefixFilter); - metadata.Add(TracingConstants.MessageKey.InfoMessage, "${nameof(this.ReadPackFileNames)}: Caught DirectoryNotFoundException exception"); - this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ReadPackFileNames)}_DirectoryNotFound", metadata); - - return new string[0]; - } - } - - return new string[0]; - } - - public virtual bool IsUsingCacheServer() - { - return !this.GitObjectRequestor.CacheServer.IsNone(this.Enlistment.RepoUrl); - } - - private static string GetRandomPackName(string packRoot) - { - string packName = "pack-" + Guid.NewGuid().ToString("N") + ".pack"; - return Path.Combine(packRoot, packName); - } - - 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 bool TryMovePackAndIdxFromTempFolder(string packName, string packTempPath, string idxName, string idxTempPath, out Exception exception) - { - exception = null; - string finalPackPath = Path.Combine(this.Enlistment.GitPackRoot, packName); - string finalIdxPath = Path.Combine(this.Enlistment.GitPackRoot, idxName); - - try - { - this.fileSystem.MoveAndOverwriteFile(packTempPath, finalPackPath); - this.fileSystem.MoveAndOverwriteFile(idxTempPath, finalIdxPath); - } - catch (Win32Exception e) - { - exception = e; - - EventMetadata metadata = CreateEventMetadata(e); - metadata.Add("packName", packName); - metadata.Add("packTempPath", packTempPath); - metadata.Add("idxName", idxName); - metadata.Add("idxTempPath", idxTempPath); - - this.fileSystem.TryDeleteFile(idxTempPath, metadataKey: nameof(idxTempPath), metadata: metadata); - this.fileSystem.TryDeleteFile(finalIdxPath, metadataKey: nameof(finalIdxPath), metadata: metadata); - this.fileSystem.TryDeleteFile(packTempPath, metadataKey: nameof(packTempPath), metadata: metadata); - this.fileSystem.TryDeleteFile(finalPackPath, metadataKey: nameof(finalPackPath), metadata: metadata); - - this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryMovePackAndIdxFromTempFolder): Failed to move pack and idx from temp folder}"); - - return false; - } - - return true; - } - - private bool TryFlushFileBuffers(string path, out Exception exception, out string error) - { - error = null; - - FileAttributes originalAttributes; - if (!this.TryGetAttributes(path, out originalAttributes, out exception)) - { - error = "Failed to get original attributes, skipping flush"; - return false; - } - - bool readOnly = (originalAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; - - if (readOnly) - { - if (!this.TrySetAttributes(path, originalAttributes & ~FileAttributes.ReadOnly, out exception)) - { - error = "Failed to clear read-only attribute, skipping flush"; - return false; - } - } - - bool flushedBuffers = false; - try - { - ScalarPlatform.Instance.FileSystem.FlushFileBuffers(path); - flushedBuffers = true; - } - catch (Win32Exception e) - { - exception = e; - error = "Win32Exception while trying to flush file buffers"; - } - - if (readOnly) - { - Exception setAttributesException; - if (!this.TrySetAttributes(path, originalAttributes, out setAttributesException)) - { - EventMetadata metadata = CreateEventMetadata(setAttributesException); - metadata.Add("path", path); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryFlushFileBuffers)}: Failed to re-enable read-only bit"); - } - } - - return flushedBuffers; - } - - private bool TryGetAttributes(string path, out FileAttributes attributes, out Exception exception) - { - attributes = 0; - exception = null; - try - { - attributes = this.fileSystem.GetAttributes(path); - return true; - } - catch (IOException e) - { - exception = e; - } - catch (UnauthorizedAccessException e) - { - exception = e; - } - - return false; - } - - private bool TrySetAttributes(string path, FileAttributes attributes, out Exception exception) - { - exception = null; - - try - { - this.fileSystem.SetAttributes(path, attributes); - return true; - } - catch (IOException e) - { - exception = e; - } - catch (UnauthorizedAccessException e) - { - exception = e; - } - - return false; - } - - private Stream OpenTempLooseObjectStream(string path) - { - return this.fileSystem.OpenFileStream( - path, - FileMode.Create, - FileAccess.Write, - FileShare.None, - FileOptions.SequentialScan, - callFlushFileBuffers: false); - } - - private LooseObjectToWrite GetLooseObjectDestination(string sha) - { - string firstTwoDigits = sha.Substring(0, 2); - string remainingDigits = sha.Substring(2); - string twoLetterFolderName = Path.Combine(this.Enlistment.GitObjectsRoot, firstTwoDigits); - this.fileSystem.CreateDirectory(twoLetterFolderName); - - return new LooseObjectToWrite( - tempFile: Path.Combine(twoLetterFolderName, Path.GetRandomFileName()), - actualFile: Path.Combine(twoLetterFolderName, remainingDigits)); - } - - /// - /// Uses a to read the packs from the stream. - /// - private RetryWrapper.CallbackResult DeserializePrefetchPacks( - GitEndPointResponseData response, - ref long latestTimestamp, - ref long bytesDownloaded, - ref List packIndexes, - GitProcess gitProcess) - { - if (packIndexes == null) - { - packIndexes = new List(); - } - - using (ITracer activity = this.Tracer.StartActivity("DeserializePrefetchPacks", EventLevel.Informational)) - { - PrefetchPacksDeserializer deserializer = new PrefetchPacksDeserializer(response.Stream); - - string tempPackFolderPath = Path.Combine(this.Enlistment.GitPackRoot, TempPackFolder); - this.fileSystem.CreateDirectory(tempPackFolderPath); - - List tempPacks = new List(); - foreach (PrefetchPacksDeserializer.PackAndIndex pack in deserializer.EnumeratePacks()) - { - // The advertised size may not match the actual, on-disk size. - long indexLength = 0; - long packLength; - - // Write the temp and index to a temp folder to avoid putting corrupt files in the pack folder - // Once the files are validated and flushed they can be moved to the pack folder - string packName = string.Format("{0}-{1}-{2}.pack", ScalarConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); - string packTempPath = Path.Combine(tempPackFolderPath, packName); - string idxName = string.Format("{0}-{1}-{2}.idx", ScalarConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); - string idxTempPath = Path.Combine(tempPackFolderPath, idxName); - - EventMetadata data = CreateEventMetadata(); - data["timestamp"] = pack.Timestamp.ToString(); - data["uniqueId"] = pack.UniqueId; - activity.RelatedEvent(EventLevel.Informational, "Receiving Pack/Index", data); - - // Write the pack - // If it fails, TryWriteTempFile cleans up the file and we retry the prefetch - Task packFlushTask; - if (!this.TryWriteTempFile(activity, pack.PackStream, packTempPath, out packLength, out packFlushTask)) - { - bytesDownloaded += packLength; - return new RetryWrapper.CallbackResult(null, true); - } - - bytesDownloaded += packLength; - - // We will try to build an index if the server does not send one - if (pack.IndexStream == null) - { - GitProcess.Result result; - if (!this.TryBuildIndex(activity, packTempPath, out result, gitProcess)) - { - if (packFlushTask != null) - { - packFlushTask.Wait(); - } - - // Move whatever has been successfully downloaded so far - Exception moveException; - this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out moveException); - - return new RetryWrapper.CallbackResult(null, true); - } - - tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, idxFlushTask: null)); - } - else - { - Task indexFlushTask; - if (this.TryWriteTempFile(activity, pack.IndexStream, idxTempPath, out indexLength, out indexFlushTask)) - { - tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, indexFlushTask)); - } - else - { - bytesDownloaded += indexLength; - - // Try to build the index manually, then retry the prefetch - GitProcess.Result result; - if (this.TryBuildIndex(activity, packTempPath, out result, gitProcess)) - { - // If we were able to recreate the failed index - // we can start the prefetch at the next timestamp - tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, idxFlushTask: null)); - } - else - { - if (packFlushTask != null) - { - packFlushTask.Wait(); - } - } - - // Move whatever has been successfully downloaded so far - Exception moveException; - this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out moveException); - - // The download stream will not be in a good state if the index download fails. - // So we have to restart the prefetch - return new RetryWrapper.CallbackResult(null, true); - } - } - - bytesDownloaded += indexLength; - } - - Exception exception = null; - if (!this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out exception)) - { - return new RetryWrapper.CallbackResult(exception, true); - } - - foreach (TempPrefetchPackAndIdx tempPack in tempPacks) - { - packIndexes.Add(tempPack.IdxName); - } - - return new RetryWrapper.CallbackResult( - new GitObjectsHttpRequestor.GitObjectTaskResult(success: true)); - } - } - - private bool TryFlushAndMoveTempPacks(List tempPacks, ref long latestTimestamp, out Exception exception) - { - exception = null; - bool moveFailed = false; - foreach (TempPrefetchPackAndIdx tempPack in tempPacks) - { - if (tempPack.PackFlushTask != null) - { - tempPack.PackFlushTask.Wait(); - } - - if (tempPack.IdxFlushTask != null) - { - tempPack.IdxFlushTask.Wait(); - } - - // If we've hit a failure moving temp files, we should stop trying to move them (but we still need to wait for all outstanding - // flush tasks) - if (!moveFailed) - { - if (this.TryMovePackAndIdxFromTempFolder(tempPack.PackName, tempPack.PackFullPath, tempPack.IdxName, tempPack.IdxFullPath, out exception)) - { - latestTimestamp = tempPack.Timestamp; - } - else - { - moveFailed = true; - } - } - } - - return !moveFailed; - } - - /// - /// Attempts to build an index for the specified path. If building the index fails, the pack file is deleted - /// - private bool TryBuildIndex( - ITracer activity, - string packFullPath, - out GitProcess.Result result, - GitProcess gitProcess) - { - result = this.IndexPackFile(packFullPath, gitProcess); - - if (result.ExitCodeIsFailure) - { - EventMetadata errorMetadata = CreateEventMetadata(); - Exception exception; - if (!this.fileSystem.TryDeleteFile(packFullPath, exception: out exception)) - { - if (exception != null) - { - errorMetadata.Add("deleteException", exception.ToString()); - } - - errorMetadata.Add("deletedBadPack", "false"); - } - - errorMetadata.Add("Operation", nameof(this.TryBuildIndex)); - errorMetadata.Add("packFullPath", packFullPath); - activity.RelatedWarning(errorMetadata, result.Errors, Keywords.Telemetry); - } - - return result.ExitCodeIsSuccess; - } - - private void CleanupTempFile(ITracer activity, string fullPath) - { - Exception e; - if (!this.fileSystem.TryDeleteFile(fullPath, exception: out e)) - { - EventMetadata info = CreateEventMetadata(e); - info.Add("file", fullPath); - activity.RelatedWarning(info, "Failed to cleanup temp file"); - } - } - - private void FinalizeTempFile(string sha, LooseObjectToWrite toWrite, bool overwriteExistingObject) - { - try - { - // Checking for existence reduces warning outputs when a streamed download tries. - if (this.fileSystem.FileExists(toWrite.ActualFile)) - { - if (overwriteExistingObject) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("file", toWrite.ActualFile); - metadata.Add("tempFile", toWrite.TempFile); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.FinalizeTempFile)}: Overwriting existing loose object"); - this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.FinalizeTempFile)}_OverwriteExistingObject", metadata); - - this.ValidateTempFile(toWrite.TempFile, sha); - this.fileSystem.MoveAndOverwriteFile(toWrite.TempFile, toWrite.ActualFile); - } - } - else - { - this.ValidateTempFile(toWrite.TempFile, sha); - - try - { - this.fileSystem.MoveFile(toWrite.TempFile, toWrite.ActualFile); - } - catch (IOException ex) - { - // IOExceptions happen when someone else is writing to our object. - // That implies they are doing what we're doing, which should be a success - EventMetadata info = CreateEventMetadata(ex); - info.Add("file", toWrite.ActualFile); - this.Tracer.RelatedWarning(info, $"{nameof(this.FinalizeTempFile)}: Exception moving temp file"); - } - } - } - finally - { - this.CleanupTempFile(this.Tracer, toWrite.TempFile); - } - } - - private void ValidateTempFile(string tempFilePath, string finalFilePath) - { - using (Stream fs = this.fileSystem.OpenFileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false)) - { - if (fs.Length == 0) - { - throw new RetryableException($"Temp file '{tempFilePath}' for '{finalFilePath}' was written with 0 bytes"); - } - else - { - byte[] buffer = new byte[10]; - - // Temp files should always have at least one non-zero byte - int bytesRead = fs.Read(buffer, 0, buffer.Length); - if (buffer.All(b => b == 0)) - { - RetryableException ex = new RetryableException( - $"Temp file '{tempFilePath}' for '{finalFilePath}' was written with {bytesRead} null bytes"); - - EventMetadata eventInfo = CreateEventMetadata(ex); - eventInfo.Add("file", tempFilePath); - eventInfo.Add("finalFilePath", finalFilePath); - this.Tracer.RelatedWarning(eventInfo, $"{nameof(this.ValidateTempFile)}: Temp file invalid"); - - throw ex; - } - } - } - } - - private RetryWrapper.CallbackResult TrySavePackOrLooseObject( - IEnumerable objectShas, - bool unpackObjects, - GitEndPointResponseData responseData, - GitProcess gitProcess) - { - if (responseData.ContentType == GitObjectContentType.LooseObject) - { - List objectShaList = objectShas.Distinct().ToList(); - if (objectShaList.Count != 1) - { - return new RetryWrapper.CallbackResult(new InvalidOperationException("Received loose object when multiple objects were requested."), shouldRetry: false); - } - - // To reduce allocations, reuse the same buffer when writing objects in this batch - byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; - - this.WriteLooseObject(responseData.Stream, objectShaList[0], overwriteExistingObject: false, bufToCopyWith: bufToCopyWith); - } - else if (responseData.ContentType == GitObjectContentType.BatchedLooseObjects) - { - // To reduce allocations, reuse the same buffer when writing objects in this batch - byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; - - BatchedLooseObjectDeserializer deserializer = new BatchedLooseObjectDeserializer( - responseData.Stream, - (stream, sha) => this.WriteLooseObject(stream, sha, overwriteExistingObject: false, bufToCopyWith: bufToCopyWith)); - deserializer.ProcessObjects(); - } - else - { - GitProcess.Result result = this.TryAddPackFile(responseData.Stream, unpackObjects, gitProcess); - if (result.ExitCodeIsFailure) - { - return new RetryWrapper.CallbackResult(new InvalidOperationException("Could not add pack file: " + result.Errors), shouldRetry: false); - } - } - - return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); - } - - private GitProcess.Result TryAddPackFile(Stream contents, bool unpackObjects, GitProcess gitProcess) - { - GitProcess.Result result; - - this.fileSystem.CreateDirectory(this.Enlistment.GitPackRoot); - - if (unpackObjects) - { - result = new GitProcess(this.Enlistment).UnpackObjects(contents); - } - else - { - string tempPackPath = this.WriteTempPackFile(contents); - return this.IndexTempPackFile(tempPackPath, gitProcess); - } - - return result; - } - - private struct LooseObjectToWrite - { - public readonly string TempFile; - public readonly string ActualFile; - - public LooseObjectToWrite(string tempFile, string actualFile) - { - this.TempFile = tempFile; - this.ActualFile = actualFile; - } - } - - private class TempPrefetchPackAndIdx - { - public TempPrefetchPackAndIdx( - long timestamp, - string packName, - string packFullPath, - Task packFlushTask, - string idxName, - string idxFullPath, - Task idxFlushTask) - { - this.Timestamp = timestamp; - this.PackName = packName; - this.PackFullPath = packFullPath; - this.PackFlushTask = packFlushTask; - this.IdxName = idxName; - this.IdxFullPath = idxFullPath; - this.IdxFlushTask = idxFlushTask; - } - - public long Timestamp { get; } - public string PackName { get; } - public string PackFullPath { get; } - public Task PackFlushTask { get; } - public string IdxName { get; } - public string IdxFullPath { get; } - public Task IdxFlushTask { get; } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Http; +using Scalar.Common.NetworkStreams; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.Common.Git +{ + public abstract class GitObjects + { + protected readonly ITracer Tracer; + protected readonly GitObjectsHttpRequestor GitObjectRequestor; + protected readonly Enlistment Enlistment; + + private const string EtwArea = nameof(GitObjects); + private const string TempPackFolder = "tempPacks"; + private const string TempIdxExtension = ".tempidx"; + + private readonly PhysicalFileSystem fileSystem; + + public GitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) + { + this.Tracer = tracer; + this.Enlistment = enlistment; + this.GitObjectRequestor = objectRequestor; + this.fileSystem = fileSystem ?? new PhysicalFileSystem(); + } + + public enum DownloadAndSaveObjectResult + { + Success, + ObjectNotOnServer, + Error + } + + public static bool IsLooseObjectsDirectory(string value) + { + return value.Length == 2 && value.All(c => Uri.IsHexDigit(c)); + } + + public virtual bool TryDownloadCommit(string commitSha) + { + const bool PreferLooseObjects = false; + IEnumerable objectIds = new[] { commitSha }; + + GitProcess gitProcess = new GitProcess(this.Enlistment); + RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadObjects( + objectIds, + onSuccess: (tryCount, response) => this.TrySavePackOrLooseObject(objectIds, PreferLooseObjects, response, gitProcess), + onFailure: (eArgs) => + { + EventMetadata metadata = CreateEventMetadata(eArgs.Error); + metadata.Add("Operation", "DownloadAndSaveObjects"); + metadata.Add("WillRetry", eArgs.WillRetry); + + if (eArgs.WillRetry) + { + this.Tracer.RelatedWarning(metadata, eArgs.Error.ToString(), Keywords.Network | Keywords.Telemetry); + } + else + { + this.Tracer.RelatedError(metadata, eArgs.Error.ToString(), Keywords.Network); + } + }, + preferBatchedLooseObjects: PreferLooseObjects); + + return output.Succeeded && output.Result.Success; + } + + public virtual void DeleteStaleTempPrefetchPackAndIdxs() + { + string[] staleTempPacks = this.ReadPackFileNames(Path.Combine(this.Enlistment.GitPackRoot, GitObjects.TempPackFolder), ScalarConstants.PrefetchPackPrefix); + foreach (string stalePackPath in staleTempPacks) + { + string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx"); + string staleTempIdxPath = Path.ChangeExtension(stalePackPath, TempIdxExtension); + + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("stalePackPath", stalePackPath); + metadata.Add("staleIdxPath", staleIdxPath); + metadata.Add("staleTempIdxPath", staleTempIdxPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting stale temp pack and/or idx file"); + + this.fileSystem.TryDeleteFile(staleTempIdxPath, metadataKey: nameof(staleTempIdxPath), metadata: metadata); + this.fileSystem.TryDeleteFile(staleIdxPath, metadataKey: nameof(staleIdxPath), metadata: metadata); + this.fileSystem.TryDeleteFile(stalePackPath, metadataKey: nameof(stalePackPath), metadata: metadata); + + this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteStaleTempPrefetchPackAndIdxs), metadata); + } + } + + public virtual void DeleteTemporaryFiles() + { + string[] temporaryFiles = this.fileSystem.GetFiles(this.Enlistment.GitPackRoot, "tmp_*"); + foreach (string temporaryFilePath in temporaryFiles) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add(nameof(temporaryFilePath), temporaryFilePath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, "Deleting temporary file"); + + this.fileSystem.TryDeleteFile(temporaryFilePath, metadataKey: nameof(temporaryFilePath), metadata: metadata); + + this.Tracer.RelatedEvent(EventLevel.Informational, nameof(this.DeleteTemporaryFiles), metadata); + } + } + + public virtual bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, out List packIndexes) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("latestTimestamp", latestTimestamp); + + using (ITracer activity = this.Tracer.StartActivity("TryDownloadPrefetchPacks", EventLevel.Informational, Keywords.Telemetry, metadata)) + { + long bytesDownloaded = 0; + + long requestId = HttpRequestor.GetNewRequestId(); + List innerPackIndexes = null; + RetryWrapper.InvocationResult result = this.GitObjectRequestor.TrySendProtocolRequest( + requestId: requestId, + onSuccess: (tryCount, response) => this.DeserializePrefetchPacks(response, ref latestTimestamp, ref bytesDownloaded, ref innerPackIndexes, gitProcess), + onFailure: RetryWrapper.StandardErrorHandler(activity, requestId, "TryDownloadPrefetchPacks"), + method: HttpMethod.Get, + endPointGenerator: () => new Uri( + string.Format( + "{0}?lastPackTimestamp={1}", + this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl, + latestTimestamp)), + requestBodyGenerator: () => null, + cancellationToken: CancellationToken.None, + acceptType: new MediaTypeWithQualityHeaderValue(ScalarConstants.MediaTypes.PrefetchPackFilesAndIndexesMediaType)); + + packIndexes = innerPackIndexes; + + if (!result.Succeeded) + { + if (result.Result != null && result.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) + { + EventMetadata warning = CreateEventMetadata(); + warning.Add(TracingConstants.MessageKey.WarningMessage, "The server does not support " + ScalarConstants.Endpoints.ScalarPrefetch); + warning.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); + activity.RelatedEvent(EventLevel.Warning, "CommandNotSupported", warning); + } + else + { + EventMetadata error = CreateEventMetadata(result.Error); + error.Add("latestTimestamp", latestTimestamp); + error.Add(nameof(this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl), this.GitObjectRequestor.CacheServer.PrefetchEndpointUrl); + activity.RelatedWarning(error, "DownloadPrefetchPacks failed.", Keywords.Telemetry); + } + } + + activity.Stop(new EventMetadata + { + { "Area", EtwArea }, + { "Success", result.Succeeded }, + { "Attempts", result.Attempts }, + { "BytesDownloaded", bytesDownloaded }, + }); + + return result.Succeeded; + } + } + + public virtual string WriteLooseObject(Stream responseStream, string sha, bool overwriteExistingObject, byte[] bufToCopyWith) + { + try + { + LooseObjectToWrite toWrite = this.GetLooseObjectDestination(sha); + + using (Stream fileStream = this.OpenTempLooseObjectStream(toWrite.TempFile)) + { + StreamUtil.CopyToWithBuffer(responseStream, fileStream, bufToCopyWith); + } + + this.FinalizeTempFile(sha, toWrite, overwriteExistingObject); + + return toWrite.ActualFile; + } + catch (IOException e) + { + throw new RetryableException("IOException while writing loose object. See inner exception for details.", e); + } + catch (UnauthorizedAccessException e) + { + throw new RetryableException("UnauthorizedAccessException while writing loose object. See inner exception for details.", e); + } + catch (Win32Exception e) + { + throw new RetryableException("Win32Exception while writing loose object. See inner exception for details.", e); + } + } + + public virtual string WriteTempPackFile(Stream stream) + { + string fileName = Path.GetRandomFileName(); + string fullPath = Path.Combine(this.Enlistment.GitPackRoot, fileName); + + Task flushTask; + long fileLength; + this.TryWriteTempFile( + tracer: null, + source: stream, + tempFilePath: fullPath, + fileLength: out fileLength, + flushTask: out flushTask, + throwOnError: true); + + flushTask?.Wait(); + + return fullPath; + } + + public virtual bool TryWriteTempFile( + ITracer tracer, + Stream source, + string tempFilePath, + out long fileLength, + out Task flushTask, + bool throwOnError = false) + { + fileLength = 0; + flushTask = null; + try + { + Stream fileStream = null; + + try + { + fileStream = this.fileSystem.OpenFileStream( + tempFilePath, + FileMode.OpenOrCreate, + FileAccess.Write, + FileShare.Read, + callFlushFileBuffers: false); // Any flushing to disk will be done asynchronously + + StreamUtil.CopyToWithBuffer(source, fileStream); + fileLength = fileStream.Length; + + if (this.Enlistment.FlushFileBuffersForPacks) + { + // Flush any data buffered in FileStream to the file system + fileStream.Flush(); + + // FlushFileBuffers using FlushAsync + // Do this last to ensure that the stream is not being accessed after it's been disposed + flushTask = fileStream.FlushAsync().ContinueWith((result) => fileStream.Dispose()); + } + } + finally + { + if (flushTask == null && fileStream != null) + { + fileStream.Dispose(); + } + } + + this.ValidateTempFile(tempFilePath, tempFilePath); + } + catch (Exception ex) + { + if (flushTask != null) + { + flushTask.Wait(); + flushTask = null; + } + + this.CleanupTempFile(this.Tracer, tempFilePath); + + if (tracer != null) + { + EventMetadata metadata = CreateEventMetadata(ex); + metadata.Add("tempFilePath", tempFilePath); + tracer.RelatedWarning(metadata, $"{nameof(this.TryWriteTempFile)}: Exception caught while writing temp file", Keywords.Telemetry); + } + + if (throwOnError) + { + throw; + } + else + { + return false; + } + } + + return true; + } + + public virtual GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null) + { + string packfilePath = GetRandomPackName(this.Enlistment.GitPackRoot); + + Exception moveFileException = null; + try + { + // We're indexing a pack file that was saved to a temp file name, and so it must be renamed + // to its final name before indexing ('git index-pack' requires that the pack file name end with .pack) + this.fileSystem.MoveFile(tempPackPath, packfilePath); + } + catch (IOException e) + { + moveFileException = e; + } + catch (UnauthorizedAccessException e) + { + moveFileException = e; + } + + if (moveFileException != null) + { + EventMetadata failureMetadata = CreateEventMetadata(moveFileException); + failureMetadata.Add("tempPackPath", tempPackPath); + failureMetadata.Add("packfilePath", packfilePath); + + this.fileSystem.TryDeleteFile(tempPackPath, metadataKey: nameof(tempPackPath), metadata: failureMetadata); + + this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexTempPackFile): Exception caught while trying to move temp pack file}"); + + return new GitProcess.Result( + string.Empty, + moveFileException != null ? moveFileException.Message : "Failed to move temp pack file to final path", + GitProcess.Result.GenericFailureCode); + } + + // TryBuildIndex will delete the pack file if indexing fails + GitProcess.Result result; + this.TryBuildIndex(this.Tracer, packfilePath, out result, gitProcess); + return result; + } + + public virtual GitProcess.Result IndexPackFile(string packfilePath, GitProcess gitProcess) + { + string tempIdxPath = Path.ChangeExtension(packfilePath, TempIdxExtension); + string idxPath = Path.ChangeExtension(packfilePath, ".idx"); + + Exception indexPackException = null; + try + { + if (gitProcess == null) + { + gitProcess = new GitProcess(this.Enlistment); + } + + GitProcess.Result result = gitProcess.IndexPack(packfilePath, tempIdxPath); + if (result.ExitCodeIsFailure) + { + Exception exception; + if (!this.fileSystem.TryDeleteFile(tempIdxPath, exception: out exception)) + { + EventMetadata metadata = CreateEventMetadata(exception); + metadata.Add("tempIdxPath", tempIdxPath); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to cleanup temp idx file after index pack failure"); + } + } + else + { + if (this.Enlistment.FlushFileBuffersForPacks) + { + Exception exception; + string error; + if (!this.TryFlushFileBuffers(tempIdxPath, out exception, out error)) + { + EventMetadata metadata = CreateEventMetadata(exception); + metadata.Add("packfilePath", packfilePath); + metadata.Add("tempIndexPath", tempIdxPath); + metadata.Add("error", error); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.IndexPackFile)}: Failed to flush temp idx file buffers"); + } + } + + this.fileSystem.MoveAndOverwriteFile(tempIdxPath, idxPath); + } + + return result; + } + catch (Win32Exception e) + { + indexPackException = e; + } + catch (IOException e) + { + indexPackException = e; + } + catch (UnauthorizedAccessException e) + { + indexPackException = e; + } + + EventMetadata failureMetadata = CreateEventMetadata(indexPackException); + failureMetadata.Add("packfilePath", packfilePath); + failureMetadata.Add("tempIdxPath", tempIdxPath); + failureMetadata.Add("idxPath", idxPath); + + this.fileSystem.TryDeleteFile(tempIdxPath, metadataKey: nameof(tempIdxPath), metadata: failureMetadata); + this.fileSystem.TryDeleteFile(idxPath, metadataKey: nameof(idxPath), metadata: failureMetadata); + + this.Tracer.RelatedWarning(failureMetadata, $"{nameof(this.IndexPackFile): Exception caught while trying to index pack file}"); + + return new GitProcess.Result( + string.Empty, + indexPackException != null ? indexPackException.Message : "Failed to index pack file", + GitProcess.Result.GenericFailureCode); + } + + public virtual string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "") + { + if (this.fileSystem.DirectoryExists(packFolderPath)) + { + try + { + return this.fileSystem.GetFiles(packFolderPath, prefixFilter + "*.pack"); + } + catch (DirectoryNotFoundException e) + { + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add("packFolderPath", packFolderPath); + metadata.Add("prefixFilter", prefixFilter); + metadata.Add(TracingConstants.MessageKey.InfoMessage, "${nameof(this.ReadPackFileNames)}: Caught DirectoryNotFoundException exception"); + this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.ReadPackFileNames)}_DirectoryNotFound", metadata); + + return new string[0]; + } + } + + return new string[0]; + } + + public virtual bool IsUsingCacheServer() + { + return !this.GitObjectRequestor.CacheServer.IsNone(this.Enlistment.RepoUrl); + } + + private static string GetRandomPackName(string packRoot) + { + string packName = "pack-" + Guid.NewGuid().ToString("N") + ".pack"; + return Path.Combine(packRoot, packName); + } + + 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 bool TryMovePackAndIdxFromTempFolder(string packName, string packTempPath, string idxName, string idxTempPath, out Exception exception) + { + exception = null; + string finalPackPath = Path.Combine(this.Enlistment.GitPackRoot, packName); + string finalIdxPath = Path.Combine(this.Enlistment.GitPackRoot, idxName); + + try + { + this.fileSystem.MoveAndOverwriteFile(packTempPath, finalPackPath); + this.fileSystem.MoveAndOverwriteFile(idxTempPath, finalIdxPath); + } + catch (Win32Exception e) + { + exception = e; + + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add("packName", packName); + metadata.Add("packTempPath", packTempPath); + metadata.Add("idxName", idxName); + metadata.Add("idxTempPath", idxTempPath); + + this.fileSystem.TryDeleteFile(idxTempPath, metadataKey: nameof(idxTempPath), metadata: metadata); + this.fileSystem.TryDeleteFile(finalIdxPath, metadataKey: nameof(finalIdxPath), metadata: metadata); + this.fileSystem.TryDeleteFile(packTempPath, metadataKey: nameof(packTempPath), metadata: metadata); + this.fileSystem.TryDeleteFile(finalPackPath, metadataKey: nameof(finalPackPath), metadata: metadata); + + this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryMovePackAndIdxFromTempFolder): Failed to move pack and idx from temp folder}"); + + return false; + } + + return true; + } + + private bool TryFlushFileBuffers(string path, out Exception exception, out string error) + { + error = null; + + FileAttributes originalAttributes; + if (!this.TryGetAttributes(path, out originalAttributes, out exception)) + { + error = "Failed to get original attributes, skipping flush"; + return false; + } + + bool readOnly = (originalAttributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; + + if (readOnly) + { + if (!this.TrySetAttributes(path, originalAttributes & ~FileAttributes.ReadOnly, out exception)) + { + error = "Failed to clear read-only attribute, skipping flush"; + return false; + } + } + + bool flushedBuffers = false; + try + { + ScalarPlatform.Instance.FileSystem.FlushFileBuffers(path); + flushedBuffers = true; + } + catch (Win32Exception e) + { + exception = e; + error = "Win32Exception while trying to flush file buffers"; + } + + if (readOnly) + { + Exception setAttributesException; + if (!this.TrySetAttributes(path, originalAttributes, out setAttributesException)) + { + EventMetadata metadata = CreateEventMetadata(setAttributesException); + metadata.Add("path", path); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryFlushFileBuffers)}: Failed to re-enable read-only bit"); + } + } + + return flushedBuffers; + } + + private bool TryGetAttributes(string path, out FileAttributes attributes, out Exception exception) + { + attributes = 0; + exception = null; + try + { + attributes = this.fileSystem.GetAttributes(path); + return true; + } + catch (IOException e) + { + exception = e; + } + catch (UnauthorizedAccessException e) + { + exception = e; + } + + return false; + } + + private bool TrySetAttributes(string path, FileAttributes attributes, out Exception exception) + { + exception = null; + + try + { + this.fileSystem.SetAttributes(path, attributes); + return true; + } + catch (IOException e) + { + exception = e; + } + catch (UnauthorizedAccessException e) + { + exception = e; + } + + return false; + } + + private Stream OpenTempLooseObjectStream(string path) + { + return this.fileSystem.OpenFileStream( + path, + FileMode.Create, + FileAccess.Write, + FileShare.None, + FileOptions.SequentialScan, + callFlushFileBuffers: false); + } + + private LooseObjectToWrite GetLooseObjectDestination(string sha) + { + string firstTwoDigits = sha.Substring(0, 2); + string remainingDigits = sha.Substring(2); + string twoLetterFolderName = Path.Combine(this.Enlistment.GitObjectsRoot, firstTwoDigits); + this.fileSystem.CreateDirectory(twoLetterFolderName); + + return new LooseObjectToWrite( + tempFile: Path.Combine(twoLetterFolderName, Path.GetRandomFileName()), + actualFile: Path.Combine(twoLetterFolderName, remainingDigits)); + } + + /// + /// Uses a to read the packs from the stream. + /// + private RetryWrapper.CallbackResult DeserializePrefetchPacks( + GitEndPointResponseData response, + ref long latestTimestamp, + ref long bytesDownloaded, + ref List packIndexes, + GitProcess gitProcess) + { + if (packIndexes == null) + { + packIndexes = new List(); + } + + using (ITracer activity = this.Tracer.StartActivity("DeserializePrefetchPacks", EventLevel.Informational)) + { + PrefetchPacksDeserializer deserializer = new PrefetchPacksDeserializer(response.Stream); + + string tempPackFolderPath = Path.Combine(this.Enlistment.GitPackRoot, TempPackFolder); + this.fileSystem.CreateDirectory(tempPackFolderPath); + + List tempPacks = new List(); + foreach (PrefetchPacksDeserializer.PackAndIndex pack in deserializer.EnumeratePacks()) + { + // The advertised size may not match the actual, on-disk size. + long indexLength = 0; + long packLength; + + // Write the temp and index to a temp folder to avoid putting corrupt files in the pack folder + // Once the files are validated and flushed they can be moved to the pack folder + string packName = string.Format("{0}-{1}-{2}.pack", ScalarConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); + string packTempPath = Path.Combine(tempPackFolderPath, packName); + string idxName = string.Format("{0}-{1}-{2}.idx", ScalarConstants.PrefetchPackPrefix, pack.Timestamp, pack.UniqueId); + string idxTempPath = Path.Combine(tempPackFolderPath, idxName); + + EventMetadata data = CreateEventMetadata(); + data["timestamp"] = pack.Timestamp.ToString(); + data["uniqueId"] = pack.UniqueId; + activity.RelatedEvent(EventLevel.Informational, "Receiving Pack/Index", data); + + // Write the pack + // If it fails, TryWriteTempFile cleans up the file and we retry the prefetch + Task packFlushTask; + if (!this.TryWriteTempFile(activity, pack.PackStream, packTempPath, out packLength, out packFlushTask)) + { + bytesDownloaded += packLength; + return new RetryWrapper.CallbackResult(null, true); + } + + bytesDownloaded += packLength; + + // We will try to build an index if the server does not send one + if (pack.IndexStream == null) + { + GitProcess.Result result; + if (!this.TryBuildIndex(activity, packTempPath, out result, gitProcess)) + { + if (packFlushTask != null) + { + packFlushTask.Wait(); + } + + // Move whatever has been successfully downloaded so far + Exception moveException; + this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out moveException); + + return new RetryWrapper.CallbackResult(null, true); + } + + tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, idxFlushTask: null)); + } + else + { + Task indexFlushTask; + if (this.TryWriteTempFile(activity, pack.IndexStream, idxTempPath, out indexLength, out indexFlushTask)) + { + tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, indexFlushTask)); + } + else + { + bytesDownloaded += indexLength; + + // Try to build the index manually, then retry the prefetch + GitProcess.Result result; + if (this.TryBuildIndex(activity, packTempPath, out result, gitProcess)) + { + // If we were able to recreate the failed index + // we can start the prefetch at the next timestamp + tempPacks.Add(new TempPrefetchPackAndIdx(pack.Timestamp, packName, packTempPath, packFlushTask, idxName, idxTempPath, idxFlushTask: null)); + } + else + { + if (packFlushTask != null) + { + packFlushTask.Wait(); + } + } + + // Move whatever has been successfully downloaded so far + Exception moveException; + this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out moveException); + + // The download stream will not be in a good state if the index download fails. + // So we have to restart the prefetch + return new RetryWrapper.CallbackResult(null, true); + } + } + + bytesDownloaded += indexLength; + } + + Exception exception = null; + if (!this.TryFlushAndMoveTempPacks(tempPacks, ref latestTimestamp, out exception)) + { + return new RetryWrapper.CallbackResult(exception, true); + } + + foreach (TempPrefetchPackAndIdx tempPack in tempPacks) + { + packIndexes.Add(tempPack.IdxName); + } + + return new RetryWrapper.CallbackResult( + new GitObjectsHttpRequestor.GitObjectTaskResult(success: true)); + } + } + + private bool TryFlushAndMoveTempPacks(List tempPacks, ref long latestTimestamp, out Exception exception) + { + exception = null; + bool moveFailed = false; + foreach (TempPrefetchPackAndIdx tempPack in tempPacks) + { + if (tempPack.PackFlushTask != null) + { + tempPack.PackFlushTask.Wait(); + } + + if (tempPack.IdxFlushTask != null) + { + tempPack.IdxFlushTask.Wait(); + } + + // If we've hit a failure moving temp files, we should stop trying to move them (but we still need to wait for all outstanding + // flush tasks) + if (!moveFailed) + { + if (this.TryMovePackAndIdxFromTempFolder(tempPack.PackName, tempPack.PackFullPath, tempPack.IdxName, tempPack.IdxFullPath, out exception)) + { + latestTimestamp = tempPack.Timestamp; + } + else + { + moveFailed = true; + } + } + } + + return !moveFailed; + } + + /// + /// Attempts to build an index for the specified path. If building the index fails, the pack file is deleted + /// + private bool TryBuildIndex( + ITracer activity, + string packFullPath, + out GitProcess.Result result, + GitProcess gitProcess) + { + result = this.IndexPackFile(packFullPath, gitProcess); + + if (result.ExitCodeIsFailure) + { + EventMetadata errorMetadata = CreateEventMetadata(); + Exception exception; + if (!this.fileSystem.TryDeleteFile(packFullPath, exception: out exception)) + { + if (exception != null) + { + errorMetadata.Add("deleteException", exception.ToString()); + } + + errorMetadata.Add("deletedBadPack", "false"); + } + + errorMetadata.Add("Operation", nameof(this.TryBuildIndex)); + errorMetadata.Add("packFullPath", packFullPath); + activity.RelatedWarning(errorMetadata, result.Errors, Keywords.Telemetry); + } + + return result.ExitCodeIsSuccess; + } + + private void CleanupTempFile(ITracer activity, string fullPath) + { + Exception e; + if (!this.fileSystem.TryDeleteFile(fullPath, exception: out e)) + { + EventMetadata info = CreateEventMetadata(e); + info.Add("file", fullPath); + activity.RelatedWarning(info, "Failed to cleanup temp file"); + } + } + + private void FinalizeTempFile(string sha, LooseObjectToWrite toWrite, bool overwriteExistingObject) + { + try + { + // Checking for existence reduces warning outputs when a streamed download tries. + if (this.fileSystem.FileExists(toWrite.ActualFile)) + { + if (overwriteExistingObject) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("file", toWrite.ActualFile); + metadata.Add("tempFile", toWrite.TempFile); + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.FinalizeTempFile)}: Overwriting existing loose object"); + this.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.FinalizeTempFile)}_OverwriteExistingObject", metadata); + + this.ValidateTempFile(toWrite.TempFile, sha); + this.fileSystem.MoveAndOverwriteFile(toWrite.TempFile, toWrite.ActualFile); + } + } + else + { + this.ValidateTempFile(toWrite.TempFile, sha); + + try + { + this.fileSystem.MoveFile(toWrite.TempFile, toWrite.ActualFile); + } + catch (IOException ex) + { + // IOExceptions happen when someone else is writing to our object. + // That implies they are doing what we're doing, which should be a success + EventMetadata info = CreateEventMetadata(ex); + info.Add("file", toWrite.ActualFile); + this.Tracer.RelatedWarning(info, $"{nameof(this.FinalizeTempFile)}: Exception moving temp file"); + } + } + } + finally + { + this.CleanupTempFile(this.Tracer, toWrite.TempFile); + } + } + + private void ValidateTempFile(string tempFilePath, string finalFilePath) + { + using (Stream fs = this.fileSystem.OpenFileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, callFlushFileBuffers: false)) + { + if (fs.Length == 0) + { + throw new RetryableException($"Temp file '{tempFilePath}' for '{finalFilePath}' was written with 0 bytes"); + } + else + { + byte[] buffer = new byte[10]; + + // Temp files should always have at least one non-zero byte + int bytesRead = fs.Read(buffer, 0, buffer.Length); + if (buffer.All(b => b == 0)) + { + RetryableException ex = new RetryableException( + $"Temp file '{tempFilePath}' for '{finalFilePath}' was written with {bytesRead} null bytes"); + + EventMetadata eventInfo = CreateEventMetadata(ex); + eventInfo.Add("file", tempFilePath); + eventInfo.Add("finalFilePath", finalFilePath); + this.Tracer.RelatedWarning(eventInfo, $"{nameof(this.ValidateTempFile)}: Temp file invalid"); + + throw ex; + } + } + } + } + + private RetryWrapper.CallbackResult TrySavePackOrLooseObject( + IEnumerable objectShas, + bool unpackObjects, + GitEndPointResponseData responseData, + GitProcess gitProcess) + { + if (responseData.ContentType == GitObjectContentType.LooseObject) + { + List objectShaList = objectShas.Distinct().ToList(); + if (objectShaList.Count != 1) + { + return new RetryWrapper.CallbackResult(new InvalidOperationException("Received loose object when multiple objects were requested."), shouldRetry: false); + } + + // To reduce allocations, reuse the same buffer when writing objects in this batch + byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; + + this.WriteLooseObject(responseData.Stream, objectShaList[0], overwriteExistingObject: false, bufToCopyWith: bufToCopyWith); + } + else if (responseData.ContentType == GitObjectContentType.BatchedLooseObjects) + { + // To reduce allocations, reuse the same buffer when writing objects in this batch + byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; + + BatchedLooseObjectDeserializer deserializer = new BatchedLooseObjectDeserializer( + responseData.Stream, + (stream, sha) => this.WriteLooseObject(stream, sha, overwriteExistingObject: false, bufToCopyWith: bufToCopyWith)); + deserializer.ProcessObjects(); + } + else + { + GitProcess.Result result = this.TryAddPackFile(responseData.Stream, unpackObjects, gitProcess); + if (result.ExitCodeIsFailure) + { + return new RetryWrapper.CallbackResult(new InvalidOperationException("Could not add pack file: " + result.Errors), shouldRetry: false); + } + } + + return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); + } + + private GitProcess.Result TryAddPackFile(Stream contents, bool unpackObjects, GitProcess gitProcess) + { + GitProcess.Result result; + + this.fileSystem.CreateDirectory(this.Enlistment.GitPackRoot); + + if (unpackObjects) + { + result = new GitProcess(this.Enlistment).UnpackObjects(contents); + } + else + { + string tempPackPath = this.WriteTempPackFile(contents); + return this.IndexTempPackFile(tempPackPath, gitProcess); + } + + return result; + } + + private struct LooseObjectToWrite + { + public readonly string TempFile; + public readonly string ActualFile; + + public LooseObjectToWrite(string tempFile, string actualFile) + { + this.TempFile = tempFile; + this.ActualFile = actualFile; + } + } + + private class TempPrefetchPackAndIdx + { + public TempPrefetchPackAndIdx( + long timestamp, + string packName, + string packFullPath, + Task packFlushTask, + string idxName, + string idxFullPath, + Task idxFlushTask) + { + this.Timestamp = timestamp; + this.PackName = packName; + this.PackFullPath = packFullPath; + this.PackFlushTask = packFlushTask; + this.IdxName = idxName; + this.IdxFullPath = idxFullPath; + this.IdxFlushTask = idxFlushTask; + } + + public long Timestamp { get; } + public string PackName { get; } + public string PackFullPath { get; } + public Task PackFlushTask { get; } + public string IdxName { get; } + public string IdxFullPath { get; } + public Task IdxFlushTask { get; } + } + } +} diff --git a/Scalar.Common/Git/GitOid.cs b/Scalar.Common/Git/GitOid.cs index 8192304ed6..c47b612ba5 100644 --- a/Scalar.Common/Git/GitOid.cs +++ b/Scalar.Common/Git/GitOid.cs @@ -1,17 +1,17 @@ -using System.Runtime.InteropServices; - -namespace Scalar.Common.Git -{ - [StructLayout(LayoutKind.Sequential)] - public struct GitOid - { - // OIDs are 20 bytes long - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] - public byte[] Id; - - public override string ToString() - { - return SHA1Util.HexStringFromBytes(this.Id); - } - } -} +using System.Runtime.InteropServices; + +namespace Scalar.Common.Git +{ + [StructLayout(LayoutKind.Sequential)] + public struct GitOid + { + // OIDs are 20 bytes long + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)] + public byte[] Id; + + public override string ToString() + { + return SHA1Util.HexStringFromBytes(this.Id); + } + } +} diff --git a/Scalar.Common/Git/GitPathConverter.cs b/Scalar.Common/Git/GitPathConverter.cs index f2c916a49b..93f7cfeddd 100644 --- a/Scalar.Common/Git/GitPathConverter.cs +++ b/Scalar.Common/Git/GitPathConverter.cs @@ -1,58 +1,58 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Scalar.Common.Git -{ - public static class GitPathConverter - { - private const int CharsInOctet = 3; - private const char OctetIndicator = '\\'; - - public static string ConvertPathOctetsToUtf8(string filePath) - { - if (filePath == null) - { - return null; - } - - int octetIndicatorIndex = filePath.IndexOf(OctetIndicator); - if (octetIndicatorIndex == -1) - { - return filePath; - } - - StringBuilder converted = new StringBuilder(); - List octets = new List(); - int index = 0; - while (octetIndicatorIndex != -1) - { - converted.Append(filePath.Substring(index, octetIndicatorIndex - index)); - while (octetIndicatorIndex < filePath.Length && filePath[octetIndicatorIndex] == OctetIndicator) - { - string octet = filePath.Substring(octetIndicatorIndex + 1, CharsInOctet); - octets.Add(Convert.ToByte(octet, 8)); - octetIndicatorIndex += CharsInOctet + 1; - } - - AddOctetsAsUtf8(converted, octets); - index = octetIndicatorIndex; - octetIndicatorIndex = filePath.IndexOf(OctetIndicator, octetIndicatorIndex); - } - - AddOctetsAsUtf8(converted, octets); - converted.Append(filePath.Substring(index)); - - return converted.ToString(); - } - - private static void AddOctetsAsUtf8(StringBuilder converted, List octets) - { - if (octets.Count > 0) - { - converted.Append(Encoding.UTF8.GetChars(octets.ToArray())); - octets.Clear(); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Text; + +namespace Scalar.Common.Git +{ + public static class GitPathConverter + { + private const int CharsInOctet = 3; + private const char OctetIndicator = '\\'; + + public static string ConvertPathOctetsToUtf8(string filePath) + { + if (filePath == null) + { + return null; + } + + int octetIndicatorIndex = filePath.IndexOf(OctetIndicator); + if (octetIndicatorIndex == -1) + { + return filePath; + } + + StringBuilder converted = new StringBuilder(); + List octets = new List(); + int index = 0; + while (octetIndicatorIndex != -1) + { + converted.Append(filePath.Substring(index, octetIndicatorIndex - index)); + while (octetIndicatorIndex < filePath.Length && filePath[octetIndicatorIndex] == OctetIndicator) + { + string octet = filePath.Substring(octetIndicatorIndex + 1, CharsInOctet); + octets.Add(Convert.ToByte(octet, 8)); + octetIndicatorIndex += CharsInOctet + 1; + } + + AddOctetsAsUtf8(converted, octets); + index = octetIndicatorIndex; + octetIndicatorIndex = filePath.IndexOf(OctetIndicator, octetIndicatorIndex); + } + + AddOctetsAsUtf8(converted, octets); + converted.Append(filePath.Substring(index)); + + return converted.ToString(); + } + + private static void AddOctetsAsUtf8(StringBuilder converted, List octets) + { + if (octets.Count > 0) + { + converted.Append(Encoding.UTF8.GetChars(octets.ToArray())); + octets.Clear(); + } + } + } +} diff --git a/Scalar.Common/Git/GitProcess.cs b/Scalar.Common/Git/GitProcess.cs index 8ac76b8d35..bfd745db21 100644 --- a/Scalar.Common/Git/GitProcess.cs +++ b/Scalar.Common/Git/GitProcess.cs @@ -1,973 +1,973 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.Common.Git -{ - public class GitProcess : ICredentialStore - { - private const int HResultEHANDLE = -2147024890; // 0x80070006 E_HANDLE - - private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false); - private static bool failedToSetEncoding = false; - - /// - /// Lock taken for duration of running executingProcess. - /// - private object executionLock = new object(); - - /// - /// Lock taken when changing the running state of executingProcess. - /// - /// Can be taken within executionLock. - /// - private object processLock = new object(); - - private string gitBinPath; - private string workingDirectoryRoot; - private string dotGitRoot; - private Process executingProcess; - private bool stopping; - - static GitProcess() - { - // If the encoding is UTF8, .Net's default behavior will include a BOM - // We need to use the BOM-less encoding because Git doesn't understand it - if (Console.InputEncoding.CodePage == UTF8NoBOM.CodePage) - { - try - { - Console.InputEncoding = UTF8NoBOM; - } - catch (IOException ex) when (ex.HResult == HResultEHANDLE) - { - // If the standard input for a console is redirected / not available, - // then we might not be able to set the InputEncoding here. - // In practice, this can happen if we attempt to run a GitProcess from within a Service, - // such as Scalar.Service. - // Record that we failed to set the encoding, but do not quite the process. - // This means that git commands that use stdin will not work, but - // for our scenarios, we do not expect these calls at this this time. - // We will check and fail if we attempt to write to stdin in in a git call below. - GitProcess.failedToSetEncoding = true; - } - } - } - - public GitProcess(Enlistment enlistment) - : this(enlistment.GitBinPath, enlistment.WorkingDirectoryBackingRoot) - { - } - - public GitProcess(string gitBinPath, string workingDirectoryRoot) - { - if (string.IsNullOrWhiteSpace(gitBinPath)) - { - throw new ArgumentException(nameof(gitBinPath)); - } - - this.gitBinPath = gitBinPath; - this.workingDirectoryRoot = workingDirectoryRoot; - - if (this.workingDirectoryRoot != null) - { - this.dotGitRoot = Path.Combine(this.workingDirectoryRoot, ScalarConstants.DotGit.Root); - } - } - - public bool LowerPriority { get; set; } - - public static Result Init(Enlistment enlistment) - { - return new GitProcess(enlistment).InvokeGitOutsideEnlistment("init \"" + enlistment.WorkingDirectoryBackingRoot + "\""); - } - - public static ConfigResult GetFromGlobalConfig(string gitBinPath, string settingName) - { - return new ConfigResult( - new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --global " + settingName), - settingName); - } - - public static ConfigResult GetFromSystemConfig(string gitBinPath, string settingName) - { - return new ConfigResult( - new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --system " + settingName), - settingName); - } - - public static ConfigResult GetFromFileConfig(string gitBinPath, string configFile, string settingName) - { - return new ConfigResult( - new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --file " + configFile + " " + settingName), - settingName); - } - - public static bool TryGetVersion(string gitBinPath, out GitVersion gitVersion, out string error) - { - GitProcess gitProcess = new GitProcess(gitBinPath, null); - Result result = gitProcess.InvokeGitOutsideEnlistment("--version"); - string version = result.Output; - - if (result.ExitCodeIsFailure || !GitVersion.TryParseGitVersionCommandResult(version, out gitVersion)) - { - gitVersion = null; - error = "Unable to determine installed git version. " + version; - return false; - } - - error = null; - return true; - } - - /// - /// Tries to kill the run git process. Make sure you only use this on git processes that can safely be killed! - /// - /// Name of the running process - /// Exit code of the kill. -1 means there was no running process. - /// Error message of the kill - /// - public bool TryKillRunningProcess(out string processName, out int exitCode, out string error) - { - this.stopping = true; - processName = null; - exitCode = -1; - error = null; - - lock (this.processLock) - { - Process process = this.executingProcess; - - if (process != null) - { - processName = process.ProcessName; - - return ScalarPlatform.Instance.TryKillProcessTree(process.Id, out exitCode, out error); - } - - return true; - } - } - - public virtual bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("url={0}\n", repoUrl); - - // Passing the username and password that we want to signal rejection for is optional. - // Credential helpers that support it can use the provided username/password values to - // perform a check that they're being asked to delete the same stored credential that - // the caller is asking them to erase. - // Ideally, we would provide these values if available, however it does not work as expected - // with our main credential helper - Windows GCM. With GCM for Windows, the credential acquired - // with credential fill for dev.azure.com URLs are not erased when the user name / password are passed in. - // Until the default credential helper works with this pattern, reject credential with just the URL. - - sb.Append("\n"); - - string stdinConfig = sb.ToString(); - - Result result = this.InvokeGitOutsideEnlistment( - GenerateCredentialVerbCommand("reject"), - stdin => stdin.Write(stdinConfig), - null); - - if (result.ExitCodeIsFailure) - { - tracer.RelatedWarning("Git could not reject credentials: {0}", result.Errors); - - errorMessage = result.Errors; - return false; - } - - errorMessage = null; - return true; - } - - public virtual bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) - { - StringBuilder sb = new StringBuilder(); - sb.AppendFormat("url={0}\n", repoUrl); - sb.AppendFormat("username={0}\n", username); - sb.AppendFormat("password={0}\n", password); - sb.Append("\n"); - - string stdinConfig = sb.ToString(); - - Result result = this.InvokeGitOutsideEnlistment( - GenerateCredentialVerbCommand("approve"), - stdin => stdin.Write(stdinConfig), - null); - - if (result.ExitCodeIsFailure) - { - tracer.RelatedWarning("Git could not approve credentials: {0}", result.Errors); - - errorMessage = result.Errors; - return false; - } - - errorMessage = null; - return true; - } - - /// - /// Input for certificate credentials looks like - /// protocol=cert - /// path=[http.sslCert value] - /// username = - /// - public virtual bool TryGetCertificatePassword( - ITracer tracer, - string certificatePath, - out string password, - out string errorMessage) - { - password = null; - errorMessage = null; - - using (ITracer activity = tracer.StartActivity("TryGetCertificatePassword", EventLevel.Informational)) - { - Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( - "credential fill", - stdin => stdin.Write("protocol=cert\npath=" + certificatePath + "\nusername=\n\n"), - parseStdOutLine: null); - - if (gitCredentialOutput.ExitCodeIsFailure) - { - EventMetadata errorData = new EventMetadata(); - errorData.Add("CertificatePath", certificatePath); - tracer.RelatedWarning( - errorData, - "Git could not get credentials: " + gitCredentialOutput.Errors, - Keywords.Network | Keywords.Telemetry); - errorMessage = gitCredentialOutput.Errors; - - return false; - } - - password = ParseValue(gitCredentialOutput.Output, "password="); - - bool success = password != null; - - EventMetadata metadata = new EventMetadata - { - { "Success", success }, - { "CertificatePath", certificatePath } - }; - - if (!success) - { - metadata.Add("Output", gitCredentialOutput.Output); - } - - activity.Stop(metadata); - return success; - } - } - - public virtual bool TryGetCredential( - ITracer tracer, - string repoUrl, - out string username, - out string password, - out string errorMessage) - { - username = null; - password = null; - errorMessage = null; - - using (ITracer activity = tracer.StartActivity(nameof(this.TryGetCredential), EventLevel.Informational)) - { - Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( - GenerateCredentialVerbCommand("fill"), - stdin => stdin.Write($"url={repoUrl}\n\n"), - parseStdOutLine: null); - - if (gitCredentialOutput.ExitCodeIsFailure) - { - EventMetadata errorData = new EventMetadata(); - tracer.RelatedWarning( - errorData, - "Git could not get credentials: " + gitCredentialOutput.Errors, - Keywords.Network | Keywords.Telemetry); - errorMessage = gitCredentialOutput.Errors; - - return false; - } - - username = ParseValue(gitCredentialOutput.Output, "username="); - password = ParseValue(gitCredentialOutput.Output, "password="); - - bool success = username != null && password != null; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("Success", success); - if (!success) - { - metadata.Add("Output", gitCredentialOutput.Output); - } - - activity.Stop(metadata); - return success; - } - } - - public bool IsValidRepo() - { - Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel"); - return result.ExitCodeIsSuccess; - } - - public Result RevParse(string gitRef) - { - return this.InvokeGitAgainstDotGitFolder("rev-parse " + gitRef); - } - - public void DeleteFromLocalConfig(string settingName) - { - this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName); - } - - public Result SetInLocalConfig(string settingName, string value, bool replaceAll = false) - { - return this.InvokeGitAgainstDotGitFolder(string.Format( - "config --local {0} \"{1}\" \"{2}\"", - replaceAll ? "--replace-all " : string.Empty, - settingName, - value)); - } - - public bool TryGetConfigUrlMatch(string section, string repositoryUrl, out Dictionary configSettings) - { - Result result = this.InvokeGitAgainstDotGitFolder($"config --get-urlmatch {section} {repositoryUrl}"); - if (result.ExitCodeIsFailure) - { - configSettings = null; - return false; - } - - configSettings = GitConfigHelper.ParseKeyValues(result.Output, ' '); - return true; - } - - public bool TryGetAllConfig(bool localOnly, out Dictionary configSettings) - { - configSettings = null; - string localParameter = localOnly ? "--local" : string.Empty; - ConfigResult result = new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --list " + localParameter), "--list"); - - if (result.TryParseAsString(out string output, out string _, string.Empty)) - { - configSettings = GitConfigHelper.ParseKeyValues(output); - return true; - } - - return false; - } - - /// - /// Get the config value give a setting name - /// - /// The name of the config setting - /// - /// If false, will run the call from inside the enlistment if the working dir found, - /// otherwise it will run it from outside the enlistment. - /// - /// The value found for the setting. - public virtual ConfigResult GetFromConfig(string settingName, bool forceOutsideEnlistment = false, PhysicalFileSystem fileSystem = null) - { - string command = string.Format("config {0}", settingName); - fileSystem = fileSystem ?? new PhysicalFileSystem(); - - // This method is called at clone time, so the physical repo may not exist yet. - return - fileSystem.DirectoryExists(this.workingDirectoryRoot) && !forceOutsideEnlistment - ? new ConfigResult(this.InvokeGitAgainstDotGitFolder(command), settingName) - : new ConfigResult(this.InvokeGitOutsideEnlistment(command), settingName); - } - - public ConfigResult GetFromLocalConfig(string settingName) - { - return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local " + settingName), settingName); - } - - /// - /// Safely gets the config value give a setting name - /// - /// The name of the config setting - /// - /// If false, will run the call from inside the enlistment if the working dir found, - /// otherwise it will run it from outside the enlistment. - /// - /// The value found for the config setting. - /// True if the config call was successful, false otherwise. - public bool TryGetFromConfig(string settingName, bool forceOutsideEnlistment, out string value, PhysicalFileSystem fileSystem = null) - { - value = null; - try - { - ConfigResult result = this.GetFromConfig(settingName, forceOutsideEnlistment, fileSystem); - return result.TryParseAsString(out value, out string _); - } - catch - { - } - - return false; - } - - public ConfigResult GetOriginUrl() - { - return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local remote.origin.url"), "remote.origin.url"); - } - - public Result DiffTree(string sourceTreeish, string targetTreeish, Action onResult) - { - return this.InvokeGitAgainstDotGitFolder("diff-tree -r -t " + sourceTreeish + " " + targetTreeish, null, onResult); - } - - public Result CreateBranchWithUpstream(string branchToCreate, string upstreamBranch) - { - return this.InvokeGitAgainstDotGitFolder("branch " + branchToCreate + " --track " + upstreamBranch); - } - - public Result ForceCheckout(string target) - { - return this.InvokeGitInWorkingDirectoryRoot("checkout -f " + target, useReadObjectHook: false); - } - - public Result ForceCheckoutAllFiles() - { - return this.InvokeGitInWorkingDirectoryRoot("checkout HEAD -- .", useReadObjectHook: true); - } - - public Result Status(bool allowObjectDownloads, bool useStatusCache, bool showUntracked = false) - { - string command = "status"; - if (!useStatusCache) - { - command += " --no-deserialize"; - } - - if (showUntracked) - { - command += " -uall"; - } - - return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: allowObjectDownloads); - } - - public Result SerializeStatus(bool allowObjectDownloads, string serializePath) - { - // specify ignored=matching and --untracked-files=complete - // so the status cache can answer status commands run by Visual Studio - // or tools with similar requirements. - return this.InvokeGitInWorkingDirectoryRoot( - string.Format("--no-optional-locks status \"--serialize={0}\" --ignored=matching --untracked-files=complete", serializePath), - useReadObjectHook: allowObjectDownloads); - } - - public Result UnpackObjects(Stream packFileStream) - { - return this.InvokeGitAgainstDotGitFolder( - "unpack-objects", - stdin => - { - packFileStream.CopyTo(stdin.BaseStream); - stdin.Write('\n'); - }, - null); - } - - public Result PackObjects(string filenamePrefix, string gitObjectsDirectory, Action packFileStream) - { - string packFilePath = Path.Combine(gitObjectsDirectory, ScalarConstants.DotGit.Objects.Pack.Name, filenamePrefix); - - // Since we don't provide paths we won't be able to complete good deltas - // avoid the unnecessary computation by setting window/depth to 0 - return this.InvokeGitAgainstDotGitFolder( - $"pack-objects {packFilePath} --non-empty --window=0 --depth=0 -q", - packFileStream, - parseStdOutLine: null, - gitObjectsDirectory: gitObjectsDirectory); - } - - /// - /// Write a new commit graph in the specified pack directory. Crawl the given pack- - /// indexes for commits and then close under everything reachable or exists in the - /// previous graph file. - /// - /// This will update the graph-head file to point to the new commit graph and delete - /// any expired graph files that previously existed. - /// - public Result WriteCommitGraph(string objectDir, List packs) - { - string command = "commit-graph write --stdin-packs --split --size-multiple=4 --object-dir \"" + objectDir + "\""; - return this.InvokeGitInWorkingDirectoryRoot( - command, - useReadObjectHook: true, - writeStdIn: writer => - { - foreach (string packIndex in packs) - { - writer.WriteLine(packIndex); - } - - // We need to close stdin or else the process will not terminate. - writer.Close(); - }); - } - - public Result VerifyCommitGraph(string objectDir) - { - string command = "commit-graph verify --shallow --object-dir \"" + objectDir + "\""; - return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: true); - } - - public Result IndexPack(string packfilePath, string idxOutputPath) - { - return this.InvokeGitAgainstDotGitFolder($"index-pack -o \"{idxOutputPath}\" \"{packfilePath}\""); - } - - /// - /// Write a new multi-pack-index (MIDX) in the specified pack directory. - /// - /// If no new packfiles are found, then this is a no-op. - /// - public Result WriteMultiPackIndex(string objectDir) - { - // We override the config settings so we keep writing the MIDX file even if it is disabled for reads. - return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index write --object-dir=\"" + objectDir + "\""); - } - - public Result VerifyMultiPackIndex(string objectDir) - { - return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"" + objectDir + "\""); - } - - public Result RemoteAdd(string remoteName, string url) - { - return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url); - } - - public Result LsTree(string treeish, Action parseStdOutLine, bool recursive, bool showAllTrees = false, bool showDirectories = false) - { - return this.InvokeGitAgainstDotGitFolder( - "ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + (showDirectories ? "-d " : string.Empty) + treeish, - null, - parseStdOutLine); - } - - public Result LsFiles(Action parseStdOutLine) - { - return this.InvokeGitInWorkingDirectoryRoot( - "ls-files -v", - useReadObjectHook: false, - parseStdOutLine: parseStdOutLine); - } - - public Result UpdateBranchSymbolicRef(string refToUpdate, string targetRef) - { - return this.InvokeGitAgainstDotGitFolder("symbolic-ref " + refToUpdate + " " + targetRef); - } - - public Result UpdateBranchSha(string refToUpdate, string targetSha) - { - // If oldCommitResult doesn't fail, then the branch exists and update-ref will want the old sha - Result oldCommitResult = this.RevParse(refToUpdate); - string oldSha = string.Empty; - if (oldCommitResult.ExitCodeIsSuccess) - { - oldSha = oldCommitResult.Output.TrimEnd('\n'); - } - - return this.InvokeGitAgainstDotGitFolder("update-ref --no-deref " + refToUpdate + " " + targetSha + " " + oldSha); - } - - public Result PrunePacked(string gitObjectDirectory) - { - return this.InvokeGitAgainstDotGitFolder( - "prune-packed -q", - writeStdIn: null, - parseStdOutLine: null, - gitObjectsDirectory: gitObjectDirectory); - } - - public Result MultiPackIndexExpire(string gitObjectDirectory) - { - return this.InvokeGitAgainstDotGitFolder($"multi-pack-index expire --object-dir=\"{gitObjectDirectory}\""); - } - - public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize) - { - return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize}"); - } - - public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory) - { - ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath); - processInfo.WorkingDirectory = workingDirectory; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardInput = true; - processInfo.RedirectStandardOutput = true; - processInfo.RedirectStandardError = redirectStandardError; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = true; - - processInfo.StandardOutputEncoding = UTF8NoBOM; - processInfo.StandardErrorEncoding = UTF8NoBOM; - - // Removing trace variables that might change git output and break parsing - // List of environment variables: https://git-scm.com/book/gr/v2/Git-Internals-Environment-Variables - foreach (string key in processInfo.EnvironmentVariables.Keys.Cast().ToList()) - { - // If GIT_TRACE is set to a fully-rooted path, then Git sends the trace - // output to that path instead of stdout (GIT_TRACE=1) or stderr (GIT_TRACE=2). - if (key.StartsWith("GIT_TRACE", StringComparison.OrdinalIgnoreCase)) - { - try - { - if (!Path.IsPathRooted(processInfo.EnvironmentVariables[key])) - { - processInfo.EnvironmentVariables.Remove(key); - } - } - catch (ArgumentException) - { - processInfo.EnvironmentVariables.Remove(key); - } - } - } - - processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; - processInfo.EnvironmentVariables["GCM_VALIDATE"] = "0"; - - if (gitObjectsDirectory != null) - { - processInfo.EnvironmentVariables["GIT_OBJECT_DIRECTORY"] = gitObjectsDirectory; - } - - if (!useReadObjectHook) - { - command = "-c " + GitConfigSetting.CoreVirtualizeObjectsName + "=false " + command; - } - - if (!string.IsNullOrEmpty(dotGitDirectory)) - { - command = "--git-dir=\"" + dotGitDirectory + "\" " + command; - } - - processInfo.Arguments = command; - - Process executingProcess = new Process(); - executingProcess.StartInfo = processInfo; - - return executingProcess; - } - - protected virtual Result InvokeGitImpl( - string command, - string workingDirectory, - string dotGitDirectory, - bool useReadObjectHook, - Action writeStdIn, - Action parseStdOutLine, - int timeoutMs, - string gitObjectsDirectory = null) - { - if (failedToSetEncoding && writeStdIn != null) - { - return new Result(string.Empty, "Attempting to use to stdin, but the process does not have the right input encodings set.", Result.GenericFailureCode); - } - - try - { - // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx - // To avoid deadlocks, use asynchronous read operations on at least one of the streams. - // Do not perform a synchronous read to the end of both redirected streams. - using (this.executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, redirectStandardError: true, gitObjectsDirectory: gitObjectsDirectory)) - { - StringBuilder output = new StringBuilder(); - StringBuilder errors = new StringBuilder(); - - this.executingProcess.ErrorDataReceived += (sender, args) => - { - if (args.Data != null) - { - errors.Append(args.Data + "\n"); - } - }; - this.executingProcess.OutputDataReceived += (sender, args) => - { - if (args.Data != null) - { - if (parseStdOutLine != null) - { - parseStdOutLine(args.Data); - } - else - { - output.Append(args.Data + "\n"); - } - } - }; - - lock (this.executionLock) - { - lock (this.processLock) - { - if (this.stopping) - { - return new Result(string.Empty, nameof(GitProcess) + " is stopping", Result.GenericFailureCode); - } - - this.executingProcess.Start(); - - if (this.LowerPriority) - { - try - { - this.executingProcess.PriorityClass = ProcessPriorityClass.BelowNormal; - } - catch (InvalidOperationException) - { - // This is thrown if the process completes before we can set its priority. - } - } - } - - writeStdIn?.Invoke(this.executingProcess.StandardInput); - this.executingProcess.StandardInput.Close(); - - this.executingProcess.BeginOutputReadLine(); - this.executingProcess.BeginErrorReadLine(); - - if (!this.executingProcess.WaitForExit(timeoutMs)) - { - this.executingProcess.Kill(); - - return new Result(output.ToString(), "Operation timed out: " + errors.ToString(), Result.GenericFailureCode); - } - } - - return new Result(output.ToString(), errors.ToString(), this.executingProcess.ExitCode); - } - } - catch (Win32Exception e) - { - return new Result(string.Empty, e.Message, Result.GenericFailureCode); - } - finally - { - this.executingProcess = null; - } - } - - private static string GenerateCredentialVerbCommand(string verb) - { - return $"-c {GitConfigSetting.CredentialUseHttpPath}=true credential {verb}"; - } - - private static string ParseValue(string contents, string prefix) - { - int startIndex = contents.IndexOf(prefix) + prefix.Length; - if (startIndex >= 0 && startIndex < contents.Length) - { - int endIndex = contents.IndexOf('\n', startIndex); - if (endIndex >= 0 && endIndex < contents.Length) - { - return - contents - .Substring(startIndex, endIndex - startIndex) - .Trim('\r'); - } - } - - return null; - } - - /// - /// Invokes git.exe without a working directory set. - /// - /// - /// For commands where git doesn't need to be (or can't be) run from inside an enlistment. - /// eg. 'git init' or 'git version' - /// - private Result InvokeGitOutsideEnlistment(string command) - { - return this.InvokeGitOutsideEnlistment(command, null, null); - } - - private Result InvokeGitOutsideEnlistment( - string command, - Action writeStdIn, - Action parseStdOutLine, - int timeout = -1) - { - return this.InvokeGitImpl( - command, - workingDirectory: Environment.SystemDirectory, - dotGitDirectory: null, - useReadObjectHook: false, - writeStdIn: writeStdIn, - parseStdOutLine: parseStdOutLine, - timeoutMs: timeout); - } - - /// - /// Invokes git.exe from an enlistment's repository root - /// - private Result InvokeGitInWorkingDirectoryRoot( - string command, - bool useReadObjectHook, - Action writeStdIn = null, - Action parseStdOutLine = null) - { - return this.InvokeGitImpl( - command, - workingDirectory: this.workingDirectoryRoot, - dotGitDirectory: null, - useReadObjectHook: useReadObjectHook, - writeStdIn: writeStdIn, - parseStdOutLine: parseStdOutLine, - timeoutMs: -1); - } - - /// - /// Invokes git.exe against an enlistment's .git folder. - /// This method should be used only with git-commands that ignore the working directory - /// - private Result InvokeGitAgainstDotGitFolder(string command) - { - return this.InvokeGitAgainstDotGitFolder(command, null, null); - } - - private Result InvokeGitAgainstDotGitFolder( - string command, - Action writeStdIn, - Action parseStdOutLine, - string gitObjectsDirectory = null) - { - // This git command should not need/use the working directory of the repo. - // Run git.exe in Environment.SystemDirectory to ensure the git.exe process - // does not touch the working directory - return this.InvokeGitImpl( - command, - workingDirectory: Environment.SystemDirectory, - dotGitDirectory: this.dotGitRoot, - useReadObjectHook: false, - writeStdIn: writeStdIn, - parseStdOutLine: parseStdOutLine, - timeoutMs: -1, - gitObjectsDirectory: gitObjectsDirectory); - } - - public class Result - { - public const int SuccessCode = 0; - public const int GenericFailureCode = 1; - - public Result(string stdout, string stderr, int exitCode) - { - this.Output = stdout; - this.Errors = stderr; - this.ExitCode = exitCode; - } - - public string Output { get; } - public string Errors { get; } - public int ExitCode { get; } - - public bool ExitCodeIsSuccess - { - get { return this.ExitCode == Result.SuccessCode; } - } - - public bool ExitCodeIsFailure - { - get { return !this.ExitCodeIsSuccess; } - } - - public bool StderrContainsErrors() - { - if (!string.IsNullOrWhiteSpace(this.Errors)) - { - return !this.Errors - .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .All(line => line.TrimStart().StartsWith("warning:", StringComparison.OrdinalIgnoreCase)); - } - - return false; - } - } - - public class ConfigResult - { - private readonly Result result; - private readonly string configName; - - public ConfigResult(Result result, string configName) - { - this.result = result; - this.configName = configName; - } - - public bool TryParseAsString(out string value, out string error, string defaultValue = null) - { - value = defaultValue; - error = string.Empty; - - if (this.result.ExitCodeIsFailure && this.result.StderrContainsErrors()) - { - error = "Error while reading '" + this.configName + "' from config: " + this.result.Errors; - return false; - } - - if (this.result.ExitCodeIsSuccess) - { - value = this.result.Output?.TrimEnd('\n'); - } - - return true; - } - - public bool TryParseAsInt(int defaultValue, int minValue, out int value, out string error) - { - value = defaultValue; - error = string.Empty; - - if (!this.TryParseAsString(out string valueString, out error)) - { - return false; - } - - if (string.IsNullOrWhiteSpace(valueString)) - { - // Use default value - return true; - } - - if (!int.TryParse(valueString, out value)) - { - error = string.Format("Misconfigured config setting {0}, could not parse value `{1}` as an int", this.configName, valueString); - return false; - } - - if (value < minValue) - { - error = string.Format("Invalid value {0} for setting {1}, value must be greater than or equal to {2}", value, this.configName, minValue); - return false; - } - - return true; - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.Common.Git +{ + public class GitProcess : ICredentialStore + { + private const int HResultEHANDLE = -2147024890; // 0x80070006 E_HANDLE + + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false); + private static bool failedToSetEncoding = false; + + /// + /// Lock taken for duration of running executingProcess. + /// + private object executionLock = new object(); + + /// + /// Lock taken when changing the running state of executingProcess. + /// + /// Can be taken within executionLock. + /// + private object processLock = new object(); + + private string gitBinPath; + private string workingDirectoryRoot; + private string dotGitRoot; + private Process executingProcess; + private bool stopping; + + static GitProcess() + { + // If the encoding is UTF8, .Net's default behavior will include a BOM + // We need to use the BOM-less encoding because Git doesn't understand it + if (Console.InputEncoding.CodePage == UTF8NoBOM.CodePage) + { + try + { + Console.InputEncoding = UTF8NoBOM; + } + catch (IOException ex) when (ex.HResult == HResultEHANDLE) + { + // If the standard input for a console is redirected / not available, + // then we might not be able to set the InputEncoding here. + // In practice, this can happen if we attempt to run a GitProcess from within a Service, + // such as Scalar.Service. + // Record that we failed to set the encoding, but do not quite the process. + // This means that git commands that use stdin will not work, but + // for our scenarios, we do not expect these calls at this this time. + // We will check and fail if we attempt to write to stdin in in a git call below. + GitProcess.failedToSetEncoding = true; + } + } + } + + public GitProcess(Enlistment enlistment) + : this(enlistment.GitBinPath, enlistment.WorkingDirectoryBackingRoot) + { + } + + public GitProcess(string gitBinPath, string workingDirectoryRoot) + { + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + throw new ArgumentException(nameof(gitBinPath)); + } + + this.gitBinPath = gitBinPath; + this.workingDirectoryRoot = workingDirectoryRoot; + + if (this.workingDirectoryRoot != null) + { + this.dotGitRoot = Path.Combine(this.workingDirectoryRoot, ScalarConstants.DotGit.Root); + } + } + + public bool LowerPriority { get; set; } + + public static Result Init(Enlistment enlistment) + { + return new GitProcess(enlistment).InvokeGitOutsideEnlistment("init \"" + enlistment.WorkingDirectoryBackingRoot + "\""); + } + + public static ConfigResult GetFromGlobalConfig(string gitBinPath, string settingName) + { + return new ConfigResult( + new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --global " + settingName), + settingName); + } + + public static ConfigResult GetFromSystemConfig(string gitBinPath, string settingName) + { + return new ConfigResult( + new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --system " + settingName), + settingName); + } + + public static ConfigResult GetFromFileConfig(string gitBinPath, string configFile, string settingName) + { + return new ConfigResult( + new GitProcess(gitBinPath, workingDirectoryRoot: null).InvokeGitOutsideEnlistment("config --file " + configFile + " " + settingName), + settingName); + } + + public static bool TryGetVersion(string gitBinPath, out GitVersion gitVersion, out string error) + { + GitProcess gitProcess = new GitProcess(gitBinPath, null); + Result result = gitProcess.InvokeGitOutsideEnlistment("--version"); + string version = result.Output; + + if (result.ExitCodeIsFailure || !GitVersion.TryParseGitVersionCommandResult(version, out gitVersion)) + { + gitVersion = null; + error = "Unable to determine installed git version. " + version; + return false; + } + + error = null; + return true; + } + + /// + /// Tries to kill the run git process. Make sure you only use this on git processes that can safely be killed! + /// + /// Name of the running process + /// Exit code of the kill. -1 means there was no running process. + /// Error message of the kill + /// + public bool TryKillRunningProcess(out string processName, out int exitCode, out string error) + { + this.stopping = true; + processName = null; + exitCode = -1; + error = null; + + lock (this.processLock) + { + Process process = this.executingProcess; + + if (process != null) + { + processName = process.ProcessName; + + return ScalarPlatform.Instance.TryKillProcessTree(process.Id, out exitCode, out error); + } + + return true; + } + } + + public virtual bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("url={0}\n", repoUrl); + + // Passing the username and password that we want to signal rejection for is optional. + // Credential helpers that support it can use the provided username/password values to + // perform a check that they're being asked to delete the same stored credential that + // the caller is asking them to erase. + // Ideally, we would provide these values if available, however it does not work as expected + // with our main credential helper - Windows GCM. With GCM for Windows, the credential acquired + // with credential fill for dev.azure.com URLs are not erased when the user name / password are passed in. + // Until the default credential helper works with this pattern, reject credential with just the URL. + + sb.Append("\n"); + + string stdinConfig = sb.ToString(); + + Result result = this.InvokeGitOutsideEnlistment( + GenerateCredentialVerbCommand("reject"), + stdin => stdin.Write(stdinConfig), + null); + + if (result.ExitCodeIsFailure) + { + tracer.RelatedWarning("Git could not reject credentials: {0}", result.Errors); + + errorMessage = result.Errors; + return false; + } + + errorMessage = null; + return true; + } + + public virtual bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string errorMessage) + { + StringBuilder sb = new StringBuilder(); + sb.AppendFormat("url={0}\n", repoUrl); + sb.AppendFormat("username={0}\n", username); + sb.AppendFormat("password={0}\n", password); + sb.Append("\n"); + + string stdinConfig = sb.ToString(); + + Result result = this.InvokeGitOutsideEnlistment( + GenerateCredentialVerbCommand("approve"), + stdin => stdin.Write(stdinConfig), + null); + + if (result.ExitCodeIsFailure) + { + tracer.RelatedWarning("Git could not approve credentials: {0}", result.Errors); + + errorMessage = result.Errors; + return false; + } + + errorMessage = null; + return true; + } + + /// + /// Input for certificate credentials looks like + /// protocol=cert + /// path=[http.sslCert value] + /// username = + /// + public virtual bool TryGetCertificatePassword( + ITracer tracer, + string certificatePath, + out string password, + out string errorMessage) + { + password = null; + errorMessage = null; + + using (ITracer activity = tracer.StartActivity("TryGetCertificatePassword", EventLevel.Informational)) + { + Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( + "credential fill", + stdin => stdin.Write("protocol=cert\npath=" + certificatePath + "\nusername=\n\n"), + parseStdOutLine: null); + + if (gitCredentialOutput.ExitCodeIsFailure) + { + EventMetadata errorData = new EventMetadata(); + errorData.Add("CertificatePath", certificatePath); + tracer.RelatedWarning( + errorData, + "Git could not get credentials: " + gitCredentialOutput.Errors, + Keywords.Network | Keywords.Telemetry); + errorMessage = gitCredentialOutput.Errors; + + return false; + } + + password = ParseValue(gitCredentialOutput.Output, "password="); + + bool success = password != null; + + EventMetadata metadata = new EventMetadata + { + { "Success", success }, + { "CertificatePath", certificatePath } + }; + + if (!success) + { + metadata.Add("Output", gitCredentialOutput.Output); + } + + activity.Stop(metadata); + return success; + } + } + + public virtual bool TryGetCredential( + ITracer tracer, + string repoUrl, + out string username, + out string password, + out string errorMessage) + { + username = null; + password = null; + errorMessage = null; + + using (ITracer activity = tracer.StartActivity(nameof(this.TryGetCredential), EventLevel.Informational)) + { + Result gitCredentialOutput = this.InvokeGitAgainstDotGitFolder( + GenerateCredentialVerbCommand("fill"), + stdin => stdin.Write($"url={repoUrl}\n\n"), + parseStdOutLine: null); + + if (gitCredentialOutput.ExitCodeIsFailure) + { + EventMetadata errorData = new EventMetadata(); + tracer.RelatedWarning( + errorData, + "Git could not get credentials: " + gitCredentialOutput.Errors, + Keywords.Network | Keywords.Telemetry); + errorMessage = gitCredentialOutput.Errors; + + return false; + } + + username = ParseValue(gitCredentialOutput.Output, "username="); + password = ParseValue(gitCredentialOutput.Output, "password="); + + bool success = username != null && password != null; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Success", success); + if (!success) + { + metadata.Add("Output", gitCredentialOutput.Output); + } + + activity.Stop(metadata); + return success; + } + } + + public bool IsValidRepo() + { + Result result = this.InvokeGitAgainstDotGitFolder("rev-parse --show-toplevel"); + return result.ExitCodeIsSuccess; + } + + public Result RevParse(string gitRef) + { + return this.InvokeGitAgainstDotGitFolder("rev-parse " + gitRef); + } + + public void DeleteFromLocalConfig(string settingName) + { + this.InvokeGitAgainstDotGitFolder("config --local --unset-all " + settingName); + } + + public Result SetInLocalConfig(string settingName, string value, bool replaceAll = false) + { + return this.InvokeGitAgainstDotGitFolder(string.Format( + "config --local {0} \"{1}\" \"{2}\"", + replaceAll ? "--replace-all " : string.Empty, + settingName, + value)); + } + + public bool TryGetConfigUrlMatch(string section, string repositoryUrl, out Dictionary configSettings) + { + Result result = this.InvokeGitAgainstDotGitFolder($"config --get-urlmatch {section} {repositoryUrl}"); + if (result.ExitCodeIsFailure) + { + configSettings = null; + return false; + } + + configSettings = GitConfigHelper.ParseKeyValues(result.Output, ' '); + return true; + } + + public bool TryGetAllConfig(bool localOnly, out Dictionary configSettings) + { + configSettings = null; + string localParameter = localOnly ? "--local" : string.Empty; + ConfigResult result = new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --list " + localParameter), "--list"); + + if (result.TryParseAsString(out string output, out string _, string.Empty)) + { + configSettings = GitConfigHelper.ParseKeyValues(output); + return true; + } + + return false; + } + + /// + /// Get the config value give a setting name + /// + /// The name of the config setting + /// + /// If false, will run the call from inside the enlistment if the working dir found, + /// otherwise it will run it from outside the enlistment. + /// + /// The value found for the setting. + public virtual ConfigResult GetFromConfig(string settingName, bool forceOutsideEnlistment = false, PhysicalFileSystem fileSystem = null) + { + string command = string.Format("config {0}", settingName); + fileSystem = fileSystem ?? new PhysicalFileSystem(); + + // This method is called at clone time, so the physical repo may not exist yet. + return + fileSystem.DirectoryExists(this.workingDirectoryRoot) && !forceOutsideEnlistment + ? new ConfigResult(this.InvokeGitAgainstDotGitFolder(command), settingName) + : new ConfigResult(this.InvokeGitOutsideEnlistment(command), settingName); + } + + public ConfigResult GetFromLocalConfig(string settingName) + { + return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local " + settingName), settingName); + } + + /// + /// Safely gets the config value give a setting name + /// + /// The name of the config setting + /// + /// If false, will run the call from inside the enlistment if the working dir found, + /// otherwise it will run it from outside the enlistment. + /// + /// The value found for the config setting. + /// True if the config call was successful, false otherwise. + public bool TryGetFromConfig(string settingName, bool forceOutsideEnlistment, out string value, PhysicalFileSystem fileSystem = null) + { + value = null; + try + { + ConfigResult result = this.GetFromConfig(settingName, forceOutsideEnlistment, fileSystem); + return result.TryParseAsString(out value, out string _); + } + catch + { + } + + return false; + } + + public ConfigResult GetOriginUrl() + { + return new ConfigResult(this.InvokeGitAgainstDotGitFolder("config --local remote.origin.url"), "remote.origin.url"); + } + + public Result DiffTree(string sourceTreeish, string targetTreeish, Action onResult) + { + return this.InvokeGitAgainstDotGitFolder("diff-tree -r -t " + sourceTreeish + " " + targetTreeish, null, onResult); + } + + public Result CreateBranchWithUpstream(string branchToCreate, string upstreamBranch) + { + return this.InvokeGitAgainstDotGitFolder("branch " + branchToCreate + " --track " + upstreamBranch); + } + + public Result ForceCheckout(string target) + { + return this.InvokeGitInWorkingDirectoryRoot("checkout -f " + target, useReadObjectHook: false); + } + + public Result ForceCheckoutAllFiles() + { + return this.InvokeGitInWorkingDirectoryRoot("checkout HEAD -- .", useReadObjectHook: true); + } + + public Result Status(bool allowObjectDownloads, bool useStatusCache, bool showUntracked = false) + { + string command = "status"; + if (!useStatusCache) + { + command += " --no-deserialize"; + } + + if (showUntracked) + { + command += " -uall"; + } + + return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: allowObjectDownloads); + } + + public Result SerializeStatus(bool allowObjectDownloads, string serializePath) + { + // specify ignored=matching and --untracked-files=complete + // so the status cache can answer status commands run by Visual Studio + // or tools with similar requirements. + return this.InvokeGitInWorkingDirectoryRoot( + string.Format("--no-optional-locks status \"--serialize={0}\" --ignored=matching --untracked-files=complete", serializePath), + useReadObjectHook: allowObjectDownloads); + } + + public Result UnpackObjects(Stream packFileStream) + { + return this.InvokeGitAgainstDotGitFolder( + "unpack-objects", + stdin => + { + packFileStream.CopyTo(stdin.BaseStream); + stdin.Write('\n'); + }, + null); + } + + public Result PackObjects(string filenamePrefix, string gitObjectsDirectory, Action packFileStream) + { + string packFilePath = Path.Combine(gitObjectsDirectory, ScalarConstants.DotGit.Objects.Pack.Name, filenamePrefix); + + // Since we don't provide paths we won't be able to complete good deltas + // avoid the unnecessary computation by setting window/depth to 0 + return this.InvokeGitAgainstDotGitFolder( + $"pack-objects {packFilePath} --non-empty --window=0 --depth=0 -q", + packFileStream, + parseStdOutLine: null, + gitObjectsDirectory: gitObjectsDirectory); + } + + /// + /// Write a new commit graph in the specified pack directory. Crawl the given pack- + /// indexes for commits and then close under everything reachable or exists in the + /// previous graph file. + /// + /// This will update the graph-head file to point to the new commit graph and delete + /// any expired graph files that previously existed. + /// + public Result WriteCommitGraph(string objectDir, List packs) + { + string command = "commit-graph write --stdin-packs --split --size-multiple=4 --object-dir \"" + objectDir + "\""; + return this.InvokeGitInWorkingDirectoryRoot( + command, + useReadObjectHook: true, + writeStdIn: writer => + { + foreach (string packIndex in packs) + { + writer.WriteLine(packIndex); + } + + // We need to close stdin or else the process will not terminate. + writer.Close(); + }); + } + + public Result VerifyCommitGraph(string objectDir) + { + string command = "commit-graph verify --shallow --object-dir \"" + objectDir + "\""; + return this.InvokeGitInWorkingDirectoryRoot(command, useReadObjectHook: true); + } + + public Result IndexPack(string packfilePath, string idxOutputPath) + { + return this.InvokeGitAgainstDotGitFolder($"index-pack -o \"{idxOutputPath}\" \"{packfilePath}\""); + } + + /// + /// Write a new multi-pack-index (MIDX) in the specified pack directory. + /// + /// If no new packfiles are found, then this is a no-op. + /// + public Result WriteMultiPackIndex(string objectDir) + { + // We override the config settings so we keep writing the MIDX file even if it is disabled for reads. + return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index write --object-dir=\"" + objectDir + "\""); + } + + public Result VerifyMultiPackIndex(string objectDir) + { + return this.InvokeGitAgainstDotGitFolder("-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"" + objectDir + "\""); + } + + public Result RemoteAdd(string remoteName, string url) + { + return this.InvokeGitAgainstDotGitFolder("remote add " + remoteName + " " + url); + } + + public Result LsTree(string treeish, Action parseStdOutLine, bool recursive, bool showAllTrees = false, bool showDirectories = false) + { + return this.InvokeGitAgainstDotGitFolder( + "ls-tree " + (recursive ? "-r " : string.Empty) + (showAllTrees ? "-t " : string.Empty) + (showDirectories ? "-d " : string.Empty) + treeish, + null, + parseStdOutLine); + } + + public Result LsFiles(Action parseStdOutLine) + { + return this.InvokeGitInWorkingDirectoryRoot( + "ls-files -v", + useReadObjectHook: false, + parseStdOutLine: parseStdOutLine); + } + + public Result UpdateBranchSymbolicRef(string refToUpdate, string targetRef) + { + return this.InvokeGitAgainstDotGitFolder("symbolic-ref " + refToUpdate + " " + targetRef); + } + + public Result UpdateBranchSha(string refToUpdate, string targetSha) + { + // If oldCommitResult doesn't fail, then the branch exists and update-ref will want the old sha + Result oldCommitResult = this.RevParse(refToUpdate); + string oldSha = string.Empty; + if (oldCommitResult.ExitCodeIsSuccess) + { + oldSha = oldCommitResult.Output.TrimEnd('\n'); + } + + return this.InvokeGitAgainstDotGitFolder("update-ref --no-deref " + refToUpdate + " " + targetSha + " " + oldSha); + } + + public Result PrunePacked(string gitObjectDirectory) + { + return this.InvokeGitAgainstDotGitFolder( + "prune-packed -q", + writeStdIn: null, + parseStdOutLine: null, + gitObjectsDirectory: gitObjectDirectory); + } + + public Result MultiPackIndexExpire(string gitObjectDirectory) + { + return this.InvokeGitAgainstDotGitFolder($"multi-pack-index expire --object-dir=\"{gitObjectDirectory}\""); + } + + public Result MultiPackIndexRepack(string gitObjectDirectory, string batchSize) + { + return this.InvokeGitAgainstDotGitFolder($"-c pack.threads=1 multi-pack-index repack --object-dir=\"{gitObjectDirectory}\" --batch-size={batchSize}"); + } + + public Process GetGitProcess(string command, string workingDirectory, string dotGitDirectory, bool useReadObjectHook, bool redirectStandardError, string gitObjectsDirectory) + { + ProcessStartInfo processInfo = new ProcessStartInfo(this.gitBinPath); + processInfo.WorkingDirectory = workingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardInput = true; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = redirectStandardError; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.CreateNoWindow = true; + + processInfo.StandardOutputEncoding = UTF8NoBOM; + processInfo.StandardErrorEncoding = UTF8NoBOM; + + // Removing trace variables that might change git output and break parsing + // List of environment variables: https://git-scm.com/book/gr/v2/Git-Internals-Environment-Variables + foreach (string key in processInfo.EnvironmentVariables.Keys.Cast().ToList()) + { + // If GIT_TRACE is set to a fully-rooted path, then Git sends the trace + // output to that path instead of stdout (GIT_TRACE=1) or stderr (GIT_TRACE=2). + if (key.StartsWith("GIT_TRACE", StringComparison.OrdinalIgnoreCase)) + { + try + { + if (!Path.IsPathRooted(processInfo.EnvironmentVariables[key])) + { + processInfo.EnvironmentVariables.Remove(key); + } + } + catch (ArgumentException) + { + processInfo.EnvironmentVariables.Remove(key); + } + } + } + + processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; + processInfo.EnvironmentVariables["GCM_VALIDATE"] = "0"; + + if (gitObjectsDirectory != null) + { + processInfo.EnvironmentVariables["GIT_OBJECT_DIRECTORY"] = gitObjectsDirectory; + } + + if (!useReadObjectHook) + { + command = "-c " + GitConfigSetting.CoreVirtualizeObjectsName + "=false " + command; + } + + if (!string.IsNullOrEmpty(dotGitDirectory)) + { + command = "--git-dir=\"" + dotGitDirectory + "\" " + command; + } + + processInfo.Arguments = command; + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + + return executingProcess; + } + + protected virtual Result InvokeGitImpl( + string command, + string workingDirectory, + string dotGitDirectory, + bool useReadObjectHook, + Action writeStdIn, + Action parseStdOutLine, + int timeoutMs, + string gitObjectsDirectory = null) + { + if (failedToSetEncoding && writeStdIn != null) + { + return new Result(string.Empty, "Attempting to use to stdin, but the process does not have the right input encodings set.", Result.GenericFailureCode); + } + + try + { + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + using (this.executingProcess = this.GetGitProcess(command, workingDirectory, dotGitDirectory, useReadObjectHook, redirectStandardError: true, gitObjectsDirectory: gitObjectsDirectory)) + { + StringBuilder output = new StringBuilder(); + StringBuilder errors = new StringBuilder(); + + this.executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors.Append(args.Data + "\n"); + } + }; + this.executingProcess.OutputDataReceived += (sender, args) => + { + if (args.Data != null) + { + if (parseStdOutLine != null) + { + parseStdOutLine(args.Data); + } + else + { + output.Append(args.Data + "\n"); + } + } + }; + + lock (this.executionLock) + { + lock (this.processLock) + { + if (this.stopping) + { + return new Result(string.Empty, nameof(GitProcess) + " is stopping", Result.GenericFailureCode); + } + + this.executingProcess.Start(); + + if (this.LowerPriority) + { + try + { + this.executingProcess.PriorityClass = ProcessPriorityClass.BelowNormal; + } + catch (InvalidOperationException) + { + // This is thrown if the process completes before we can set its priority. + } + } + } + + writeStdIn?.Invoke(this.executingProcess.StandardInput); + this.executingProcess.StandardInput.Close(); + + this.executingProcess.BeginOutputReadLine(); + this.executingProcess.BeginErrorReadLine(); + + if (!this.executingProcess.WaitForExit(timeoutMs)) + { + this.executingProcess.Kill(); + + return new Result(output.ToString(), "Operation timed out: " + errors.ToString(), Result.GenericFailureCode); + } + } + + return new Result(output.ToString(), errors.ToString(), this.executingProcess.ExitCode); + } + } + catch (Win32Exception e) + { + return new Result(string.Empty, e.Message, Result.GenericFailureCode); + } + finally + { + this.executingProcess = null; + } + } + + private static string GenerateCredentialVerbCommand(string verb) + { + return $"-c {GitConfigSetting.CredentialUseHttpPath}=true credential {verb}"; + } + + private static string ParseValue(string contents, string prefix) + { + int startIndex = contents.IndexOf(prefix) + prefix.Length; + if (startIndex >= 0 && startIndex < contents.Length) + { + int endIndex = contents.IndexOf('\n', startIndex); + if (endIndex >= 0 && endIndex < contents.Length) + { + return + contents + .Substring(startIndex, endIndex - startIndex) + .Trim('\r'); + } + } + + return null; + } + + /// + /// Invokes git.exe without a working directory set. + /// + /// + /// For commands where git doesn't need to be (or can't be) run from inside an enlistment. + /// eg. 'git init' or 'git version' + /// + private Result InvokeGitOutsideEnlistment(string command) + { + return this.InvokeGitOutsideEnlistment(command, null, null); + } + + private Result InvokeGitOutsideEnlistment( + string command, + Action writeStdIn, + Action parseStdOutLine, + int timeout = -1) + { + return this.InvokeGitImpl( + command, + workingDirectory: Environment.SystemDirectory, + dotGitDirectory: null, + useReadObjectHook: false, + writeStdIn: writeStdIn, + parseStdOutLine: parseStdOutLine, + timeoutMs: timeout); + } + + /// + /// Invokes git.exe from an enlistment's repository root + /// + private Result InvokeGitInWorkingDirectoryRoot( + string command, + bool useReadObjectHook, + Action writeStdIn = null, + Action parseStdOutLine = null) + { + return this.InvokeGitImpl( + command, + workingDirectory: this.workingDirectoryRoot, + dotGitDirectory: null, + useReadObjectHook: useReadObjectHook, + writeStdIn: writeStdIn, + parseStdOutLine: parseStdOutLine, + timeoutMs: -1); + } + + /// + /// Invokes git.exe against an enlistment's .git folder. + /// This method should be used only with git-commands that ignore the working directory + /// + private Result InvokeGitAgainstDotGitFolder(string command) + { + return this.InvokeGitAgainstDotGitFolder(command, null, null); + } + + private Result InvokeGitAgainstDotGitFolder( + string command, + Action writeStdIn, + Action parseStdOutLine, + string gitObjectsDirectory = null) + { + // This git command should not need/use the working directory of the repo. + // Run git.exe in Environment.SystemDirectory to ensure the git.exe process + // does not touch the working directory + return this.InvokeGitImpl( + command, + workingDirectory: Environment.SystemDirectory, + dotGitDirectory: this.dotGitRoot, + useReadObjectHook: false, + writeStdIn: writeStdIn, + parseStdOutLine: parseStdOutLine, + timeoutMs: -1, + gitObjectsDirectory: gitObjectsDirectory); + } + + public class Result + { + public const int SuccessCode = 0; + public const int GenericFailureCode = 1; + + public Result(string stdout, string stderr, int exitCode) + { + this.Output = stdout; + this.Errors = stderr; + this.ExitCode = exitCode; + } + + public string Output { get; } + public string Errors { get; } + public int ExitCode { get; } + + public bool ExitCodeIsSuccess + { + get { return this.ExitCode == Result.SuccessCode; } + } + + public bool ExitCodeIsFailure + { + get { return !this.ExitCodeIsSuccess; } + } + + public bool StderrContainsErrors() + { + if (!string.IsNullOrWhiteSpace(this.Errors)) + { + return !this.Errors + .Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .All(line => line.TrimStart().StartsWith("warning:", StringComparison.OrdinalIgnoreCase)); + } + + return false; + } + } + + public class ConfigResult + { + private readonly Result result; + private readonly string configName; + + public ConfigResult(Result result, string configName) + { + this.result = result; + this.configName = configName; + } + + public bool TryParseAsString(out string value, out string error, string defaultValue = null) + { + value = defaultValue; + error = string.Empty; + + if (this.result.ExitCodeIsFailure && this.result.StderrContainsErrors()) + { + error = "Error while reading '" + this.configName + "' from config: " + this.result.Errors; + return false; + } + + if (this.result.ExitCodeIsSuccess) + { + value = this.result.Output?.TrimEnd('\n'); + } + + return true; + } + + public bool TryParseAsInt(int defaultValue, int minValue, out int value, out string error) + { + value = defaultValue; + error = string.Empty; + + if (!this.TryParseAsString(out string valueString, out error)) + { + return false; + } + + if (string.IsNullOrWhiteSpace(valueString)) + { + // Use default value + return true; + } + + if (!int.TryParse(valueString, out value)) + { + error = string.Format("Misconfigured config setting {0}, could not parse value `{1}` as an int", this.configName, valueString); + return false; + } + + if (value < minValue) + { + error = string.Format("Invalid value {0} for setting {1}, value must be greater than or equal to {2}", value, this.configName, minValue); + return false; + } + + return true; + } + } + } +} diff --git a/Scalar.Common/Git/GitRefs.cs b/Scalar.Common/Git/GitRefs.cs index c5e8c262b8..1f531e9897 100644 --- a/Scalar.Common/Git/GitRefs.cs +++ b/Scalar.Common/Git/GitRefs.cs @@ -1,112 +1,112 @@ -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Scalar.Common.Git -{ - public class GitRefs - { - private const string Head = "HEAD\0"; - private const string Master = "master"; - private const string HeadRefPrefix = "refs/heads/"; - private const string TagsRefPrefix = "refs/tags/"; - private const string OriginRemoteRefPrefix = "refs/remotes/origin/"; - - private Dictionary commitsPerRef; - - private string remoteHeadCommitId = null; - - public GitRefs(IEnumerable infoRefsResponse, string branch) - { - // First 4 characters of a given line are the length of the line and not part of the commit id so - // skip them (https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols) - this.commitsPerRef = - infoRefsResponse - .Where(line => - line.Contains(" " + HeadRefPrefix) || - (line.Contains(" " + TagsRefPrefix) && !line.Contains("^"))) - .Where(line => - branch == null || - line.EndsWith(HeadRefPrefix + branch)) - .Select(line => line.Split(' ')) - .ToDictionary( - line => line[1].Replace(HeadRefPrefix, OriginRemoteRefPrefix), - line => line[0].Substring(4)); - - string lineWithHeadCommit = infoRefsResponse.FirstOrDefault(line => line.Contains(Head)); - - if (lineWithHeadCommit != null) - { - string[] tokens = lineWithHeadCommit.Split(' '); - - if (tokens.Length >= 2 && tokens[1].StartsWith(Head)) - { - // First 8 characters are not part of the commit id so skip them - this.remoteHeadCommitId = tokens[0].Substring(8); - } - } - } - - public int Count - { - get { return this.commitsPerRef.Count; } - } - - public string GetTipCommitId(string branch) - { - return this.commitsPerRef[OriginRemoteRefPrefix + branch]; - } - - public string GetDefaultBranch() - { - IEnumerable> headRefMatches = this.commitsPerRef.Where(reference => - reference.Value == this.remoteHeadCommitId - && reference.Key.StartsWith(OriginRemoteRefPrefix)); - - if (headRefMatches.Count() == 0 || headRefMatches.Count(reference => reference.Key == (OriginRemoteRefPrefix + Master)) > 0) - { - // Default to master if no HEAD or if the commit ID or the dafult branch matches master (this is - // the same behavior as git.exe) - return Master; - } - - // If the HEAD commit ID does not match master grab the first branch that matches - string defaultBranch = headRefMatches.First().Key; - - if (defaultBranch.Length < OriginRemoteRefPrefix.Length) - { - return Master; - } - - return defaultBranch.Substring(OriginRemoteRefPrefix.Length); - } - - /// - /// Checks if the specified branch exists (case sensitive) - /// - public bool HasBranch(string branch) - { - string branchRef = OriginRemoteRefPrefix + branch; - return this.commitsPerRef.ContainsKey(branchRef); - } - - public IEnumerable> GetBranchRefPairs() - { - return this.commitsPerRef.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)); - } - - public string ToPackedRefs() - { - StringBuilder sb = new StringBuilder(); - const string LF = "\n"; - - sb.Append("# pack-refs with: peeled fully-peeled" + LF); - foreach (string refName in this.commitsPerRef.Keys.OrderBy(key => key)) - { - sb.Append(this.commitsPerRef[refName] + " " + refName + LF); - } - - return sb.ToString(); - } - } -} +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Scalar.Common.Git +{ + public class GitRefs + { + private const string Head = "HEAD\0"; + private const string Master = "master"; + private const string HeadRefPrefix = "refs/heads/"; + private const string TagsRefPrefix = "refs/tags/"; + private const string OriginRemoteRefPrefix = "refs/remotes/origin/"; + + private Dictionary commitsPerRef; + + private string remoteHeadCommitId = null; + + public GitRefs(IEnumerable infoRefsResponse, string branch) + { + // First 4 characters of a given line are the length of the line and not part of the commit id so + // skip them (https://git-scm.com/book/en/v2/Git-Internals-Transfer-Protocols) + this.commitsPerRef = + infoRefsResponse + .Where(line => + line.Contains(" " + HeadRefPrefix) || + (line.Contains(" " + TagsRefPrefix) && !line.Contains("^"))) + .Where(line => + branch == null || + line.EndsWith(HeadRefPrefix + branch)) + .Select(line => line.Split(' ')) + .ToDictionary( + line => line[1].Replace(HeadRefPrefix, OriginRemoteRefPrefix), + line => line[0].Substring(4)); + + string lineWithHeadCommit = infoRefsResponse.FirstOrDefault(line => line.Contains(Head)); + + if (lineWithHeadCommit != null) + { + string[] tokens = lineWithHeadCommit.Split(' '); + + if (tokens.Length >= 2 && tokens[1].StartsWith(Head)) + { + // First 8 characters are not part of the commit id so skip them + this.remoteHeadCommitId = tokens[0].Substring(8); + } + } + } + + public int Count + { + get { return this.commitsPerRef.Count; } + } + + public string GetTipCommitId(string branch) + { + return this.commitsPerRef[OriginRemoteRefPrefix + branch]; + } + + public string GetDefaultBranch() + { + IEnumerable> headRefMatches = this.commitsPerRef.Where(reference => + reference.Value == this.remoteHeadCommitId + && reference.Key.StartsWith(OriginRemoteRefPrefix)); + + if (headRefMatches.Count() == 0 || headRefMatches.Count(reference => reference.Key == (OriginRemoteRefPrefix + Master)) > 0) + { + // Default to master if no HEAD or if the commit ID or the dafult branch matches master (this is + // the same behavior as git.exe) + return Master; + } + + // If the HEAD commit ID does not match master grab the first branch that matches + string defaultBranch = headRefMatches.First().Key; + + if (defaultBranch.Length < OriginRemoteRefPrefix.Length) + { + return Master; + } + + return defaultBranch.Substring(OriginRemoteRefPrefix.Length); + } + + /// + /// Checks if the specified branch exists (case sensitive) + /// + public bool HasBranch(string branch) + { + string branchRef = OriginRemoteRefPrefix + branch; + return this.commitsPerRef.ContainsKey(branchRef); + } + + public IEnumerable> GetBranchRefPairs() + { + return this.commitsPerRef.Select(kvp => new KeyValuePair(kvp.Key, kvp.Value)); + } + + public string ToPackedRefs() + { + StringBuilder sb = new StringBuilder(); + const string LF = "\n"; + + sb.Append("# pack-refs with: peeled fully-peeled" + LF); + foreach (string refName in this.commitsPerRef.Keys.OrderBy(key => key)) + { + sb.Append(this.commitsPerRef[refName] + " " + refName + LF); + } + + return sb.ToString(); + } + } +} diff --git a/Scalar.Common/Git/GitRepo.cs b/Scalar.Common/Git/GitRepo.cs index 284c863de2..ce9a912a4d 100644 --- a/Scalar.Common/Git/GitRepo.cs +++ b/Scalar.Common/Git/GitRepo.cs @@ -1,232 +1,232 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.IO.Compression; -using System.Linq; - -namespace Scalar.Common.Git -{ - public class GitRepo : IDisposable - { - private static readonly byte[] LooseBlobHeader = new byte[] { (byte)'b', (byte)'l', (byte)'o', (byte)'b', (byte)' ' }; - - private ITracer tracer; - private PhysicalFileSystem fileSystem; - private LibGit2RepoInvoker libgit2RepoInvoker; - private Enlistment enlistment; - - public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem, Func repoFactory = null) - { - this.tracer = tracer; - this.enlistment = enlistment; - this.fileSystem = fileSystem; - - this.ScalarLock = new ScalarLock(tracer); - - this.libgit2RepoInvoker = new LibGit2RepoInvoker( - tracer, - repoFactory ?? (() => new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot))); - } - - // For Unit Testing - protected GitRepo(ITracer tracer) - { - this.ScalarLock = new ScalarLock(tracer); - } - - private enum LooseBlobState - { - Invalid, - Missing, - Exists, - Corrupt, - Unknown, - } - - public ScalarLock ScalarLock - { - get; - private set; - } - - public void CloseActiveRepo() - { - this.libgit2RepoInvoker?.DisposeSharedRepo(); - } - - public void OpenRepo() - { - this.libgit2RepoInvoker?.InitializeSharedRepo(); - } - - public bool TryGetIsBlob(string sha, out bool isBlob) - { - return this.libgit2RepoInvoker.TryInvoke(repo => repo.IsBlob(sha), out isBlob); - } - - public virtual bool CommitAndRootTreeExists(string commitSha) - { - bool output = false; - this.libgit2RepoInvoker.TryInvoke(repo => repo.CommitAndRootTreeExists(commitSha), out output); - return output; - } - - public virtual bool ObjectExists(string blobSha) - { - bool output = false; - this.libgit2RepoInvoker.TryInvoke(repo => repo.ObjectExists(blobSha), out output); - return output; - } - - /// - /// Try to find the size of a given blob by SHA1 hash. - /// - /// Returns true iff the blob exists as a loose object. - /// - public virtual bool TryGetBlobLength(string blobSha, out long size) - { - return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists; - } - - public void Dispose() - { - if (this.libgit2RepoInvoker != null) - { - this.libgit2RepoInvoker.Dispose(); - this.libgit2RepoInvoker = null; - } - } - - private static bool ReadLooseObjectHeader(Stream input, out long size) - { - size = 0; - - byte[] buffer = new byte[5]; - input.Read(buffer, 0, buffer.Length); - if (!Enumerable.SequenceEqual(buffer, LooseBlobHeader)) - { - return false; - } - - while (true) - { - int v = input.ReadByte(); - if (v == -1) - { - return false; - } - - if (v == '\0') - { - break; - } - - size = (size * 10) + (v - '0'); - } - - return true; - } - - private LooseBlobState GetLooseBlobStateAtPath(string blobPath, Action writeAction, out long size) - { - bool corruptLooseObject = false; - try - { - if (this.fileSystem.FileExists(blobPath)) - { - using (Stream file = this.fileSystem.OpenFileStream(blobPath, FileMode.Open, FileAccess.Read, FileShare.Read, callFlushFileBuffers: false)) - { - // The DeflateStream header starts 2 bytes into the gzip header, but they are otherwise compatible - file.Position = 2; - using (DeflateStream deflate = new DeflateStream(file, CompressionMode.Decompress)) - { - if (!ReadLooseObjectHeader(deflate, out size)) - { - corruptLooseObject = true; - return LooseBlobState.Corrupt; - } - - writeAction?.Invoke(deflate, size); - return LooseBlobState.Exists; - } - } - } - - size = -1; - return LooseBlobState.Missing; - } - catch (InvalidDataException ex) - { - corruptLooseObject = true; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("blobPath", blobPath); - metadata.Add("Exception", ex.ToString()); - this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob (InvalidDataException)", Keywords.Telemetry); - - size = -1; - return LooseBlobState.Corrupt; - } - catch (IOException ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("blobPath", blobPath); - metadata.Add("Exception", ex.ToString()); - this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob from disk", Keywords.Telemetry); - - size = -1; - return LooseBlobState.Unknown; - } - finally - { - if (corruptLooseObject) - { - string corruptBlobsFolderPath = Path.Combine(this.enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.CorruptObjectsName); - string corruptBlobPath = Path.Combine(corruptBlobsFolderPath, Path.GetRandomFileName()); - - EventMetadata metadata = new EventMetadata(); - metadata.Add("blobPath", blobPath); - metadata.Add("corruptBlobPath", corruptBlobPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.GetLooseBlobStateAtPath) + ": Renaming corrupt loose object"); - this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObject", metadata); - - try - { - this.fileSystem.CreateDirectory(corruptBlobsFolderPath); - this.fileSystem.MoveFile(blobPath, corruptBlobPath); - } - catch (Exception e) - { - metadata = new EventMetadata(); - metadata.Add("blobPath", blobPath); - metadata.Add("blobBackupPath", corruptBlobPath); - metadata.Add("Exception", e.ToString()); - metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetLooseBlobStateAtPath) + ": Failed to rename corrupt loose object"); - this.tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObjectFailed", metadata, Keywords.Telemetry); - } - } - } - } - - private LooseBlobState GetLooseBlobState(string blobSha, Action writeAction, out long size) - { - string blobPath = Path.Combine( - this.enlistment.GitObjectsRoot, - blobSha.Substring(0, 2), - blobSha.Substring(2)); - - LooseBlobState state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); - if (state == LooseBlobState.Missing) - { - blobPath = Path.Combine( - this.enlistment.LocalObjectsRoot, - blobSha.Substring(0, 2), - blobSha.Substring(2)); - state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); - } - - return state; - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace Scalar.Common.Git +{ + public class GitRepo : IDisposable + { + private static readonly byte[] LooseBlobHeader = new byte[] { (byte)'b', (byte)'l', (byte)'o', (byte)'b', (byte)' ' }; + + private ITracer tracer; + private PhysicalFileSystem fileSystem; + private LibGit2RepoInvoker libgit2RepoInvoker; + private Enlistment enlistment; + + public GitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem, Func repoFactory = null) + { + this.tracer = tracer; + this.enlistment = enlistment; + this.fileSystem = fileSystem; + + this.ScalarLock = new ScalarLock(tracer); + + this.libgit2RepoInvoker = new LibGit2RepoInvoker( + tracer, + repoFactory ?? (() => new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot))); + } + + // For Unit Testing + protected GitRepo(ITracer tracer) + { + this.ScalarLock = new ScalarLock(tracer); + } + + private enum LooseBlobState + { + Invalid, + Missing, + Exists, + Corrupt, + Unknown, + } + + public ScalarLock ScalarLock + { + get; + private set; + } + + public void CloseActiveRepo() + { + this.libgit2RepoInvoker?.DisposeSharedRepo(); + } + + public void OpenRepo() + { + this.libgit2RepoInvoker?.InitializeSharedRepo(); + } + + public bool TryGetIsBlob(string sha, out bool isBlob) + { + return this.libgit2RepoInvoker.TryInvoke(repo => repo.IsBlob(sha), out isBlob); + } + + public virtual bool CommitAndRootTreeExists(string commitSha) + { + bool output = false; + this.libgit2RepoInvoker.TryInvoke(repo => repo.CommitAndRootTreeExists(commitSha), out output); + return output; + } + + public virtual bool ObjectExists(string blobSha) + { + bool output = false; + this.libgit2RepoInvoker.TryInvoke(repo => repo.ObjectExists(blobSha), out output); + return output; + } + + /// + /// Try to find the size of a given blob by SHA1 hash. + /// + /// Returns true iff the blob exists as a loose object. + /// + public virtual bool TryGetBlobLength(string blobSha, out long size) + { + return this.GetLooseBlobState(blobSha, null, out size) == LooseBlobState.Exists; + } + + public void Dispose() + { + if (this.libgit2RepoInvoker != null) + { + this.libgit2RepoInvoker.Dispose(); + this.libgit2RepoInvoker = null; + } + } + + private static bool ReadLooseObjectHeader(Stream input, out long size) + { + size = 0; + + byte[] buffer = new byte[5]; + input.Read(buffer, 0, buffer.Length); + if (!Enumerable.SequenceEqual(buffer, LooseBlobHeader)) + { + return false; + } + + while (true) + { + int v = input.ReadByte(); + if (v == -1) + { + return false; + } + + if (v == '\0') + { + break; + } + + size = (size * 10) + (v - '0'); + } + + return true; + } + + private LooseBlobState GetLooseBlobStateAtPath(string blobPath, Action writeAction, out long size) + { + bool corruptLooseObject = false; + try + { + if (this.fileSystem.FileExists(blobPath)) + { + using (Stream file = this.fileSystem.OpenFileStream(blobPath, FileMode.Open, FileAccess.Read, FileShare.Read, callFlushFileBuffers: false)) + { + // The DeflateStream header starts 2 bytes into the gzip header, but they are otherwise compatible + file.Position = 2; + using (DeflateStream deflate = new DeflateStream(file, CompressionMode.Decompress)) + { + if (!ReadLooseObjectHeader(deflate, out size)) + { + corruptLooseObject = true; + return LooseBlobState.Corrupt; + } + + writeAction?.Invoke(deflate, size); + return LooseBlobState.Exists; + } + } + } + + size = -1; + return LooseBlobState.Missing; + } + catch (InvalidDataException ex) + { + corruptLooseObject = true; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("blobPath", blobPath); + metadata.Add("Exception", ex.ToString()); + this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob (InvalidDataException)", Keywords.Telemetry); + + size = -1; + return LooseBlobState.Corrupt; + } + catch (IOException ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("blobPath", blobPath); + metadata.Add("Exception", ex.ToString()); + this.tracer.RelatedWarning(metadata, nameof(this.GetLooseBlobStateAtPath) + ": Failed to stream blob from disk", Keywords.Telemetry); + + size = -1; + return LooseBlobState.Unknown; + } + finally + { + if (corruptLooseObject) + { + string corruptBlobsFolderPath = Path.Combine(this.enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.CorruptObjectsName); + string corruptBlobPath = Path.Combine(corruptBlobsFolderPath, Path.GetRandomFileName()); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("blobPath", blobPath); + metadata.Add("corruptBlobPath", corruptBlobPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.GetLooseBlobStateAtPath) + ": Renaming corrupt loose object"); + this.tracer.RelatedEvent(EventLevel.Informational, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObject", metadata); + + try + { + this.fileSystem.CreateDirectory(corruptBlobsFolderPath); + this.fileSystem.MoveFile(blobPath, corruptBlobPath); + } + catch (Exception e) + { + metadata = new EventMetadata(); + metadata.Add("blobPath", blobPath); + metadata.Add("blobBackupPath", corruptBlobPath); + metadata.Add("Exception", e.ToString()); + metadata.Add(TracingConstants.MessageKey.WarningMessage, nameof(this.GetLooseBlobStateAtPath) + ": Failed to rename corrupt loose object"); + this.tracer.RelatedEvent(EventLevel.Warning, nameof(this.GetLooseBlobStateAtPath) + "_RenameCorruptObjectFailed", metadata, Keywords.Telemetry); + } + } + } + } + + private LooseBlobState GetLooseBlobState(string blobSha, Action writeAction, out long size) + { + string blobPath = Path.Combine( + this.enlistment.GitObjectsRoot, + blobSha.Substring(0, 2), + blobSha.Substring(2)); + + LooseBlobState state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); + if (state == LooseBlobState.Missing) + { + blobPath = Path.Combine( + this.enlistment.LocalObjectsRoot, + blobSha.Substring(0, 2), + blobSha.Substring(2)); + state = this.GetLooseBlobStateAtPath(blobPath, writeAction, out size); + } + + return state; + } + } +} diff --git a/Scalar.Common/Git/GitSsl.cs b/Scalar.Common/Git/GitSsl.cs index b5904a5bf9..1c448643e1 100644 --- a/Scalar.Common/Git/GitSsl.cs +++ b/Scalar.Common/Git/GitSsl.cs @@ -1,237 +1,237 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; -using System.Text.RegularExpressions; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using Scalar.Common.X509Certificates; - -namespace Scalar.Common.Git -{ - public class GitSsl - { - private readonly string certificatePathOrSubjectCommonName; - private readonly bool isCertificatePasswordProtected; - private readonly Func createCertificateStore; - private readonly CertificateVerifier certificateVerifier; - private readonly PhysicalFileSystem fileSystem; - - public GitSsl( - IDictionary configSettings, - Func createCertificateStore = null, - CertificateVerifier certificateVerifier = null, - PhysicalFileSystem fileSystem = null) : this(createCertificateStore, certificateVerifier, fileSystem) - { - if (configSettings != null) - { - if (configSettings.TryGetValue(GitConfigSetting.HttpSslCert, out GitConfigSetting sslCerts)) - { - this.certificatePathOrSubjectCommonName = sslCerts.Values.Last(); - } - - this.isCertificatePasswordProtected = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslCertPasswordProtected, this.isCertificatePasswordProtected); - - this.ShouldVerify = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslVerify, this.ShouldVerify); - } - } - - private GitSsl(Func createCertificateStore, CertificateVerifier certificateVerifier, PhysicalFileSystem fileSystem) - { - this.fileSystem = fileSystem ?? new PhysicalFileSystem(); - - this.createCertificateStore = createCertificateStore ?? (() => new SystemCertificateStore()); - - this.certificateVerifier = certificateVerifier ?? new CertificateVerifier(); - - this.certificatePathOrSubjectCommonName = null; - - this.isCertificatePasswordProtected = false; - - // True by default, both to have good default security settings and to match git behavior. - // https://git-scm.com/docs/git-config#git-config-httpsslVerify - this.ShouldVerify = true; - } - - /// - /// Gets a value indicating whether SSL certificates being loaded should be verified. Also used to determine, whether client should verify server SSL certificate. True by default. - /// - /// true if should verify SSL certificates; otherwise, false. - public bool ShouldVerify { get; } - - public X509Certificate2 GetCertificate(ITracer tracer, GitProcess gitProcess) - { - if (string.IsNullOrEmpty(this.certificatePathOrSubjectCommonName)) - { - return null; - } - - EventMetadata metadata = new EventMetadata - { - { "CertificatePathOrSubjectCommonName", this.certificatePathOrSubjectCommonName }, - { "IsCertificatePasswordProtected", this.isCertificatePasswordProtected }, - { "ShouldVerify", this.ShouldVerify } - }; - - X509Certificate2 result = - this.GetCertificateFromFile(tracer, metadata, gitProcess) ?? - this.GetCertificateFromStore(tracer, metadata); - - if (result == null) - { - tracer.RelatedError(metadata, $"Certificate {this.certificatePathOrSubjectCommonName} not found"); - } - - return result; - } - - private static bool SetBoolSettingOrThrow(IDictionary configSettings, string settingName, bool currentValue) - { - if (configSettings.TryGetValue(settingName, out GitConfigSetting settingValues)) - { - try - { - return bool.Parse(settingValues.Values.Last()); - } - catch (FormatException) - { - throw new InvalidRepoException($"{settingName} git setting did not have a bool-parsable value. Found: {string.Join(" ", settingValues.Values)}"); - } - } - - return currentValue; - } - - private static void LogWithAppropriateLevel(ITracer tracer, EventMetadata metadata, IEnumerable certificates, string logMessage) - { - int numberOfCertificates = certificates.Count(); - - switch (numberOfCertificates) - { - case 0: - tracer.RelatedError(metadata, logMessage); - break; - case 1: - tracer.RelatedInfo(metadata, logMessage); - break; - default: - tracer.RelatedWarning(metadata, logMessage); - break; - } - } - - private static string GetSubjectNameLineForLogging(IEnumerable certificates) - { - return string.Join( - Environment.NewLine, - certificates.Select(x => x.Subject)); - } - - private string GetCertificatePassword(ITracer tracer, GitProcess git) - { - if (git.TryGetCertificatePassword(tracer, this.certificatePathOrSubjectCommonName, out string password, out string error)) - { - return password; - } - - return null; - } - - private X509Certificate2 GetCertificateFromFile(ITracer tracer, EventMetadata metadata, GitProcess gitProcess) - { - string certificatePassword = null; - if (this.isCertificatePasswordProtected) - { - certificatePassword = this.GetCertificatePassword(tracer, gitProcess); - - if (string.IsNullOrEmpty(certificatePassword)) - { - tracer.RelatedWarning( - metadata, - "Git config indicates, that certificate is password protected, but retrieved password was null or empty!"); - } - - metadata.Add("isPasswordSpecified", string.IsNullOrEmpty(certificatePassword)); - } - - if (this.fileSystem.FileExists(this.certificatePathOrSubjectCommonName)) - { - try - { - byte[] certificateContent = this.fileSystem.ReadAllBytes(this.certificatePathOrSubjectCommonName); - X509Certificate2 cert = new X509Certificate2(certificateContent, certificatePassword); - if (this.ShouldVerify && cert != null && !this.certificateVerifier.Verify(cert)) - { - tracer.RelatedWarning(metadata, "Certficate was found, but is invalid."); - return null; - } - - return cert; - } - catch (CryptographicException cryptEx) - { - metadata.Add("Exception", cryptEx.ToString()); - tracer.RelatedError(metadata, "Error, while loading certificate from disk"); - return null; - } - } - - return null; - } - - private X509Certificate2 GetCertificateFromStore(ITracer tracer, EventMetadata metadata) - { - try - { - using (SystemCertificateStore certificateStore = this.createCertificateStore()) - { - X509Certificate2Collection findResults = certificateStore.Find( - X509FindType.FindBySubjectName, - this.certificatePathOrSubjectCommonName, - this.ShouldVerify); - - if (findResults?.Count > 0) - { - LogWithAppropriateLevel( - tracer, - metadata, - findResults.OfType(), - string.Format( - "Found {0} certificates by provided name. Matching DNs: {1}", - findResults.Count, - GetSubjectNameLineForLogging(findResults.OfType()))); - - X509Certificate2[] certsWithMatchingCns = findResults - .OfType() - .Where(x => x.HasPrivateKey && Regex.IsMatch(x.Subject, string.Format("(^|,\\s?)CN={0}(,|$)", this.certificatePathOrSubjectCommonName))) // We only want certificates, that have private keys, as we need them. We also want a complete CN match - .OrderByDescending(x => this.certificateVerifier.Verify(x)) // Ordering by validity in a descending order will bring valid certificates to the beginning - .ThenBy(x => x.NotBefore) // We take the one, that was issued earliest, first - .ThenByDescending(x => x.NotAfter) // We then take the one, that is valid for the longest period - .ToArray(); - - LogWithAppropriateLevel( - tracer, - metadata, - certsWithMatchingCns, - string.Format( - "Found {0} certificates with a private key and an exact CN match. DNs (sorted by priority, will take first): {1}", - certsWithMatchingCns.Length, - GetSubjectNameLineForLogging(certsWithMatchingCns))); - - return certsWithMatchingCns.FirstOrDefault(); - } - } - } - catch (CryptographicException cryptEx) - { - metadata.Add("Exception", cryptEx.ToString()); - tracer.RelatedError(metadata, "Error, while searching for certificate in store"); - return null; - } - - return null; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using Scalar.Common.X509Certificates; + +namespace Scalar.Common.Git +{ + public class GitSsl + { + private readonly string certificatePathOrSubjectCommonName; + private readonly bool isCertificatePasswordProtected; + private readonly Func createCertificateStore; + private readonly CertificateVerifier certificateVerifier; + private readonly PhysicalFileSystem fileSystem; + + public GitSsl( + IDictionary configSettings, + Func createCertificateStore = null, + CertificateVerifier certificateVerifier = null, + PhysicalFileSystem fileSystem = null) : this(createCertificateStore, certificateVerifier, fileSystem) + { + if (configSettings != null) + { + if (configSettings.TryGetValue(GitConfigSetting.HttpSslCert, out GitConfigSetting sslCerts)) + { + this.certificatePathOrSubjectCommonName = sslCerts.Values.Last(); + } + + this.isCertificatePasswordProtected = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslCertPasswordProtected, this.isCertificatePasswordProtected); + + this.ShouldVerify = SetBoolSettingOrThrow(configSettings, GitConfigSetting.HttpSslVerify, this.ShouldVerify); + } + } + + private GitSsl(Func createCertificateStore, CertificateVerifier certificateVerifier, PhysicalFileSystem fileSystem) + { + this.fileSystem = fileSystem ?? new PhysicalFileSystem(); + + this.createCertificateStore = createCertificateStore ?? (() => new SystemCertificateStore()); + + this.certificateVerifier = certificateVerifier ?? new CertificateVerifier(); + + this.certificatePathOrSubjectCommonName = null; + + this.isCertificatePasswordProtected = false; + + // True by default, both to have good default security settings and to match git behavior. + // https://git-scm.com/docs/git-config#git-config-httpsslVerify + this.ShouldVerify = true; + } + + /// + /// Gets a value indicating whether SSL certificates being loaded should be verified. Also used to determine, whether client should verify server SSL certificate. True by default. + /// + /// true if should verify SSL certificates; otherwise, false. + public bool ShouldVerify { get; } + + public X509Certificate2 GetCertificate(ITracer tracer, GitProcess gitProcess) + { + if (string.IsNullOrEmpty(this.certificatePathOrSubjectCommonName)) + { + return null; + } + + EventMetadata metadata = new EventMetadata + { + { "CertificatePathOrSubjectCommonName", this.certificatePathOrSubjectCommonName }, + { "IsCertificatePasswordProtected", this.isCertificatePasswordProtected }, + { "ShouldVerify", this.ShouldVerify } + }; + + X509Certificate2 result = + this.GetCertificateFromFile(tracer, metadata, gitProcess) ?? + this.GetCertificateFromStore(tracer, metadata); + + if (result == null) + { + tracer.RelatedError(metadata, $"Certificate {this.certificatePathOrSubjectCommonName} not found"); + } + + return result; + } + + private static bool SetBoolSettingOrThrow(IDictionary configSettings, string settingName, bool currentValue) + { + if (configSettings.TryGetValue(settingName, out GitConfigSetting settingValues)) + { + try + { + return bool.Parse(settingValues.Values.Last()); + } + catch (FormatException) + { + throw new InvalidRepoException($"{settingName} git setting did not have a bool-parsable value. Found: {string.Join(" ", settingValues.Values)}"); + } + } + + return currentValue; + } + + private static void LogWithAppropriateLevel(ITracer tracer, EventMetadata metadata, IEnumerable certificates, string logMessage) + { + int numberOfCertificates = certificates.Count(); + + switch (numberOfCertificates) + { + case 0: + tracer.RelatedError(metadata, logMessage); + break; + case 1: + tracer.RelatedInfo(metadata, logMessage); + break; + default: + tracer.RelatedWarning(metadata, logMessage); + break; + } + } + + private static string GetSubjectNameLineForLogging(IEnumerable certificates) + { + return string.Join( + Environment.NewLine, + certificates.Select(x => x.Subject)); + } + + private string GetCertificatePassword(ITracer tracer, GitProcess git) + { + if (git.TryGetCertificatePassword(tracer, this.certificatePathOrSubjectCommonName, out string password, out string error)) + { + return password; + } + + return null; + } + + private X509Certificate2 GetCertificateFromFile(ITracer tracer, EventMetadata metadata, GitProcess gitProcess) + { + string certificatePassword = null; + if (this.isCertificatePasswordProtected) + { + certificatePassword = this.GetCertificatePassword(tracer, gitProcess); + + if (string.IsNullOrEmpty(certificatePassword)) + { + tracer.RelatedWarning( + metadata, + "Git config indicates, that certificate is password protected, but retrieved password was null or empty!"); + } + + metadata.Add("isPasswordSpecified", string.IsNullOrEmpty(certificatePassword)); + } + + if (this.fileSystem.FileExists(this.certificatePathOrSubjectCommonName)) + { + try + { + byte[] certificateContent = this.fileSystem.ReadAllBytes(this.certificatePathOrSubjectCommonName); + X509Certificate2 cert = new X509Certificate2(certificateContent, certificatePassword); + if (this.ShouldVerify && cert != null && !this.certificateVerifier.Verify(cert)) + { + tracer.RelatedWarning(metadata, "Certficate was found, but is invalid."); + return null; + } + + return cert; + } + catch (CryptographicException cryptEx) + { + metadata.Add("Exception", cryptEx.ToString()); + tracer.RelatedError(metadata, "Error, while loading certificate from disk"); + return null; + } + } + + return null; + } + + private X509Certificate2 GetCertificateFromStore(ITracer tracer, EventMetadata metadata) + { + try + { + using (SystemCertificateStore certificateStore = this.createCertificateStore()) + { + X509Certificate2Collection findResults = certificateStore.Find( + X509FindType.FindBySubjectName, + this.certificatePathOrSubjectCommonName, + this.ShouldVerify); + + if (findResults?.Count > 0) + { + LogWithAppropriateLevel( + tracer, + metadata, + findResults.OfType(), + string.Format( + "Found {0} certificates by provided name. Matching DNs: {1}", + findResults.Count, + GetSubjectNameLineForLogging(findResults.OfType()))); + + X509Certificate2[] certsWithMatchingCns = findResults + .OfType() + .Where(x => x.HasPrivateKey && Regex.IsMatch(x.Subject, string.Format("(^|,\\s?)CN={0}(,|$)", this.certificatePathOrSubjectCommonName))) // We only want certificates, that have private keys, as we need them. We also want a complete CN match + .OrderByDescending(x => this.certificateVerifier.Verify(x)) // Ordering by validity in a descending order will bring valid certificates to the beginning + .ThenBy(x => x.NotBefore) // We take the one, that was issued earliest, first + .ThenByDescending(x => x.NotAfter) // We then take the one, that is valid for the longest period + .ToArray(); + + LogWithAppropriateLevel( + tracer, + metadata, + certsWithMatchingCns, + string.Format( + "Found {0} certificates with a private key and an exact CN match. DNs (sorted by priority, will take first): {1}", + certsWithMatchingCns.Length, + GetSubjectNameLineForLogging(certsWithMatchingCns))); + + return certsWithMatchingCns.FirstOrDefault(); + } + } + } + catch (CryptographicException cryptEx) + { + metadata.Add("Exception", cryptEx.ToString()); + tracer.RelatedError(metadata, "Error, while searching for certificate in store"); + return null; + } + + return null; + } + } +} diff --git a/Scalar.Common/Git/GitVersion.cs b/Scalar.Common/Git/GitVersion.cs index 5c8f39f2d5..f357eed676 100644 --- a/Scalar.Common/Git/GitVersion.cs +++ b/Scalar.Common/Git/GitVersion.cs @@ -1,177 +1,177 @@ -using System; - -namespace Scalar.Common.Git -{ - public class GitVersion - { - public GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) - { - this.Major = major; - this.Minor = minor; - this.Build = build; - this.Platform = platform; - this.Revision = revision; - this.MinorRevision = minorRevision; - } - - public int Major { get; private set; } - public int Minor { get; private set; } - public string Platform { get; private set; } - public int Build { get; private set; } - public int Revision { get; private set; } - public int MinorRevision { get; private set; } - - public static bool TryParseGitVersionCommandResult(string input, out GitVersion version) - { - // git version output is of the form - // git version 2.17.0.scalar.1.preview.3 - - const string GitVersionExpectedPrefix = "git version "; - - if (input.StartsWith(GitVersionExpectedPrefix)) - { - input = input.Substring(GitVersionExpectedPrefix.Length); - } - - return TryParseVersion(input, out version); - } - - public static bool TryParseInstallerName(string input, string installerExtension, out GitVersion version) - { - // Installer name is of the form - // Git-2.14.1.scalar.1.1.gb16030b-64-bit.exe - - version = null; - - if (!input.StartsWith("Git-", StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - if (!input.EndsWith("-64-bit" + installerExtension, StringComparison.InvariantCultureIgnoreCase)) - { - return false; - } - - return TryParseVersion(input.Substring(4, input.Length - 15), out version); - } - - public static bool TryParseVersion(string input, out GitVersion version) - { - version = null; - int major, minor, build, revision, minorRevision; - - if (string.IsNullOrWhiteSpace(input)) - { - return false; - } - - string[] parsedComponents = input.Split(new char[] { '.' }); - int parsedComponentsLength = parsedComponents.Length; - if (parsedComponentsLength < 5) - { - return false; - } - - if (!TryParseComponent(parsedComponents[0], out major)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[1], out minor)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[2], out build)) - { - return false; - } - - if (!TryParseComponent(parsedComponents[4], out revision)) - { - return false; - } - - if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) - { - minorRevision = 0; - } - - string platform = parsedComponents[3]; - - version = new GitVersion(major, minor, build, platform, revision, minorRevision); - return true; - } - - public bool IsEqualTo(GitVersion other) - { - if (this.Platform != other.Platform) - { - return false; - } - - return this.CompareVersionNumbers(other) == 0; - } - - public bool IsLessThan(GitVersion other) - { - return this.CompareVersionNumbers(other) < 0; - } - - public override string ToString() - { - return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); - } - - private static bool TryParseComponent(string component, out int parsedComponent) - { - if (!int.TryParse(component, out parsedComponent)) - { - return false; - } - - if (parsedComponent < 0) - { - return false; - } - - return true; - } - - private int CompareVersionNumbers(GitVersion other) - { - if (other == null) - { - return -1; - } - - if (this.Major != other.Major) - { - return this.Major.CompareTo(other.Major); - } - - if (this.Minor != other.Minor) - { - return this.Minor.CompareTo(other.Minor); - } - - if (this.Build != other.Build) - { - return this.Build.CompareTo(other.Build); - } - - if (this.Revision != other.Revision) - { - return this.Revision.CompareTo(other.Revision); - } - - if (this.MinorRevision != other.MinorRevision) - { - return this.MinorRevision.CompareTo(other.MinorRevision); - } - - return 0; - } - } -} +using System; + +namespace Scalar.Common.Git +{ + public class GitVersion + { + public GitVersion(int major, int minor, int build, string platform, int revision, int minorRevision) + { + this.Major = major; + this.Minor = minor; + this.Build = build; + this.Platform = platform; + this.Revision = revision; + this.MinorRevision = minorRevision; + } + + public int Major { get; private set; } + public int Minor { get; private set; } + public string Platform { get; private set; } + public int Build { get; private set; } + public int Revision { get; private set; } + public int MinorRevision { get; private set; } + + public static bool TryParseGitVersionCommandResult(string input, out GitVersion version) + { + // git version output is of the form + // git version 2.17.0.scalar.1.preview.3 + + const string GitVersionExpectedPrefix = "git version "; + + if (input.StartsWith(GitVersionExpectedPrefix)) + { + input = input.Substring(GitVersionExpectedPrefix.Length); + } + + return TryParseVersion(input, out version); + } + + public static bool TryParseInstallerName(string input, string installerExtension, out GitVersion version) + { + // Installer name is of the form + // Git-2.14.1.scalar.1.1.gb16030b-64-bit.exe + + version = null; + + if (!input.StartsWith("Git-", StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + if (!input.EndsWith("-64-bit" + installerExtension, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + return TryParseVersion(input.Substring(4, input.Length - 15), out version); + } + + public static bool TryParseVersion(string input, out GitVersion version) + { + version = null; + int major, minor, build, revision, minorRevision; + + if (string.IsNullOrWhiteSpace(input)) + { + return false; + } + + string[] parsedComponents = input.Split(new char[] { '.' }); + int parsedComponentsLength = parsedComponents.Length; + if (parsedComponentsLength < 5) + { + return false; + } + + if (!TryParseComponent(parsedComponents[0], out major)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[1], out minor)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[2], out build)) + { + return false; + } + + if (!TryParseComponent(parsedComponents[4], out revision)) + { + return false; + } + + if (parsedComponentsLength < 6 || !TryParseComponent(parsedComponents[5], out minorRevision)) + { + minorRevision = 0; + } + + string platform = parsedComponents[3]; + + version = new GitVersion(major, minor, build, platform, revision, minorRevision); + return true; + } + + public bool IsEqualTo(GitVersion other) + { + if (this.Platform != other.Platform) + { + return false; + } + + return this.CompareVersionNumbers(other) == 0; + } + + public bool IsLessThan(GitVersion other) + { + return this.CompareVersionNumbers(other) < 0; + } + + public override string ToString() + { + return string.Format("{0}.{1}.{2}.{3}.{4}.{5}", this.Major, this.Minor, this.Build, this.Platform, this.Revision, this.MinorRevision); + } + + private static bool TryParseComponent(string component, out int parsedComponent) + { + if (!int.TryParse(component, out parsedComponent)) + { + return false; + } + + if (parsedComponent < 0) + { + return false; + } + + return true; + } + + private int CompareVersionNumbers(GitVersion other) + { + if (other == null) + { + return -1; + } + + if (this.Major != other.Major) + { + return this.Major.CompareTo(other.Major); + } + + if (this.Minor != other.Minor) + { + return this.Minor.CompareTo(other.Minor); + } + + if (this.Build != other.Build) + { + return this.Build.CompareTo(other.Build); + } + + if (this.Revision != other.Revision) + { + return this.Revision.CompareTo(other.Revision); + } + + if (this.MinorRevision != other.MinorRevision) + { + return this.MinorRevision.CompareTo(other.MinorRevision); + } + + return 0; + } + } +} diff --git a/Scalar.Common/Git/HashingStream.cs b/Scalar.Common/Git/HashingStream.cs index 6243e86d7a..8c9060f802 100644 --- a/Scalar.Common/Git/HashingStream.cs +++ b/Scalar.Common/Git/HashingStream.cs @@ -1,134 +1,134 @@ -using System; -using System.IO; -using System.Security.Cryptography; - -namespace Scalar.Common.Git -{ - public class HashingStream : Stream - { - private readonly HashAlgorithm hash; - - private Stream stream; - - private bool closed; - private byte[] hashResult; - - public HashingStream(Stream stream) - { - this.stream = stream; - - this.hash = SHA1.Create(); - this.hashResult = null; - this.hash.Initialize(); - this.closed = false; - } - - public override bool CanSeek - { - get { return false; } - } - - public byte[] Hash - { - get - { - this.FinishHash(); - return this.hashResult; - } - } - - public override bool CanRead - { - get { return true; } - } - - public override long Length - { - get { return this.stream.Length; } - } - - public override long Position - { - get { return this.stream.Position; } - set { throw new NotImplementedException(); } - } - - public override bool CanWrite - { - get { return false; } - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotSupportedException(); - } - - public override void Close() - { - if (!this.closed) - { - this.FinishHash(); - - this.closed = true; - - if (this.stream != null) - { - this.stream.Close(); - } - } - - base.Close(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int bytesRead = this.stream.Read(buffer, offset, count); - if (bytesRead > 0) - { - this.hash.TransformBlock(buffer, offset, bytesRead, null, 0); - } - - return bytesRead; - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - protected override void Dispose(bool disposing) - { - if (this.hash != null) - { - this.hash.Dispose(); - } - - if (this.stream != null) - { - this.stream.Dispose(); - this.stream = null; - } - - base.Dispose(disposing); - } - - private void FinishHash() - { - if (this.hashResult == null) - { - this.hash.TransformFinalBlock(new byte[0], 0, 0); - this.hashResult = this.hash.Hash; - } - } - } +using System; +using System.IO; +using System.Security.Cryptography; + +namespace Scalar.Common.Git +{ + public class HashingStream : Stream + { + private readonly HashAlgorithm hash; + + private Stream stream; + + private bool closed; + private byte[] hashResult; + + public HashingStream(Stream stream) + { + this.stream = stream; + + this.hash = SHA1.Create(); + this.hashResult = null; + this.hash.Initialize(); + this.closed = false; + } + + public override bool CanSeek + { + get { return false; } + } + + public byte[] Hash + { + get + { + this.FinishHash(); + return this.hashResult; + } + } + + public override bool CanRead + { + get { return true; } + } + + public override long Length + { + get { return this.stream.Length; } + } + + public override long Position + { + get { return this.stream.Position; } + set { throw new NotImplementedException(); } + } + + public override bool CanWrite + { + get { return false; } + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotImplementedException(); + } + + public override void SetLength(long value) + { + throw new NotSupportedException(); + } + + public override void Close() + { + if (!this.closed) + { + this.FinishHash(); + + this.closed = true; + + if (this.stream != null) + { + this.stream.Close(); + } + } + + base.Close(); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesRead = this.stream.Read(buffer, offset, count); + if (bytesRead > 0) + { + this.hash.TransformBlock(buffer, offset, bytesRead, null, 0); + } + + return bytesRead; + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotImplementedException(); + } + + public override void Flush() + { + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + if (this.hash != null) + { + this.hash.Dispose(); + } + + if (this.stream != null) + { + this.stream.Dispose(); + this.stream = null; + } + + base.Dispose(disposing); + } + + private void FinishHash() + { + if (this.hashResult == null) + { + this.hash.TransformFinalBlock(new byte[0], 0, 0); + this.hashResult = this.hash.Hash; + } + } + } } diff --git a/Scalar.Common/Git/ICredentialStore.cs b/Scalar.Common/Git/ICredentialStore.cs index ff257807ac..58ba2a22aa 100644 --- a/Scalar.Common/Git/ICredentialStore.cs +++ b/Scalar.Common/Git/ICredentialStore.cs @@ -1,13 +1,13 @@ -using Scalar.Common.Tracing; - -namespace Scalar.Common.Git -{ - public interface ICredentialStore - { - bool TryGetCredential(ITracer tracer, string url, out string username, out string password, out string error); - - bool TryStoreCredential(ITracer tracer, string url, string username, string password, out string error); - - bool TryDeleteCredential(ITracer tracer, string url, string username, string password, out string error); - } -} +using Scalar.Common.Tracing; + +namespace Scalar.Common.Git +{ + public interface ICredentialStore + { + bool TryGetCredential(ITracer tracer, string url, out string username, out string password, out string error); + + bool TryStoreCredential(ITracer tracer, string url, string username, string password, out string error); + + bool TryDeleteCredential(ITracer tracer, string url, string username, string password, out string error); + } +} diff --git a/Scalar.Common/Git/IGitInstallation.cs b/Scalar.Common/Git/IGitInstallation.cs index b5a6ac4bb0..5fc4435c50 100644 --- a/Scalar.Common/Git/IGitInstallation.cs +++ b/Scalar.Common/Git/IGitInstallation.cs @@ -1,8 +1,8 @@ -namespace Scalar.Common.Git -{ - public interface IGitInstallation - { - bool GitExists(string gitBinPath); - string GetInstalledGitBinPath(); - } +namespace Scalar.Common.Git +{ + public interface IGitInstallation + { + bool GitExists(string gitBinPath); + string GetInstalledGitBinPath(); + } } diff --git a/Scalar.Common/Git/LibGit2Repo.cs b/Scalar.Common/Git/LibGit2Repo.cs index 14cc5ddbfa..2282a32409 100644 --- a/Scalar.Common/Git/LibGit2Repo.cs +++ b/Scalar.Common/Git/LibGit2Repo.cs @@ -1,219 +1,219 @@ -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.Common.Git -{ - public class LibGit2Repo : IDisposable - { - private bool disposedValue = false; - - public LibGit2Repo(ITracer tracer, string repoPath) - { - this.Tracer = tracer; - - Native.Init(); - - IntPtr repoHandle; - if (Native.Repo.Open(out repoHandle, repoPath) != Native.SuccessCode) - { - string reason = Native.GetLastError(); - string message = "Couldn't open repo at " + repoPath + ": " + reason; - tracer.RelatedError(message); - - Native.Shutdown(); - throw new InvalidDataException(message); - } - - this.RepoHandle = repoHandle; - } - - protected LibGit2Repo() - { - } - - ~LibGit2Repo() - { - this.Dispose(false); - } - - protected ITracer Tracer { get; } - protected IntPtr RepoHandle { get; private set; } - - public bool IsBlob(string sha) - { - IntPtr objHandle; - if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.SuccessCode) - { - return false; - } - - try - { - switch (Native.Object.GetType(objHandle)) - { - case Native.ObjectTypes.Blob: - return true; - - default: - return false; - } - } - finally - { - Native.Object.Free(objHandle); - } - } - - public virtual string GetTreeSha(string commitish) - { - IntPtr objHandle; - if (Native.RevParseSingle(out objHandle, this.RepoHandle, commitish) != Native.SuccessCode) - { - return null; - } - - try - { - switch (Native.Object.GetType(objHandle)) - { - case Native.ObjectTypes.Commit: - GitOid output = Native.IntPtrToGitOid(Native.Commit.GetTreeId(objHandle)); - return output.ToString(); - } - } - finally - { - Native.Object.Free(objHandle); - } - - return null; - } - - public virtual bool CommitAndRootTreeExists(string commitish) - { - string treeSha = this.GetTreeSha(commitish); - if (treeSha == null) - { - return false; - } - - return this.ObjectExists(treeSha.ToString()); - } - - public virtual bool ObjectExists(string sha) - { - IntPtr objHandle; - if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.SuccessCode) - { - return false; - } - - Native.Object.Free(objHandle); - return true; - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!this.disposedValue) - { - Native.Repo.Free(this.RepoHandle); - Native.Shutdown(); - this.disposedValue = true; - } - } - - public static class Native - { - public const uint SuccessCode = 0; - - public const string Git2NativeLibName = "git2"; - - public enum ObjectTypes - { - Commit = 1, - Tree = 2, - Blob = 3, - } - - public static GitOid IntPtrToGitOid(IntPtr oidPtr) - { - return Marshal.PtrToStructure(oidPtr); - } - - [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_init")] - public static extern void Init(); - - [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_shutdown")] - public static extern int Shutdown(); - - [DllImport(Git2NativeLibName, EntryPoint = "git_revparse_single")] - public static extern uint RevParseSingle(out IntPtr objectHandle, IntPtr repoHandle, string oid); - - public static string GetLastError() - { - IntPtr ptr = GetLastGitError(); - if (ptr == IntPtr.Zero) - { - return "Operation was successful"; - } - - return Marshal.PtrToStructure(ptr).Message; - } - - [DllImport(Git2NativeLibName, EntryPoint = "giterr_last")] - private static extern IntPtr GetLastGitError(); - - [StructLayout(LayoutKind.Sequential)] - private struct GitError - { - [MarshalAs(UnmanagedType.LPStr)] - public string Message; - - public int Klass; - } - - public static class Repo - { - [DllImport(Git2NativeLibName, EntryPoint = "git_repository_open")] - public static extern uint Open(out IntPtr repoHandle, string path); - - [DllImport(Git2NativeLibName, EntryPoint = "git_repository_free")] - public static extern void Free(IntPtr repoHandle); - } - - public static class Object - { - [DllImport(Git2NativeLibName, EntryPoint = "git_object_type")] - public static extern ObjectTypes GetType(IntPtr objectHandle); - - [DllImport(Git2NativeLibName, EntryPoint = "git_object_free")] - public static extern void Free(IntPtr objHandle); - } - - public static class Commit - { - /// A handle to an oid owned by LibGit2 - [DllImport(Git2NativeLibName, EntryPoint = "git_commit_tree_id")] - public static extern IntPtr GetTreeId(IntPtr commitHandle); - } - - public static class Blob - { - [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawsize")] - [return: MarshalAs(UnmanagedType.U8)] - public static extern long GetRawSize(IntPtr objectHandle); - - [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")] - public static unsafe extern byte* GetRawContent(IntPtr objectHandle); - } - } - } +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.Common.Git +{ + public class LibGit2Repo : IDisposable + { + private bool disposedValue = false; + + public LibGit2Repo(ITracer tracer, string repoPath) + { + this.Tracer = tracer; + + Native.Init(); + + IntPtr repoHandle; + if (Native.Repo.Open(out repoHandle, repoPath) != Native.SuccessCode) + { + string reason = Native.GetLastError(); + string message = "Couldn't open repo at " + repoPath + ": " + reason; + tracer.RelatedError(message); + + Native.Shutdown(); + throw new InvalidDataException(message); + } + + this.RepoHandle = repoHandle; + } + + protected LibGit2Repo() + { + } + + ~LibGit2Repo() + { + this.Dispose(false); + } + + protected ITracer Tracer { get; } + protected IntPtr RepoHandle { get; private set; } + + public bool IsBlob(string sha) + { + IntPtr objHandle; + if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.SuccessCode) + { + return false; + } + + try + { + switch (Native.Object.GetType(objHandle)) + { + case Native.ObjectTypes.Blob: + return true; + + default: + return false; + } + } + finally + { + Native.Object.Free(objHandle); + } + } + + public virtual string GetTreeSha(string commitish) + { + IntPtr objHandle; + if (Native.RevParseSingle(out objHandle, this.RepoHandle, commitish) != Native.SuccessCode) + { + return null; + } + + try + { + switch (Native.Object.GetType(objHandle)) + { + case Native.ObjectTypes.Commit: + GitOid output = Native.IntPtrToGitOid(Native.Commit.GetTreeId(objHandle)); + return output.ToString(); + } + } + finally + { + Native.Object.Free(objHandle); + } + + return null; + } + + public virtual bool CommitAndRootTreeExists(string commitish) + { + string treeSha = this.GetTreeSha(commitish); + if (treeSha == null) + { + return false; + } + + return this.ObjectExists(treeSha.ToString()); + } + + public virtual bool ObjectExists(string sha) + { + IntPtr objHandle; + if (Native.RevParseSingle(out objHandle, this.RepoHandle, sha) != Native.SuccessCode) + { + return false; + } + + Native.Object.Free(objHandle); + return true; + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + Native.Repo.Free(this.RepoHandle); + Native.Shutdown(); + this.disposedValue = true; + } + } + + public static class Native + { + public const uint SuccessCode = 0; + + public const string Git2NativeLibName = "git2"; + + public enum ObjectTypes + { + Commit = 1, + Tree = 2, + Blob = 3, + } + + public static GitOid IntPtrToGitOid(IntPtr oidPtr) + { + return Marshal.PtrToStructure(oidPtr); + } + + [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_init")] + public static extern void Init(); + + [DllImport(Git2NativeLibName, EntryPoint = "git_libgit2_shutdown")] + public static extern int Shutdown(); + + [DllImport(Git2NativeLibName, EntryPoint = "git_revparse_single")] + public static extern uint RevParseSingle(out IntPtr objectHandle, IntPtr repoHandle, string oid); + + public static string GetLastError() + { + IntPtr ptr = GetLastGitError(); + if (ptr == IntPtr.Zero) + { + return "Operation was successful"; + } + + return Marshal.PtrToStructure(ptr).Message; + } + + [DllImport(Git2NativeLibName, EntryPoint = "giterr_last")] + private static extern IntPtr GetLastGitError(); + + [StructLayout(LayoutKind.Sequential)] + private struct GitError + { + [MarshalAs(UnmanagedType.LPStr)] + public string Message; + + public int Klass; + } + + public static class Repo + { + [DllImport(Git2NativeLibName, EntryPoint = "git_repository_open")] + public static extern uint Open(out IntPtr repoHandle, string path); + + [DllImport(Git2NativeLibName, EntryPoint = "git_repository_free")] + public static extern void Free(IntPtr repoHandle); + } + + public static class Object + { + [DllImport(Git2NativeLibName, EntryPoint = "git_object_type")] + public static extern ObjectTypes GetType(IntPtr objectHandle); + + [DllImport(Git2NativeLibName, EntryPoint = "git_object_free")] + public static extern void Free(IntPtr objHandle); + } + + public static class Commit + { + /// A handle to an oid owned by LibGit2 + [DllImport(Git2NativeLibName, EntryPoint = "git_commit_tree_id")] + public static extern IntPtr GetTreeId(IntPtr commitHandle); + } + + public static class Blob + { + [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawsize")] + [return: MarshalAs(UnmanagedType.U8)] + public static extern long GetRawSize(IntPtr objectHandle); + + [DllImport(Git2NativeLibName, EntryPoint = "git_blob_rawcontent")] + public static unsafe extern byte* GetRawContent(IntPtr objectHandle); + } + } + } } diff --git a/Scalar.Common/Git/LibGit2RepoInvoker.cs b/Scalar.Common/Git/LibGit2RepoInvoker.cs index 306388abdf..4fe028cb56 100644 --- a/Scalar.Common/Git/LibGit2RepoInvoker.cs +++ b/Scalar.Common/Git/LibGit2RepoInvoker.cs @@ -1,103 +1,103 @@ -using Scalar.Common.Tracing; -using System; -using System.Threading; - -namespace Scalar.Common.Git -{ - public class LibGit2RepoInvoker : IDisposable - { - private readonly Func createRepo; - private readonly ITracer tracer; - private readonly object sharedRepoLock = new object(); - private volatile bool disposing; - private volatile int activeCallers; - private LibGit2Repo sharedRepo; - - public LibGit2RepoInvoker(ITracer tracer, Func createRepo) - { - this.tracer = tracer; - this.createRepo = createRepo; - - this.InitializeSharedRepo(); - } - - public void Dispose() - { - this.disposing = true; - - lock (this.sharedRepoLock) - { - this.sharedRepo?.Dispose(); - this.sharedRepo = null; - } - } - - public bool TryInvoke(Func function, out TResult result) - { - try - { - Interlocked.Increment(ref this.activeCallers); - LibGit2Repo repo = this.GetSharedRepo(); - - if (repo != null) - { - result = function(repo); - return true; - } - - result = default(TResult); - return false; - } - catch (Exception e) - { - this.tracer.RelatedWarning("Exception while invoking libgit2: " + e.ToString(), Keywords.Telemetry); - throw; - } - finally - { - Interlocked.Decrement(ref this.activeCallers); - } - } - - public void DisposeSharedRepo() - { - lock (this.sharedRepoLock) - { - if (this.disposing || this.activeCallers > 0) - { - return; - } - - this.sharedRepo?.Dispose(); - this.sharedRepo = null; - } - } - - public void InitializeSharedRepo() - { - // Run a test on the shared repo to ensure the object store - // is loaded, as that is what takes a long time with many packs. - // Using a potentially-real object id is important, as the empty - // SHA will stop early instead of loading the object store. - this.GetSharedRepo()?.ObjectExists("30380be3963a75e4a34e10726795d644659e1129"); - } - - private LibGit2Repo GetSharedRepo() - { - lock (this.sharedRepoLock) - { - if (this.disposing) - { - return null; - } - - if (this.sharedRepo == null) - { - this.sharedRepo = this.createRepo(); - } - - return this.sharedRepo; - } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Threading; + +namespace Scalar.Common.Git +{ + public class LibGit2RepoInvoker : IDisposable + { + private readonly Func createRepo; + private readonly ITracer tracer; + private readonly object sharedRepoLock = new object(); + private volatile bool disposing; + private volatile int activeCallers; + private LibGit2Repo sharedRepo; + + public LibGit2RepoInvoker(ITracer tracer, Func createRepo) + { + this.tracer = tracer; + this.createRepo = createRepo; + + this.InitializeSharedRepo(); + } + + public void Dispose() + { + this.disposing = true; + + lock (this.sharedRepoLock) + { + this.sharedRepo?.Dispose(); + this.sharedRepo = null; + } + } + + public bool TryInvoke(Func function, out TResult result) + { + try + { + Interlocked.Increment(ref this.activeCallers); + LibGit2Repo repo = this.GetSharedRepo(); + + if (repo != null) + { + result = function(repo); + return true; + } + + result = default(TResult); + return false; + } + catch (Exception e) + { + this.tracer.RelatedWarning("Exception while invoking libgit2: " + e.ToString(), Keywords.Telemetry); + throw; + } + finally + { + Interlocked.Decrement(ref this.activeCallers); + } + } + + public void DisposeSharedRepo() + { + lock (this.sharedRepoLock) + { + if (this.disposing || this.activeCallers > 0) + { + return; + } + + this.sharedRepo?.Dispose(); + this.sharedRepo = null; + } + } + + public void InitializeSharedRepo() + { + // Run a test on the shared repo to ensure the object store + // is loaded, as that is what takes a long time with many packs. + // Using a potentially-real object id is important, as the empty + // SHA will stop early instead of loading the object store. + this.GetSharedRepo()?.ObjectExists("30380be3963a75e4a34e10726795d644659e1129"); + } + + private LibGit2Repo GetSharedRepo() + { + lock (this.sharedRepoLock) + { + if (this.disposing) + { + return null; + } + + if (this.sharedRepo == null) + { + this.sharedRepo = this.createRepo(); + } + + return this.sharedRepo; + } + } + } +} diff --git a/Scalar.Common/Git/RefLogEntry.cs b/Scalar.Common/Git/RefLogEntry.cs index f636403974..d8634b1ae6 100644 --- a/Scalar.Common/Git/RefLogEntry.cs +++ b/Scalar.Common/Git/RefLogEntry.cs @@ -1,44 +1,44 @@ -namespace Scalar.Common.Git -{ - public class RefLogEntry - { - public RefLogEntry(string sourceSha, string targetSha, string reason) - { - this.SourceSha = sourceSha; - this.TargetSha = targetSha; - this.Reason = reason; - } - - public string SourceSha { get; } - public string TargetSha { get; } - public string Reason { get; } - - public static bool TryParse(string line, out RefLogEntry entry) - { - entry = null; - if (string.IsNullOrEmpty(line)) - { - return false; - } - - if (line.Length < ScalarConstants.ShaStringLength + 1 + ScalarConstants.ShaStringLength) - { - return false; - } - - string sourceSha = line.Substring(0, ScalarConstants.ShaStringLength); - string targetSha = line.Substring(ScalarConstants.ShaStringLength + 1, ScalarConstants.ShaStringLength); - - int reasonStart = line.LastIndexOf("\t"); - if (reasonStart < 0) - { - return false; - } - - string reason = line.Substring(reasonStart + 1); - - entry = new RefLogEntry(sourceSha, targetSha, reason); - return true; - } - } -} +namespace Scalar.Common.Git +{ + public class RefLogEntry + { + public RefLogEntry(string sourceSha, string targetSha, string reason) + { + this.SourceSha = sourceSha; + this.TargetSha = targetSha; + this.Reason = reason; + } + + public string SourceSha { get; } + public string TargetSha { get; } + public string Reason { get; } + + public static bool TryParse(string line, out RefLogEntry entry) + { + entry = null; + if (string.IsNullOrEmpty(line)) + { + return false; + } + + if (line.Length < ScalarConstants.ShaStringLength + 1 + ScalarConstants.ShaStringLength) + { + return false; + } + + string sourceSha = line.Substring(0, ScalarConstants.ShaStringLength); + string targetSha = line.Substring(ScalarConstants.ShaStringLength + 1, ScalarConstants.ShaStringLength); + + int reasonStart = line.LastIndexOf("\t"); + if (reasonStart < 0) + { + return false; + } + + string reason = line.Substring(reasonStart + 1); + + entry = new RefLogEntry(sourceSha, targetSha, reason); + return true; + } + } +} diff --git a/Scalar.Common/Git/ScalarGitObjects.cs b/Scalar.Common/Git/ScalarGitObjects.cs index 4d8cbfa7a3..c9857722a8 100644 --- a/Scalar.Common/Git/ScalarGitObjects.cs +++ b/Scalar.Common/Git/ScalarGitObjects.cs @@ -1,111 +1,111 @@ -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Threading; - -namespace Scalar.Common.Git -{ - public class ScalarGitObjects : GitObjects - { - private static readonly TimeSpan NegativeCacheTTL = TimeSpan.FromSeconds(30); - - private ConcurrentDictionary objectNegativeCache; - - public ScalarGitObjects(ScalarContext context, GitObjectsHttpRequestor objectRequestor) - : base(context.Tracer, context.Enlistment, objectRequestor, context.FileSystem) - { - this.Context = context; - this.objectNegativeCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - } - - public enum RequestSource - { - Invalid = 0, - FileStreamCallback, - ScalarVerb, - NamedPipeMessage, - SymLinkCreation, - } - - protected ScalarContext Context { get; private set; } - - public DownloadAndSaveObjectResult TryDownloadAndSaveObject(string objectId, RequestSource requestSource) - { - return this.TryDownloadAndSaveObject(objectId, CancellationToken.None, requestSource, retryOnFailure: true); - } - - public bool TryGetBlobSizeLocally(string sha, out long length) - { - return this.Context.Repository.TryGetBlobLength(sha, out length); - } - - public List GetFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) - { - return this.GitObjectRequestor.QueryForFileSizes(objectIds, cancellationToken); - } - - private DownloadAndSaveObjectResult TryDownloadAndSaveObject( - string objectId, - CancellationToken cancellationToken, - RequestSource requestSource, - bool retryOnFailure) - { - if (objectId == ScalarConstants.AllZeroSha) - { - return DownloadAndSaveObjectResult.Error; - } - - DateTime negativeCacheRequestTime; - if (this.objectNegativeCache.TryGetValue(objectId, out negativeCacheRequestTime)) - { - if (negativeCacheRequestTime > DateTime.Now.Subtract(NegativeCacheTTL)) - { - return DownloadAndSaveObjectResult.ObjectNotOnServer; - } - - this.objectNegativeCache.TryRemove(objectId, out negativeCacheRequestTime); - } - - // To reduce allocations, reuse the same buffer when writing objects in this batch - byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; - - RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadLooseObject( - objectId, - retryOnFailure, - cancellationToken, - requestSource.ToString(), - onSuccess: (tryCount, response) => - { - // If the request is from git.exe (i.e. NamedPipeMessage) then we should assume that if there is an - // object on disk it's corrupt somehow (which is why git is asking for it) - this.WriteLooseObject( - response.Stream, - objectId, - overwriteExistingObject: requestSource == RequestSource.NamedPipeMessage, - bufToCopyWith: bufToCopyWith); - - return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); - }); - - if (output.Result != null) - { - if (output.Succeeded && output.Result.Success) - { - return DownloadAndSaveObjectResult.Success; - } - - if (output.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) - { - this.objectNegativeCache.AddOrUpdate(objectId, DateTime.Now, (unused1, unused2) => DateTime.Now); - return DownloadAndSaveObjectResult.ObjectNotOnServer; - } - } - - return DownloadAndSaveObjectResult.Error; - } - } +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Threading; + +namespace Scalar.Common.Git +{ + public class ScalarGitObjects : GitObjects + { + private static readonly TimeSpan NegativeCacheTTL = TimeSpan.FromSeconds(30); + + private ConcurrentDictionary objectNegativeCache; + + public ScalarGitObjects(ScalarContext context, GitObjectsHttpRequestor objectRequestor) + : base(context.Tracer, context.Enlistment, objectRequestor, context.FileSystem) + { + this.Context = context; + this.objectNegativeCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + } + + public enum RequestSource + { + Invalid = 0, + FileStreamCallback, + ScalarVerb, + NamedPipeMessage, + SymLinkCreation, + } + + protected ScalarContext Context { get; private set; } + + public DownloadAndSaveObjectResult TryDownloadAndSaveObject(string objectId, RequestSource requestSource) + { + return this.TryDownloadAndSaveObject(objectId, CancellationToken.None, requestSource, retryOnFailure: true); + } + + public bool TryGetBlobSizeLocally(string sha, out long length) + { + return this.Context.Repository.TryGetBlobLength(sha, out length); + } + + public List GetFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) + { + return this.GitObjectRequestor.QueryForFileSizes(objectIds, cancellationToken); + } + + private DownloadAndSaveObjectResult TryDownloadAndSaveObject( + string objectId, + CancellationToken cancellationToken, + RequestSource requestSource, + bool retryOnFailure) + { + if (objectId == ScalarConstants.AllZeroSha) + { + return DownloadAndSaveObjectResult.Error; + } + + DateTime negativeCacheRequestTime; + if (this.objectNegativeCache.TryGetValue(objectId, out negativeCacheRequestTime)) + { + if (negativeCacheRequestTime > DateTime.Now.Subtract(NegativeCacheTTL)) + { + return DownloadAndSaveObjectResult.ObjectNotOnServer; + } + + this.objectNegativeCache.TryRemove(objectId, out negativeCacheRequestTime); + } + + // To reduce allocations, reuse the same buffer when writing objects in this batch + byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; + + RetryWrapper.InvocationResult output = this.GitObjectRequestor.TryDownloadLooseObject( + objectId, + retryOnFailure, + cancellationToken, + requestSource.ToString(), + onSuccess: (tryCount, response) => + { + // If the request is from git.exe (i.e. NamedPipeMessage) then we should assume that if there is an + // object on disk it's corrupt somehow (which is why git is asking for it) + this.WriteLooseObject( + response.Stream, + objectId, + overwriteExistingObject: requestSource == RequestSource.NamedPipeMessage, + bufToCopyWith: bufToCopyWith); + + return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); + }); + + if (output.Result != null) + { + if (output.Succeeded && output.Result.Success) + { + return DownloadAndSaveObjectResult.Success; + } + + if (output.Result.HttpStatusCodeResult == HttpStatusCode.NotFound) + { + this.objectNegativeCache.AddOrUpdate(objectId, DateTime.Now, (unused1, unused2) => DateTime.Now); + return DownloadAndSaveObjectResult.ObjectNotOnServer; + } + } + + return DownloadAndSaveObjectResult.Error; + } + } } diff --git a/Scalar.Common/Git/Sha1Id.cs b/Scalar.Common/Git/Sha1Id.cs index b6e723b520..3ccdadce24 100644 --- a/Scalar.Common/Git/Sha1Id.cs +++ b/Scalar.Common/Git/Sha1Id.cs @@ -1,206 +1,206 @@ -using System; -using System.Runtime.InteropServices; - -namespace Scalar.Common.Git -{ - [StructLayout(LayoutKind.Explicit, Size = ShaBufferLength, Pack = 1)] - public struct Sha1Id - { - public static readonly Sha1Id None = new Sha1Id(); - - private const int ShaBufferLength = (2 * sizeof(ulong)) + sizeof(uint); - private const int ShaStringLength = 2 * ShaBufferLength; - - [FieldOffset(0)] - private ulong shaBytes1Through8; - - [FieldOffset(8)] - private ulong shaBytes9Through16; - - [FieldOffset(16)] - private uint shaBytes17Through20; - - public Sha1Id(ulong shaBytes1Through8, ulong shaBytes9Through16, uint shaBytes17Through20) - { - this.shaBytes1Through8 = shaBytes1Through8; - this.shaBytes9Through16 = shaBytes9Through16; - this.shaBytes17Through20 = shaBytes17Through20; - } - - public Sha1Id(string sha) - { - if (sha == null) - { - throw new ArgumentNullException(nameof(sha)); - } - - if (sha.Length != ShaStringLength) - { - throw new ArgumentException($"Must be length {ShaStringLength}", nameof(sha)); - } - - this.shaBytes1Through8 = ShaSubStringToULong(sha.Substring(0, 16)); - this.shaBytes9Through16 = ShaSubStringToULong(sha.Substring(16, 16)); - this.shaBytes17Through20 = ShaSubStringToUInt(sha.Substring(32, 8)); - } - - public static bool TryParse(string sha, out Sha1Id sha1, out string error) - { - error = null; - - try - { - sha1 = new Sha1Id(sha); - return true; - } - catch (Exception e) - { - error = e.Message; - } - - sha1 = new Sha1Id(0, 0, 0); - return false; - } - - public static void ShaBufferToParts( - byte[] shaBuffer, - out ulong shaBytes1Through8, - out ulong shaBytes9Through16, - out uint shaBytes17Through20) - { - if (shaBuffer == null) - { - throw new ArgumentNullException(nameof(shaBuffer)); - } - - if (shaBuffer.Length != ShaBufferLength) - { - throw new ArgumentException($"Must be length {ShaBufferLength}", nameof(shaBuffer)); - } - - unsafe - { - fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) - { - shaBytes1Through8 = *(ulong*)firstChunk; - shaBytes9Through16 = *(ulong*)secondChunk; - shaBytes17Through20 = *(uint*)thirdChunk; - } - } - } - - public void ToBuffer(byte[] shaBuffer) - { - unsafe - { - fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) - { - *(ulong*)firstChunk = this.shaBytes1Through8; - *(ulong*)secondChunk = this.shaBytes9Through16; - *(uint*)thirdChunk = this.shaBytes17Through20; - } - } - } - - public override string ToString() - { - char[] shaString = new char[ShaStringLength]; - BytesToCharArray(shaString, 0, this.shaBytes1Through8, sizeof(ulong)); - BytesToCharArray(shaString, 2 * sizeof(ulong), this.shaBytes9Through16, sizeof(ulong)); - BytesToCharArray(shaString, 2 * (2 * sizeof(ulong)), this.shaBytes17Through20, sizeof(uint)); - return new string(shaString, 0, shaString.Length); - } - - private static void BytesToCharArray(char[] shaString, int startIndex, ulong shaBytes, int numBytes) - { - byte b; - int firstArrayIndex; - for (int i = 0; i < numBytes; ++i) - { - b = (byte)(shaBytes >> (i * 8)); - firstArrayIndex = startIndex + (i * 2); - shaString[firstArrayIndex] = GetHexValue(b / 16); - shaString[firstArrayIndex + 1] = GetHexValue(b % 16); - } - } - - private static ulong ShaSubStringToULong(string shaSubString) - { - if (shaSubString == null) - { - throw new ArgumentNullException(nameof(shaSubString)); - } - - if (shaSubString.Length != sizeof(ulong) * 2) - { - throw new ArgumentException($"Must be length {sizeof(ulong) * 2}", nameof(shaSubString)); - } - - ulong bytes = 0; - string upperCaseSha = shaSubString.ToUpper(); - int stringIndex = 0; - for (int i = 0; i < sizeof(ulong); ++i) - { - stringIndex = i * 2; - char firstChar = shaSubString[stringIndex]; - char secondChar = shaSubString[stringIndex + 1]; - byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); - bytes = bytes | ((ulong)nextByte << (i * 8)); - } - - return bytes; - } - - private static uint ShaSubStringToUInt(string shaSubString) - { - if (shaSubString == null) - { - throw new ArgumentNullException(nameof(shaSubString)); - } - - if (shaSubString.Length != sizeof(uint) * 2) - { - throw new ArgumentException($"Must be length {sizeof(uint) * 2}", nameof(shaSubString)); - } - - uint bytes = 0; - string upperCaseSha = shaSubString.ToUpper(); - int stringIndex = 0; - for (int i = 0; i < sizeof(uint); ++i) - { - stringIndex = i * 2; - char firstChar = shaSubString[stringIndex]; - char secondChar = shaSubString[stringIndex + 1]; - byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); - bytes = bytes | ((uint)nextByte << (i * 8)); - } - - return bytes; - } - - private static char GetHexValue(int i) - { - if (i < 10) - { - return (char)(i + '0'); - } - - return (char)(i - 10 + 'A'); - } - - private static byte CharToByte(char c) - { - if (c >= '0' && c <= '9') - { - return (byte)(c - '0'); - } - - if (c >= 'A' && c <= 'F') - { - return (byte)(10 + (c - 'A')); - } - - throw new ArgumentException($"Invalid character {c}", nameof(c)); - } - } -} +using System; +using System.Runtime.InteropServices; + +namespace Scalar.Common.Git +{ + [StructLayout(LayoutKind.Explicit, Size = ShaBufferLength, Pack = 1)] + public struct Sha1Id + { + public static readonly Sha1Id None = new Sha1Id(); + + private const int ShaBufferLength = (2 * sizeof(ulong)) + sizeof(uint); + private const int ShaStringLength = 2 * ShaBufferLength; + + [FieldOffset(0)] + private ulong shaBytes1Through8; + + [FieldOffset(8)] + private ulong shaBytes9Through16; + + [FieldOffset(16)] + private uint shaBytes17Through20; + + public Sha1Id(ulong shaBytes1Through8, ulong shaBytes9Through16, uint shaBytes17Through20) + { + this.shaBytes1Through8 = shaBytes1Through8; + this.shaBytes9Through16 = shaBytes9Through16; + this.shaBytes17Through20 = shaBytes17Through20; + } + + public Sha1Id(string sha) + { + if (sha == null) + { + throw new ArgumentNullException(nameof(sha)); + } + + if (sha.Length != ShaStringLength) + { + throw new ArgumentException($"Must be length {ShaStringLength}", nameof(sha)); + } + + this.shaBytes1Through8 = ShaSubStringToULong(sha.Substring(0, 16)); + this.shaBytes9Through16 = ShaSubStringToULong(sha.Substring(16, 16)); + this.shaBytes17Through20 = ShaSubStringToUInt(sha.Substring(32, 8)); + } + + public static bool TryParse(string sha, out Sha1Id sha1, out string error) + { + error = null; + + try + { + sha1 = new Sha1Id(sha); + return true; + } + catch (Exception e) + { + error = e.Message; + } + + sha1 = new Sha1Id(0, 0, 0); + return false; + } + + public static void ShaBufferToParts( + byte[] shaBuffer, + out ulong shaBytes1Through8, + out ulong shaBytes9Through16, + out uint shaBytes17Through20) + { + if (shaBuffer == null) + { + throw new ArgumentNullException(nameof(shaBuffer)); + } + + if (shaBuffer.Length != ShaBufferLength) + { + throw new ArgumentException($"Must be length {ShaBufferLength}", nameof(shaBuffer)); + } + + unsafe + { + fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) + { + shaBytes1Through8 = *(ulong*)firstChunk; + shaBytes9Through16 = *(ulong*)secondChunk; + shaBytes17Through20 = *(uint*)thirdChunk; + } + } + } + + public void ToBuffer(byte[] shaBuffer) + { + unsafe + { + fixed (byte* firstChunk = &shaBuffer[0], secondChunk = &shaBuffer[sizeof(ulong)], thirdChunk = &shaBuffer[sizeof(ulong) * 2]) + { + *(ulong*)firstChunk = this.shaBytes1Through8; + *(ulong*)secondChunk = this.shaBytes9Through16; + *(uint*)thirdChunk = this.shaBytes17Through20; + } + } + } + + public override string ToString() + { + char[] shaString = new char[ShaStringLength]; + BytesToCharArray(shaString, 0, this.shaBytes1Through8, sizeof(ulong)); + BytesToCharArray(shaString, 2 * sizeof(ulong), this.shaBytes9Through16, sizeof(ulong)); + BytesToCharArray(shaString, 2 * (2 * sizeof(ulong)), this.shaBytes17Through20, sizeof(uint)); + return new string(shaString, 0, shaString.Length); + } + + private static void BytesToCharArray(char[] shaString, int startIndex, ulong shaBytes, int numBytes) + { + byte b; + int firstArrayIndex; + for (int i = 0; i < numBytes; ++i) + { + b = (byte)(shaBytes >> (i * 8)); + firstArrayIndex = startIndex + (i * 2); + shaString[firstArrayIndex] = GetHexValue(b / 16); + shaString[firstArrayIndex + 1] = GetHexValue(b % 16); + } + } + + private static ulong ShaSubStringToULong(string shaSubString) + { + if (shaSubString == null) + { + throw new ArgumentNullException(nameof(shaSubString)); + } + + if (shaSubString.Length != sizeof(ulong) * 2) + { + throw new ArgumentException($"Must be length {sizeof(ulong) * 2}", nameof(shaSubString)); + } + + ulong bytes = 0; + string upperCaseSha = shaSubString.ToUpper(); + int stringIndex = 0; + for (int i = 0; i < sizeof(ulong); ++i) + { + stringIndex = i * 2; + char firstChar = shaSubString[stringIndex]; + char secondChar = shaSubString[stringIndex + 1]; + byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); + bytes = bytes | ((ulong)nextByte << (i * 8)); + } + + return bytes; + } + + private static uint ShaSubStringToUInt(string shaSubString) + { + if (shaSubString == null) + { + throw new ArgumentNullException(nameof(shaSubString)); + } + + if (shaSubString.Length != sizeof(uint) * 2) + { + throw new ArgumentException($"Must be length {sizeof(uint) * 2}", nameof(shaSubString)); + } + + uint bytes = 0; + string upperCaseSha = shaSubString.ToUpper(); + int stringIndex = 0; + for (int i = 0; i < sizeof(uint); ++i) + { + stringIndex = i * 2; + char firstChar = shaSubString[stringIndex]; + char secondChar = shaSubString[stringIndex + 1]; + byte nextByte = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); + bytes = bytes | ((uint)nextByte << (i * 8)); + } + + return bytes; + } + + private static char GetHexValue(int i) + { + if (i < 10) + { + return (char)(i + '0'); + } + + return (char)(i - 10 + 'A'); + } + + private static byte CharToByte(char c) + { + if (c >= '0' && c <= '9') + { + return (byte)(c - '0'); + } + + if (c >= 'A' && c <= 'F') + { + return (byte)(10 + (c - 'A')); + } + + throw new ArgumentException($"Invalid character {c}", nameof(c)); + } + } +} diff --git a/Scalar.Common/GitCommandLineParser.cs b/Scalar.Common/GitCommandLineParser.cs index 005a83d209..a20c309784 100644 --- a/Scalar.Common/GitCommandLineParser.cs +++ b/Scalar.Common/GitCommandLineParser.cs @@ -1,182 +1,182 @@ -using System; -using System.Linq; - -namespace Scalar.Common -{ - public class GitCommandLineParser - { - private const int GitIndex = 0; - private const int VerbIndex = 1; - private const int ArgumentsOffset = 2; - - private readonly string[] parts; - private Verbs commandVerb; - - public GitCommandLineParser(string command) - { - if (!string.IsNullOrWhiteSpace(command)) - { - this.parts = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - - if (this.parts.Length < VerbIndex + 1 || - this.parts[GitIndex] != "git") - { - this.parts = null; - } - else - { - this.commandVerb = this.StringToVerbs(this.parts[VerbIndex]); - } - } - } - - [Flags] - public enum Verbs - { - Other = 1 << 0, - AddOrStage = 1 << 1, - Checkout = 1 << 2, - Commit = 1 << 3, - Move = 1 << 4, - Reset = 1 << 5, - Status = 1 << 6, - UpdateIndex = 1 << 7, - } - - public bool IsValidGitCommand - { - get { return this.parts != null; } - } - - public bool IsResetMixed() - { - return - this.IsResetSoftOrMixed() && - !this.HasArgument("--soft"); - } - - public bool IsResetSoftOrMixed() - { - return - this.IsVerb(Verbs.Reset) && - !this.HasArgument("--hard") && - !this.HasArgument("--keep") && - !this.HasArgument("--merge"); - } - - public bool IsSerializedStatus() - { - return this.IsVerb(Verbs.Status) && - this.HasArgumentPrefix("--serialize"); - } - - /// - /// This method currently just makes a best effort to detect file paths. Only use this method for optional optimizations - /// related to file paths. Do NOT use this method if you require a reliable answer. - /// - /// True if file paths were detected, otherwise false - public bool IsCheckoutWithFilePaths() - { - if (this.IsVerb(Verbs.Checkout)) - { - int numArguments = this.parts.Length - ArgumentsOffset; - - // The simplest way to know that we're dealing with file paths is if there are any arguments after a -- - // e.g. git checkout branchName -- fileName - int dashDashIndex; - if (this.HasAnyArgument(arg => arg == "--", out dashDashIndex) && - numArguments > dashDashIndex + 1) - { - return true; - } - - // We also special case one usage with HEAD, as long as there are no other arguments with - or -- that might - // result in behavior we haven't tested. - // e.g. git checkout HEAD fileName - if (numArguments >= 2 && - !this.HasAnyArgument(arg => arg.StartsWith("-")) && - this.HasArgumentAtIndex(ScalarConstants.DotGit.HeadName, argumentIndex: 0)) - { - return true; - } - - // Note: we have definitely missed some cases of file paths, e.g.: - // git checkout branchName fileName (detecting this reliably requires more complicated parsing) - // git checkout --patch (we currently have no need to optimize this scenario) - } - - return false; - } - - public bool IsVerb(Verbs verbs) - { - if (!this.IsValidGitCommand) - { - return false; - } - - return (verbs & this.commandVerb) == this.commandVerb; - } - - private Verbs StringToVerbs(string verb) - { - switch (verb) - { - case "add": return Verbs.AddOrStage; - case "checkout": return Verbs.Checkout; - case "commit": return Verbs.Commit; - case "mv": return Verbs.Move; - case "reset": return Verbs.Reset; - case "stage": return Verbs.AddOrStage; - case "status": return Verbs.Status; - case "update-index": return Verbs.UpdateIndex; - default: return Verbs.Other; - } - } - - private bool HasArgument(string argument) - { - return this.HasAnyArgument(arg => arg == argument); - } - - private bool HasArgumentPrefix(string argument) - { - return this.HasAnyArgument(arg => arg.StartsWith(argument, StringComparison.Ordinal)); - } - - private bool HasArgumentAtIndex(string argument, int argumentIndex) - { - int actualIndex = argumentIndex + ArgumentsOffset; - return - this.parts.Length > actualIndex && - this.parts[actualIndex] == argument; - } - - private bool HasAnyArgument(Predicate argumentPredicate) - { - int argumentIndex; - return this.HasAnyArgument(argumentPredicate, out argumentIndex); - } - - private bool HasAnyArgument(Predicate argumentPredicate, out int argumentIndex) - { - argumentIndex = -1; - - if (!this.IsValidGitCommand) - { - return false; - } - - for (int i = ArgumentsOffset; i < this.parts.Length; i++) - { - if (argumentPredicate(this.parts[i])) - { - argumentIndex = i - ArgumentsOffset; - return true; - } - } - - return false; - } - } -} +using System; +using System.Linq; + +namespace Scalar.Common +{ + public class GitCommandLineParser + { + private const int GitIndex = 0; + private const int VerbIndex = 1; + private const int ArgumentsOffset = 2; + + private readonly string[] parts; + private Verbs commandVerb; + + public GitCommandLineParser(string command) + { + if (!string.IsNullOrWhiteSpace(command)) + { + this.parts = command.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); + + if (this.parts.Length < VerbIndex + 1 || + this.parts[GitIndex] != "git") + { + this.parts = null; + } + else + { + this.commandVerb = this.StringToVerbs(this.parts[VerbIndex]); + } + } + } + + [Flags] + public enum Verbs + { + Other = 1 << 0, + AddOrStage = 1 << 1, + Checkout = 1 << 2, + Commit = 1 << 3, + Move = 1 << 4, + Reset = 1 << 5, + Status = 1 << 6, + UpdateIndex = 1 << 7, + } + + public bool IsValidGitCommand + { + get { return this.parts != null; } + } + + public bool IsResetMixed() + { + return + this.IsResetSoftOrMixed() && + !this.HasArgument("--soft"); + } + + public bool IsResetSoftOrMixed() + { + return + this.IsVerb(Verbs.Reset) && + !this.HasArgument("--hard") && + !this.HasArgument("--keep") && + !this.HasArgument("--merge"); + } + + public bool IsSerializedStatus() + { + return this.IsVerb(Verbs.Status) && + this.HasArgumentPrefix("--serialize"); + } + + /// + /// This method currently just makes a best effort to detect file paths. Only use this method for optional optimizations + /// related to file paths. Do NOT use this method if you require a reliable answer. + /// + /// True if file paths were detected, otherwise false + public bool IsCheckoutWithFilePaths() + { + if (this.IsVerb(Verbs.Checkout)) + { + int numArguments = this.parts.Length - ArgumentsOffset; + + // The simplest way to know that we're dealing with file paths is if there are any arguments after a -- + // e.g. git checkout branchName -- fileName + int dashDashIndex; + if (this.HasAnyArgument(arg => arg == "--", out dashDashIndex) && + numArguments > dashDashIndex + 1) + { + return true; + } + + // We also special case one usage with HEAD, as long as there are no other arguments with - or -- that might + // result in behavior we haven't tested. + // e.g. git checkout HEAD fileName + if (numArguments >= 2 && + !this.HasAnyArgument(arg => arg.StartsWith("-")) && + this.HasArgumentAtIndex(ScalarConstants.DotGit.HeadName, argumentIndex: 0)) + { + return true; + } + + // Note: we have definitely missed some cases of file paths, e.g.: + // git checkout branchName fileName (detecting this reliably requires more complicated parsing) + // git checkout --patch (we currently have no need to optimize this scenario) + } + + return false; + } + + public bool IsVerb(Verbs verbs) + { + if (!this.IsValidGitCommand) + { + return false; + } + + return (verbs & this.commandVerb) == this.commandVerb; + } + + private Verbs StringToVerbs(string verb) + { + switch (verb) + { + case "add": return Verbs.AddOrStage; + case "checkout": return Verbs.Checkout; + case "commit": return Verbs.Commit; + case "mv": return Verbs.Move; + case "reset": return Verbs.Reset; + case "stage": return Verbs.AddOrStage; + case "status": return Verbs.Status; + case "update-index": return Verbs.UpdateIndex; + default: return Verbs.Other; + } + } + + private bool HasArgument(string argument) + { + return this.HasAnyArgument(arg => arg == argument); + } + + private bool HasArgumentPrefix(string argument) + { + return this.HasAnyArgument(arg => arg.StartsWith(argument, StringComparison.Ordinal)); + } + + private bool HasArgumentAtIndex(string argument, int argumentIndex) + { + int actualIndex = argumentIndex + ArgumentsOffset; + return + this.parts.Length > actualIndex && + this.parts[actualIndex] == argument; + } + + private bool HasAnyArgument(Predicate argumentPredicate) + { + int argumentIndex; + return this.HasAnyArgument(argumentPredicate, out argumentIndex); + } + + private bool HasAnyArgument(Predicate argumentPredicate, out int argumentIndex) + { + argumentIndex = -1; + + if (!this.IsValidGitCommand) + { + return false; + } + + for (int i = ArgumentsOffset; i < this.parts.Length; i++) + { + if (argumentPredicate(this.parts[i])) + { + argumentIndex = i - ArgumentsOffset; + return true; + } + } + + return false; + } + } +} diff --git a/Scalar.Common/GitHubUpgrader.cs b/Scalar.Common/GitHubUpgrader.cs index 8c7b467aef..f08400123f 100644 --- a/Scalar.Common/GitHubUpgrader.cs +++ b/Scalar.Common/GitHubUpgrader.cs @@ -1,687 +1,687 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; -using System.Threading.Tasks; - -namespace Scalar.Common -{ - public class GitHubUpgrader : ProductUpgrader - { - private const string GitHubReleaseURL = @"https://api.github.com/repos/microsoft/scalar/releases"; - private const string JSONMediaType = @"application/vnd.github.v3+json"; - private const string UserAgent = @"Scalar_Auto_Upgrader"; - private const string CommonInstallerArgs = "/VERYSILENT /CLOSEAPPLICATIONS /SUPPRESSMSGBOXES /NORESTART"; - private const string ScalarInstallerArgs = CommonInstallerArgs + " /REMOUNTREPOS=false"; - private const string GitInstallerArgs = CommonInstallerArgs + " /ALLOWDOWNGRADE=1"; - private const string GitAssetId = "Git"; - private const string ScalarAssetId = "Scalar"; - private const string GitInstallerFileNamePrefix = "Git-"; - private const string ScalarSigner = "Microsoft Corporation"; - private const string ScalarCertIssuer = "Microsoft Code Signing PCA"; - private const string GitSigner = "Johannes Schindelin"; - private const string GitCertIssuer = "COMODO RSA Code Signing CA"; - - private static readonly HashSet ScalarInstallerFileNamePrefixCandidates = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "SetupScalar", - "Scalar" - }; - - private Version newestVersion; - private Release newestRelease; - - public GitHubUpgrader( - string currentVersion, - ITracer tracer, - PhysicalFileSystem fileSystem, - GitHubUpgraderConfig upgraderConfig, - bool dryRun = false, - bool noVerify = false) - : base(currentVersion, tracer, dryRun, noVerify, fileSystem) - { - this.Config = upgraderConfig; - } - - public GitHubUpgraderConfig Config { get; private set; } - - public override bool SupportsAnonymousVersionQuery { get => true; } - - public static GitHubUpgrader Create( - ITracer tracer, - PhysicalFileSystem fileSystem, - LocalScalarConfig scalarConfig, - bool dryRun, - bool noVerify, - out string error) - { - return Create(tracer, fileSystem, dryRun, noVerify, scalarConfig, out error); - } - - public static GitHubUpgrader Create( - ITracer tracer, - PhysicalFileSystem fileSystem, - bool dryRun, - bool noVerify, - LocalScalarConfig localConfig, - out string error) - { - GitHubUpgrader upgrader = null; - GitHubUpgraderConfig gitHubUpgraderConfig = new GitHubUpgraderConfig(tracer, localConfig); - - if (!gitHubUpgraderConfig.TryLoad(out error)) - { - return null; - } - - if (gitHubUpgraderConfig.ConfigError()) - { - gitHubUpgraderConfig.ConfigAlertMessage(out error); - return null; - } - - upgrader = new GitHubUpgrader( - ProcessHelper.GetCurrentProcessVersion(), - tracer, - fileSystem, - gitHubUpgraderConfig, - dryRun, - noVerify); - - return upgrader; - } - - public override bool UpgradeAllowed(out string message) - { - return this.Config.UpgradeAllowed(out message); - } - - public override bool TryQueryNewestVersion( - out Version newVersion, - out string message) - { - List releases; - - newVersion = null; - if (this.TryFetchReleases(out releases, out message)) - { - foreach (Release nextRelease in releases) - { - Version releaseVersion = null; - - if (nextRelease.Ring <= this.Config.UpgradeRing && - nextRelease.TryParseVersion(out releaseVersion) && - releaseVersion > this.installedVersion) - { - newVersion = releaseVersion; - this.newestVersion = releaseVersion; - this.newestRelease = nextRelease; - message = $"New Scalar version {newVersion.ToString()} available in ring {this.Config.UpgradeRing}."; - break; - } - } - - if (newVersion == null) - { - message = $"Great news, you're all caught up on upgrades in the {this.Config.UpgradeRing} ring!"; - } - - return true; - } - - return false; - } - - public override bool TryDownloadNewestVersion(out string errorMessage) - { - if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage)) - { - this.tracer.RelatedError($"{nameof(GitHubUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}"); - return false; - } - - bool downloadedGit = false; - bool downloadedScalar = false; - - foreach (Asset asset in this.newestRelease.Assets) - { - bool targetOSMatch = string.Equals(Path.GetExtension(asset.Name), ScalarPlatform.Instance.Constants.InstallerExtension, StringComparison.OrdinalIgnoreCase); - bool isGitAsset = this.IsGitAsset(asset); - bool isScalarAsset = isGitAsset ? false : this.IsScalarAsset(asset); - if (!targetOSMatch || (!isScalarAsset && !isGitAsset)) - { - continue; - } - - if (!this.TryDownloadAsset(asset, out errorMessage)) - { - errorMessage = $"Could not download {(isScalarAsset ? ScalarAssetId : GitAssetId)} installer. {errorMessage}"; - return false; - } - else - { - downloadedGit = isGitAsset ? true : downloadedGit; - downloadedScalar = isScalarAsset ? true : downloadedScalar; - } - } - - if (!downloadedGit || !downloadedScalar) - { - errorMessage = $"Could not find {(!downloadedGit ? GitAssetId : ScalarAssetId)} installer in the latest release."; - return false; - } - - errorMessage = null; - return true; - } - - public override bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error) - { - string localError; - - this.TryGetGitVersion(out GitVersion newGitVersion, out localError); - - if (!installActionWrapper( - () => - { - if (!this.TryInstallUpgrade(GitAssetId, newGitVersion.ToString(), out localError)) - { - return false; - } - - return true; - }, - $"Installing Git version: {newGitVersion}")) - { - error = localError; - return false; - } - - if (!installActionWrapper( - () => - { - if (!this.TryInstallUpgrade(ScalarAssetId, this.newestVersion.ToString(), out localError)) - { - return false; - } - - return true; - }, - $"Installing Scalar version: {this.newestVersion}")) - { - error = localError; - return false; - } - - this.LogVersionInfo(this.newestVersion, newGitVersion, "Newly Installed Version"); - - error = null; - return true; - } - - public override bool TryCleanup(out string error) - { - error = string.Empty; - if (this.newestRelease == null) - { - return true; - } - - foreach (Asset asset in this.newestRelease.Assets) - { - Exception exception; - if (!this.TryDeleteDownloadedAsset(asset, out exception)) - { - error += $"Could not delete {asset.LocalPath}. {exception.ToString()}." + Environment.NewLine; - } - } - - if (!string.IsNullOrEmpty(error)) - { - error.TrimEnd(Environment.NewLine.ToCharArray()); - return false; - } - - error = null; - return true; - } - - protected virtual bool TryDeleteDownloadedAsset(Asset asset, out Exception exception) - { - return this.fileSystem.TryDeleteFile(asset.LocalPath, out exception); - } - - protected virtual bool TryDownloadAsset(Asset asset, out string errorMessage) - { - errorMessage = null; - - string downloadPath = ProductUpgraderInfo.GetAssetDownloadsPath(); - string localPath = Path.Combine(downloadPath, asset.Name); - WebClient webClient = new WebClient(); - - try - { - webClient.DownloadFile(asset.DownloadURL, localPath); - asset.LocalPath = localPath; - } - catch (WebException webException) - { - errorMessage = "Download error: " + webException.Message; - this.TraceException(webException, nameof(this.TryDownloadAsset), $"Error downloading asset {asset.Name}."); - return false; - } - - return true; - } - - protected virtual bool TryFetchReleases(out List releases, out string errorMessage) - { - HttpClient client = new HttpClient(); - - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(JSONMediaType)); - client.DefaultRequestHeaders.Add("User-Agent", UserAgent); - - releases = null; - errorMessage = null; - - try - { - Stream result = client.GetStreamAsync(GitHubReleaseURL).GetAwaiter().GetResult(); - - DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List)); - releases = serializer.ReadObject(result) as List; - return true; - } - catch (HttpRequestException exception) - { - errorMessage = string.Format("Network error: could not connect to GitHub({0}). {1}", GitHubReleaseURL, exception.Message); - this.TraceException(exception, nameof(this.TryFetchReleases), $"Error fetching release info."); - } - catch (TaskCanceledException exception) - { - // GetStreamAsync can also throw a TaskCanceledException to indicate a timeout - // https://github.com/dotnet/corefx/issues/20296 - errorMessage = string.Format("Network error: could not connect to GitHub({0}). {1}", GitHubReleaseURL, exception.Message); - this.TraceException(exception, nameof(this.TryFetchReleases), $"Error fetching release info."); - } - catch (SerializationException exception) - { - errorMessage = string.Format("Parse error: could not parse releases info from GitHub({0}). {1}", GitHubReleaseURL, exception.Message); - this.TraceException(exception, nameof(this.TryFetchReleases), $"Error parsing release info."); - } - - return false; - } - - protected virtual void RunInstaller(string path, string args, string certCN, string issuerCN, out int exitCode, out string error) - { - using (Stream stream = this.fileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, false)) - { - string expectedCNPrefix = $"CN={certCN}, "; - string expectecIssuerCNPrefix = $"CN={issuerCN}"; - string subject; - string issuer; - if (!ScalarPlatform.Instance.TryVerifyAuthenticodeSignature(path, out subject, out issuer, out error)) - { - exitCode = -1; - return; - } - - if (!subject.StartsWith(expectedCNPrefix) || !issuer.StartsWith(expectecIssuerCNPrefix)) - { - exitCode = -1; - error = $"Installer {path} is signed by unknown signer."; - this.tracer.RelatedError($"Installer {path} is signed by unknown signer. Signed by {subject}, issued by {issuer} expected signer is {certCN}, issuer {issuerCN}."); - return; - } - - this.RunInstaller(path, args, out exitCode, out error); - } - } - - private bool TryGetGitVersion(out GitVersion gitVersion, out string error) - { - error = null; - - foreach (Asset asset in this.newestRelease.Assets) - { - if (asset.Name.StartsWith(GitInstallerFileNamePrefix) && - GitVersion.TryParseInstallerName(asset.Name, ScalarPlatform.Instance.Constants.InstallerExtension, out gitVersion)) - { - return true; - } - } - - error = "Could not find Git version info in newest release"; - gitVersion = null; - - return false; - } - - private bool TryInstallUpgrade(string assetId, string version, out string consoleError) - { - bool installSuccess = false; - EventMetadata metadata = new EventMetadata(); - metadata.Add("Upgrade Step", nameof(this.TryInstallUpgrade)); - metadata.Add("AssetId", assetId); - metadata.Add("Version", version); - - using (ITracer activity = this.tracer.StartActivity($"{nameof(this.TryInstallUpgrade)}", EventLevel.Informational, metadata)) - { - if (!this.TryRunInstaller(assetId, out installSuccess, out consoleError) || - !installSuccess) - { - this.tracer.RelatedError(metadata, $"{nameof(this.TryInstallUpgrade)} failed. {consoleError}"); - return false; - } - - activity.RelatedInfo("Successfully installed Scalar version: " + version); - } - - return installSuccess; - } - - private bool TryRunInstaller(string assetId, out bool installationSucceeded, out string error) - { - error = null; - installationSucceeded = false; - - int exitCode = 0; - bool launched = this.TryRunInstallerForAsset(assetId, out exitCode, out error); - installationSucceeded = exitCode == 0; - - return launched; - } - - private bool TryRunInstallerForAsset(string assetId, out int installerExitCode, out string error) - { - error = null; - installerExitCode = 0; - - bool installerIsRun = false; - string path; - string installerArgs; - if (this.TryGetLocalInstallerPath(assetId, out path, out installerArgs)) - { - if (!this.dryRun) - { - string logFilePath = ScalarEnlistment.GetNewLogFileName( - ProductUpgraderInfo.GetLogDirectoryPath(), - Path.GetFileNameWithoutExtension(path), - this.UpgradeInstanceId, - this.fileSystem); - - string args = installerArgs + " /Log=" + logFilePath; - string certCN = null; - string issuerCN = null; - switch (assetId) - { - case ScalarAssetId: - { - certCN = ScalarSigner; - issuerCN = ScalarCertIssuer; - break; - } - - case GitAssetId: - { - certCN = GitSigner; - issuerCN = GitCertIssuer; - break; - } - } - - this.RunInstaller(path, args, certCN, issuerCN, out installerExitCode, out error); - - if (installerExitCode != 0 && string.IsNullOrEmpty(error)) - { - error = assetId + " installer failed. Error log: " + logFilePath; - } - } - - installerIsRun = true; - } - else - { - error = "Could not find downloaded installer for " + assetId; - } - - return installerIsRun; - } - - private bool TryGetLocalInstallerPath(string assetId, out string path, out string args) - { - foreach (Asset asset in this.newestRelease.Assets) - { - if (string.Equals(Path.GetExtension(asset.Name), ScalarPlatform.Instance.Constants.InstallerExtension, StringComparison.OrdinalIgnoreCase)) - { - path = asset.LocalPath; - if (assetId == GitAssetId && this.IsGitAsset(asset)) - { - args = GitInstallerArgs; - return true; - } - - if (assetId == ScalarAssetId && this.IsScalarAsset(asset)) - { - args = ScalarInstallerArgs; - return true; - } - } - } - - path = null; - args = null; - return false; - } - - private bool IsScalarAsset(Asset asset) - { - return this.AssetInstallerNameCompare(asset, ScalarInstallerFileNamePrefixCandidates); - } - - private bool IsGitAsset(Asset asset) - { - return this.AssetInstallerNameCompare(asset, new string[] { GitInstallerFileNamePrefix }); - } - - private bool AssetInstallerNameCompare(Asset asset, IEnumerable expectedFileNamePrefixes) - { - foreach (string fileNamePrefix in expectedFileNamePrefixes) - { - if (asset.Name.StartsWith(fileNamePrefix, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - private void LogVersionInfo( - Version scalarVersion, - GitVersion gitVersion, - string message) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(scalarVersion), scalarVersion.ToString()); - metadata.Add(nameof(gitVersion), gitVersion.ToString()); - - this.tracer.RelatedEvent(EventLevel.Informational, message, metadata); - } - - public class GitHubUpgraderConfig - { - public GitHubUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) - { - this.Tracer = tracer; - this.LocalConfig = localScalarConfig; - } - - public enum RingType - { - // The values here should be ascending. - // Invalid - User has set an incorrect ring - // NoConfig - User has Not set any ring yet - // None - User has set a valid "None" ring - // (Fast should be greater than Slow, - // Slow should be greater than None, None greater than Invalid.) - // This is required for the correct implementation of Ring based - // upgrade logic. - Invalid = 0, - NoConfig = None - 1, - None = 10, - Slow = None + 1, - Fast = Slow + 1, - } - - public RingType UpgradeRing { get; private set; } - public LocalScalarConfig LocalConfig { get; private set; } - private ITracer Tracer { get; set; } - - public bool TryLoad(out string error) - { - this.UpgradeRing = RingType.NoConfig; - - string ringConfig = null; - string loadError = "Could not read Scalar Config." + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; - - if (!this.LocalConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out ringConfig, out error)) - { - error = loadError; - return false; - } - - this.ParseUpgradeRing(ringConfig); - return true; - } - - public void ParseUpgradeRing(string ringConfig) - { - if (string.IsNullOrEmpty(ringConfig)) - { - this.UpgradeRing = RingType.None; - return; - } - - RingType ringType; - if (Enum.TryParse(ringConfig, ignoreCase: true, result: out ringType) && - Enum.IsDefined(typeof(RingType), ringType) && - ringType != RingType.Invalid) - { - this.UpgradeRing = ringType; - } - else - { - this.UpgradeRing = RingType.Invalid; - } - } - - public bool ConfigError() - { - return this.UpgradeRing == RingType.Invalid; - } - - public bool UpgradeAllowed(out string message) - { - if (this.UpgradeRing == RingType.Slow || this.UpgradeRing == RingType.Fast) - { - message = null; - return true; - } - - this.ConfigAlertMessage(out message); - return false; - } - - public void ConfigAlertMessage(out string message) - { - message = null; - - if (this.UpgradeRing == GitHubUpgraderConfig.RingType.None) - { - message = ScalarConstants.UpgradeVerbMessages.NoneRingConsoleAlert + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; - } - - if (this.UpgradeRing == GitHubUpgraderConfig.RingType.NoConfig) - { - message = ScalarConstants.UpgradeVerbMessages.NoRingConfigConsoleAlert + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; - } - - if (this.UpgradeRing == GitHubUpgraderConfig.RingType.Invalid) - { - string ring; - string error; - string prefix = string.Empty; - if (this.LocalConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out ring, out error)) - { - prefix = $"Invalid upgrade ring `{ring}` specified in scalar config. "; - } - - message = prefix + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; - } - } - } - - [DataContract(Name = "asset")] - protected class Asset - { - [DataMember(Name = "name")] - public string Name { get; set; } - - [DataMember(Name = "size")] - public long Size { get; set; } - - [DataMember(Name = "browser_download_url")] - public Uri DownloadURL { get; set; } - - [IgnoreDataMember] - public string LocalPath { get; set; } - } - - [DataContract(Name = "release")] - protected class Release - { - [DataMember(Name = "name")] - public string Name { get; set; } - - [DataMember(Name = "tag_name")] - public string Tag { get; set; } - - [DataMember(Name = "prerelease")] - public bool PreRelease { get; set; } - - [DataMember(Name = "assets")] - public List Assets { get; set; } - - [IgnoreDataMember] - public GitHubUpgraderConfig.RingType Ring - { - get - { - return this.PreRelease == true ? GitHubUpgraderConfig.RingType.Fast : GitHubUpgraderConfig.RingType.Slow; - } - } - - public bool TryParseVersion(out Version version) - { - version = null; - - if (this.Tag.StartsWith("v", StringComparison.CurrentCultureIgnoreCase)) - { - return Version.TryParse(this.Tag.Substring(1), out version); - } - - return false; - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Threading.Tasks; + +namespace Scalar.Common +{ + public class GitHubUpgrader : ProductUpgrader + { + private const string GitHubReleaseURL = @"https://api.github.com/repos/microsoft/scalar/releases"; + private const string JSONMediaType = @"application/vnd.github.v3+json"; + private const string UserAgent = @"Scalar_Auto_Upgrader"; + private const string CommonInstallerArgs = "/VERYSILENT /CLOSEAPPLICATIONS /SUPPRESSMSGBOXES /NORESTART"; + private const string ScalarInstallerArgs = CommonInstallerArgs + " /REMOUNTREPOS=false"; + private const string GitInstallerArgs = CommonInstallerArgs + " /ALLOWDOWNGRADE=1"; + private const string GitAssetId = "Git"; + private const string ScalarAssetId = "Scalar"; + private const string GitInstallerFileNamePrefix = "Git-"; + private const string ScalarSigner = "Microsoft Corporation"; + private const string ScalarCertIssuer = "Microsoft Code Signing PCA"; + private const string GitSigner = "Johannes Schindelin"; + private const string GitCertIssuer = "COMODO RSA Code Signing CA"; + + private static readonly HashSet ScalarInstallerFileNamePrefixCandidates = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "SetupScalar", + "Scalar" + }; + + private Version newestVersion; + private Release newestRelease; + + public GitHubUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + GitHubUpgraderConfig upgraderConfig, + bool dryRun = false, + bool noVerify = false) + : base(currentVersion, tracer, dryRun, noVerify, fileSystem) + { + this.Config = upgraderConfig; + } + + public GitHubUpgraderConfig Config { get; private set; } + + public override bool SupportsAnonymousVersionQuery { get => true; } + + public static GitHubUpgrader Create( + ITracer tracer, + PhysicalFileSystem fileSystem, + LocalScalarConfig scalarConfig, + bool dryRun, + bool noVerify, + out string error) + { + return Create(tracer, fileSystem, dryRun, noVerify, scalarConfig, out error); + } + + public static GitHubUpgrader Create( + ITracer tracer, + PhysicalFileSystem fileSystem, + bool dryRun, + bool noVerify, + LocalScalarConfig localConfig, + out string error) + { + GitHubUpgrader upgrader = null; + GitHubUpgraderConfig gitHubUpgraderConfig = new GitHubUpgraderConfig(tracer, localConfig); + + if (!gitHubUpgraderConfig.TryLoad(out error)) + { + return null; + } + + if (gitHubUpgraderConfig.ConfigError()) + { + gitHubUpgraderConfig.ConfigAlertMessage(out error); + return null; + } + + upgrader = new GitHubUpgrader( + ProcessHelper.GetCurrentProcessVersion(), + tracer, + fileSystem, + gitHubUpgraderConfig, + dryRun, + noVerify); + + return upgrader; + } + + public override bool UpgradeAllowed(out string message) + { + return this.Config.UpgradeAllowed(out message); + } + + public override bool TryQueryNewestVersion( + out Version newVersion, + out string message) + { + List releases; + + newVersion = null; + if (this.TryFetchReleases(out releases, out message)) + { + foreach (Release nextRelease in releases) + { + Version releaseVersion = null; + + if (nextRelease.Ring <= this.Config.UpgradeRing && + nextRelease.TryParseVersion(out releaseVersion) && + releaseVersion > this.installedVersion) + { + newVersion = releaseVersion; + this.newestVersion = releaseVersion; + this.newestRelease = nextRelease; + message = $"New Scalar version {newVersion.ToString()} available in ring {this.Config.UpgradeRing}."; + break; + } + } + + if (newVersion == null) + { + message = $"Great news, you're all caught up on upgrades in the {this.Config.UpgradeRing} ring!"; + } + + return true; + } + + return false; + } + + public override bool TryDownloadNewestVersion(out string errorMessage) + { + if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage)) + { + this.tracer.RelatedError($"{nameof(GitHubUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}"); + return false; + } + + bool downloadedGit = false; + bool downloadedScalar = false; + + foreach (Asset asset in this.newestRelease.Assets) + { + bool targetOSMatch = string.Equals(Path.GetExtension(asset.Name), ScalarPlatform.Instance.Constants.InstallerExtension, StringComparison.OrdinalIgnoreCase); + bool isGitAsset = this.IsGitAsset(asset); + bool isScalarAsset = isGitAsset ? false : this.IsScalarAsset(asset); + if (!targetOSMatch || (!isScalarAsset && !isGitAsset)) + { + continue; + } + + if (!this.TryDownloadAsset(asset, out errorMessage)) + { + errorMessage = $"Could not download {(isScalarAsset ? ScalarAssetId : GitAssetId)} installer. {errorMessage}"; + return false; + } + else + { + downloadedGit = isGitAsset ? true : downloadedGit; + downloadedScalar = isScalarAsset ? true : downloadedScalar; + } + } + + if (!downloadedGit || !downloadedScalar) + { + errorMessage = $"Could not find {(!downloadedGit ? GitAssetId : ScalarAssetId)} installer in the latest release."; + return false; + } + + errorMessage = null; + return true; + } + + public override bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error) + { + string localError; + + this.TryGetGitVersion(out GitVersion newGitVersion, out localError); + + if (!installActionWrapper( + () => + { + if (!this.TryInstallUpgrade(GitAssetId, newGitVersion.ToString(), out localError)) + { + return false; + } + + return true; + }, + $"Installing Git version: {newGitVersion}")) + { + error = localError; + return false; + } + + if (!installActionWrapper( + () => + { + if (!this.TryInstallUpgrade(ScalarAssetId, this.newestVersion.ToString(), out localError)) + { + return false; + } + + return true; + }, + $"Installing Scalar version: {this.newestVersion}")) + { + error = localError; + return false; + } + + this.LogVersionInfo(this.newestVersion, newGitVersion, "Newly Installed Version"); + + error = null; + return true; + } + + public override bool TryCleanup(out string error) + { + error = string.Empty; + if (this.newestRelease == null) + { + return true; + } + + foreach (Asset asset in this.newestRelease.Assets) + { + Exception exception; + if (!this.TryDeleteDownloadedAsset(asset, out exception)) + { + error += $"Could not delete {asset.LocalPath}. {exception.ToString()}." + Environment.NewLine; + } + } + + if (!string.IsNullOrEmpty(error)) + { + error.TrimEnd(Environment.NewLine.ToCharArray()); + return false; + } + + error = null; + return true; + } + + protected virtual bool TryDeleteDownloadedAsset(Asset asset, out Exception exception) + { + return this.fileSystem.TryDeleteFile(asset.LocalPath, out exception); + } + + protected virtual bool TryDownloadAsset(Asset asset, out string errorMessage) + { + errorMessage = null; + + string downloadPath = ProductUpgraderInfo.GetAssetDownloadsPath(); + string localPath = Path.Combine(downloadPath, asset.Name); + WebClient webClient = new WebClient(); + + try + { + webClient.DownloadFile(asset.DownloadURL, localPath); + asset.LocalPath = localPath; + } + catch (WebException webException) + { + errorMessage = "Download error: " + webException.Message; + this.TraceException(webException, nameof(this.TryDownloadAsset), $"Error downloading asset {asset.Name}."); + return false; + } + + return true; + } + + protected virtual bool TryFetchReleases(out List releases, out string errorMessage) + { + HttpClient client = new HttpClient(); + + client.DefaultRequestHeaders.Accept.Clear(); + client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(JSONMediaType)); + client.DefaultRequestHeaders.Add("User-Agent", UserAgent); + + releases = null; + errorMessage = null; + + try + { + Stream result = client.GetStreamAsync(GitHubReleaseURL).GetAwaiter().GetResult(); + + DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List)); + releases = serializer.ReadObject(result) as List; + return true; + } + catch (HttpRequestException exception) + { + errorMessage = string.Format("Network error: could not connect to GitHub({0}). {1}", GitHubReleaseURL, exception.Message); + this.TraceException(exception, nameof(this.TryFetchReleases), $"Error fetching release info."); + } + catch (TaskCanceledException exception) + { + // GetStreamAsync can also throw a TaskCanceledException to indicate a timeout + // https://github.com/dotnet/corefx/issues/20296 + errorMessage = string.Format("Network error: could not connect to GitHub({0}). {1}", GitHubReleaseURL, exception.Message); + this.TraceException(exception, nameof(this.TryFetchReleases), $"Error fetching release info."); + } + catch (SerializationException exception) + { + errorMessage = string.Format("Parse error: could not parse releases info from GitHub({0}). {1}", GitHubReleaseURL, exception.Message); + this.TraceException(exception, nameof(this.TryFetchReleases), $"Error parsing release info."); + } + + return false; + } + + protected virtual void RunInstaller(string path, string args, string certCN, string issuerCN, out int exitCode, out string error) + { + using (Stream stream = this.fileSystem.OpenFileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, false)) + { + string expectedCNPrefix = $"CN={certCN}, "; + string expectecIssuerCNPrefix = $"CN={issuerCN}"; + string subject; + string issuer; + if (!ScalarPlatform.Instance.TryVerifyAuthenticodeSignature(path, out subject, out issuer, out error)) + { + exitCode = -1; + return; + } + + if (!subject.StartsWith(expectedCNPrefix) || !issuer.StartsWith(expectecIssuerCNPrefix)) + { + exitCode = -1; + error = $"Installer {path} is signed by unknown signer."; + this.tracer.RelatedError($"Installer {path} is signed by unknown signer. Signed by {subject}, issued by {issuer} expected signer is {certCN}, issuer {issuerCN}."); + return; + } + + this.RunInstaller(path, args, out exitCode, out error); + } + } + + private bool TryGetGitVersion(out GitVersion gitVersion, out string error) + { + error = null; + + foreach (Asset asset in this.newestRelease.Assets) + { + if (asset.Name.StartsWith(GitInstallerFileNamePrefix) && + GitVersion.TryParseInstallerName(asset.Name, ScalarPlatform.Instance.Constants.InstallerExtension, out gitVersion)) + { + return true; + } + } + + error = "Could not find Git version info in newest release"; + gitVersion = null; + + return false; + } + + private bool TryInstallUpgrade(string assetId, string version, out string consoleError) + { + bool installSuccess = false; + EventMetadata metadata = new EventMetadata(); + metadata.Add("Upgrade Step", nameof(this.TryInstallUpgrade)); + metadata.Add("AssetId", assetId); + metadata.Add("Version", version); + + using (ITracer activity = this.tracer.StartActivity($"{nameof(this.TryInstallUpgrade)}", EventLevel.Informational, metadata)) + { + if (!this.TryRunInstaller(assetId, out installSuccess, out consoleError) || + !installSuccess) + { + this.tracer.RelatedError(metadata, $"{nameof(this.TryInstallUpgrade)} failed. {consoleError}"); + return false; + } + + activity.RelatedInfo("Successfully installed Scalar version: " + version); + } + + return installSuccess; + } + + private bool TryRunInstaller(string assetId, out bool installationSucceeded, out string error) + { + error = null; + installationSucceeded = false; + + int exitCode = 0; + bool launched = this.TryRunInstallerForAsset(assetId, out exitCode, out error); + installationSucceeded = exitCode == 0; + + return launched; + } + + private bool TryRunInstallerForAsset(string assetId, out int installerExitCode, out string error) + { + error = null; + installerExitCode = 0; + + bool installerIsRun = false; + string path; + string installerArgs; + if (this.TryGetLocalInstallerPath(assetId, out path, out installerArgs)) + { + if (!this.dryRun) + { + string logFilePath = ScalarEnlistment.GetNewLogFileName( + ProductUpgraderInfo.GetLogDirectoryPath(), + Path.GetFileNameWithoutExtension(path), + this.UpgradeInstanceId, + this.fileSystem); + + string args = installerArgs + " /Log=" + logFilePath; + string certCN = null; + string issuerCN = null; + switch (assetId) + { + case ScalarAssetId: + { + certCN = ScalarSigner; + issuerCN = ScalarCertIssuer; + break; + } + + case GitAssetId: + { + certCN = GitSigner; + issuerCN = GitCertIssuer; + break; + } + } + + this.RunInstaller(path, args, certCN, issuerCN, out installerExitCode, out error); + + if (installerExitCode != 0 && string.IsNullOrEmpty(error)) + { + error = assetId + " installer failed. Error log: " + logFilePath; + } + } + + installerIsRun = true; + } + else + { + error = "Could not find downloaded installer for " + assetId; + } + + return installerIsRun; + } + + private bool TryGetLocalInstallerPath(string assetId, out string path, out string args) + { + foreach (Asset asset in this.newestRelease.Assets) + { + if (string.Equals(Path.GetExtension(asset.Name), ScalarPlatform.Instance.Constants.InstallerExtension, StringComparison.OrdinalIgnoreCase)) + { + path = asset.LocalPath; + if (assetId == GitAssetId && this.IsGitAsset(asset)) + { + args = GitInstallerArgs; + return true; + } + + if (assetId == ScalarAssetId && this.IsScalarAsset(asset)) + { + args = ScalarInstallerArgs; + return true; + } + } + } + + path = null; + args = null; + return false; + } + + private bool IsScalarAsset(Asset asset) + { + return this.AssetInstallerNameCompare(asset, ScalarInstallerFileNamePrefixCandidates); + } + + private bool IsGitAsset(Asset asset) + { + return this.AssetInstallerNameCompare(asset, new string[] { GitInstallerFileNamePrefix }); + } + + private bool AssetInstallerNameCompare(Asset asset, IEnumerable expectedFileNamePrefixes) + { + foreach (string fileNamePrefix in expectedFileNamePrefixes) + { + if (asset.Name.StartsWith(fileNamePrefix, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + private void LogVersionInfo( + Version scalarVersion, + GitVersion gitVersion, + string message) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(scalarVersion), scalarVersion.ToString()); + metadata.Add(nameof(gitVersion), gitVersion.ToString()); + + this.tracer.RelatedEvent(EventLevel.Informational, message, metadata); + } + + public class GitHubUpgraderConfig + { + public GitHubUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) + { + this.Tracer = tracer; + this.LocalConfig = localScalarConfig; + } + + public enum RingType + { + // The values here should be ascending. + // Invalid - User has set an incorrect ring + // NoConfig - User has Not set any ring yet + // None - User has set a valid "None" ring + // (Fast should be greater than Slow, + // Slow should be greater than None, None greater than Invalid.) + // This is required for the correct implementation of Ring based + // upgrade logic. + Invalid = 0, + NoConfig = None - 1, + None = 10, + Slow = None + 1, + Fast = Slow + 1, + } + + public RingType UpgradeRing { get; private set; } + public LocalScalarConfig LocalConfig { get; private set; } + private ITracer Tracer { get; set; } + + public bool TryLoad(out string error) + { + this.UpgradeRing = RingType.NoConfig; + + string ringConfig = null; + string loadError = "Could not read Scalar Config." + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; + + if (!this.LocalConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out ringConfig, out error)) + { + error = loadError; + return false; + } + + this.ParseUpgradeRing(ringConfig); + return true; + } + + public void ParseUpgradeRing(string ringConfig) + { + if (string.IsNullOrEmpty(ringConfig)) + { + this.UpgradeRing = RingType.None; + return; + } + + RingType ringType; + if (Enum.TryParse(ringConfig, ignoreCase: true, result: out ringType) && + Enum.IsDefined(typeof(RingType), ringType) && + ringType != RingType.Invalid) + { + this.UpgradeRing = ringType; + } + else + { + this.UpgradeRing = RingType.Invalid; + } + } + + public bool ConfigError() + { + return this.UpgradeRing == RingType.Invalid; + } + + public bool UpgradeAllowed(out string message) + { + if (this.UpgradeRing == RingType.Slow || this.UpgradeRing == RingType.Fast) + { + message = null; + return true; + } + + this.ConfigAlertMessage(out message); + return false; + } + + public void ConfigAlertMessage(out string message) + { + message = null; + + if (this.UpgradeRing == GitHubUpgraderConfig.RingType.None) + { + message = ScalarConstants.UpgradeVerbMessages.NoneRingConsoleAlert + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; + } + + if (this.UpgradeRing == GitHubUpgraderConfig.RingType.NoConfig) + { + message = ScalarConstants.UpgradeVerbMessages.NoRingConfigConsoleAlert + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; + } + + if (this.UpgradeRing == GitHubUpgraderConfig.RingType.Invalid) + { + string ring; + string error; + string prefix = string.Empty; + if (this.LocalConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out ring, out error)) + { + prefix = $"Invalid upgrade ring `{ring}` specified in scalar config. "; + } + + message = prefix + Environment.NewLine + ScalarConstants.UpgradeVerbMessages.SetUpgradeRingCommand; + } + } + } + + [DataContract(Name = "asset")] + protected class Asset + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "size")] + public long Size { get; set; } + + [DataMember(Name = "browser_download_url")] + public Uri DownloadURL { get; set; } + + [IgnoreDataMember] + public string LocalPath { get; set; } + } + + [DataContract(Name = "release")] + protected class Release + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "tag_name")] + public string Tag { get; set; } + + [DataMember(Name = "prerelease")] + public bool PreRelease { get; set; } + + [DataMember(Name = "assets")] + public List Assets { get; set; } + + [IgnoreDataMember] + public GitHubUpgraderConfig.RingType Ring + { + get + { + return this.PreRelease == true ? GitHubUpgraderConfig.RingType.Fast : GitHubUpgraderConfig.RingType.Slow; + } + } + + public bool TryParseVersion(out Version version) + { + version = null; + + if (this.Tag.StartsWith("v", StringComparison.CurrentCultureIgnoreCase)) + { + return Version.TryParse(this.Tag.Substring(1), out version); + } + + return false; + } + } + } +} diff --git a/Scalar.Common/HeartbeatThread.cs b/Scalar.Common/HeartbeatThread.cs index 39a9763333..c16894e58c 100644 --- a/Scalar.Common/HeartbeatThread.cs +++ b/Scalar.Common/HeartbeatThread.cs @@ -1,71 +1,71 @@ -using Scalar.Common.Tracing; -using System; -using System.Threading; - -namespace Scalar.Common -{ - public class HeartbeatThread - { - private static readonly TimeSpan HeartBeatWaitTime = TimeSpan.FromMinutes(60); - - private readonly ITracer tracer; - private readonly IHeartBeatMetadataProvider dataProvider; - - private Timer timer; - private DateTime startTime; - private DateTime lastHeartBeatTime; - - public HeartbeatThread(ITracer tracer, IHeartBeatMetadataProvider dataProvider) - { - this.tracer = tracer; - this.dataProvider = dataProvider; - } - - public void Start() - { - this.startTime = DateTime.Now; - this.lastHeartBeatTime = DateTime.Now; - this.timer = new Timer( - this.EmitHeartbeat, - state: null, - dueTime: HeartBeatWaitTime, - period: HeartBeatWaitTime); - } - - public void Stop() - { - using (WaitHandle waitHandle = new ManualResetEvent(false)) - { - if (this.timer.Dispose(waitHandle)) - { - waitHandle.WaitOne(); - waitHandle.Close(); - } - } - - this.EmitHeartbeat(unusedState: null); - } - - private void EmitHeartbeat(object unusedState) - { - try - { - EventMetadata metadata = this.dataProvider.GetAndResetHeartBeatMetadata(out bool writeToLogFile) ?? new EventMetadata(); - EventLevel eventLevel = writeToLogFile ? EventLevel.Informational : EventLevel.Verbose; - DateTime now = DateTime.Now; - metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); - metadata.Add("MinutesUptime", (long)(now - this.startTime).TotalMinutes); - metadata.Add("MinutesSinceLast", (int)(now - this.lastHeartBeatTime).TotalMinutes); - this.lastHeartBeatTime = now; - this.tracer.RelatedEvent(eventLevel, "Heartbeat", metadata, Keywords.Telemetry); - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "HeartbeatThread"); - metadata.Add("Exception", e.ToString()); - this.tracer.RelatedWarning(metadata, "Swallowing unhandled exception in EmitHeartbeat", Keywords.Telemetry); - } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Threading; + +namespace Scalar.Common +{ + public class HeartbeatThread + { + private static readonly TimeSpan HeartBeatWaitTime = TimeSpan.FromMinutes(60); + + private readonly ITracer tracer; + private readonly IHeartBeatMetadataProvider dataProvider; + + private Timer timer; + private DateTime startTime; + private DateTime lastHeartBeatTime; + + public HeartbeatThread(ITracer tracer, IHeartBeatMetadataProvider dataProvider) + { + this.tracer = tracer; + this.dataProvider = dataProvider; + } + + public void Start() + { + this.startTime = DateTime.Now; + this.lastHeartBeatTime = DateTime.Now; + this.timer = new Timer( + this.EmitHeartbeat, + state: null, + dueTime: HeartBeatWaitTime, + period: HeartBeatWaitTime); + } + + public void Stop() + { + using (WaitHandle waitHandle = new ManualResetEvent(false)) + { + if (this.timer.Dispose(waitHandle)) + { + waitHandle.WaitOne(); + waitHandle.Close(); + } + } + + this.EmitHeartbeat(unusedState: null); + } + + private void EmitHeartbeat(object unusedState) + { + try + { + EventMetadata metadata = this.dataProvider.GetAndResetHeartBeatMetadata(out bool writeToLogFile) ?? new EventMetadata(); + EventLevel eventLevel = writeToLogFile ? EventLevel.Informational : EventLevel.Verbose; + DateTime now = DateTime.Now; + metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); + metadata.Add("MinutesUptime", (long)(now - this.startTime).TotalMinutes); + metadata.Add("MinutesSinceLast", (int)(now - this.lastHeartBeatTime).TotalMinutes); + this.lastHeartBeatTime = now; + this.tracer.RelatedEvent(eventLevel, "Heartbeat", metadata, Keywords.Telemetry); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "HeartbeatThread"); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedWarning(metadata, "Swallowing unhandled exception in EmitHeartbeat", Keywords.Telemetry); + } + } + } +} diff --git a/Scalar.Common/Http/CacheServerInfo.cs b/Scalar.Common/Http/CacheServerInfo.cs index 8a38440090..3a0424a97e 100644 --- a/Scalar.Common/Http/CacheServerInfo.cs +++ b/Scalar.Common/Http/CacheServerInfo.cs @@ -1,68 +1,68 @@ -using Newtonsoft.Json; -using System; - -namespace Scalar.Common.Http -{ - public class CacheServerInfo - { - private const string ObjectsEndpointSuffix = "/gvfs/objects"; - private const string PrefetchEndpointSuffix = "/gvfs/prefetch"; - private const string SizesEndpointSuffix = "/gvfs/sizes"; - - [JsonConstructor] - public CacheServerInfo(string url, string name, bool globalDefault = false) - { - this.Url = url; - this.Name = name; - this.GlobalDefault = globalDefault; - - if (this.Url != null) - { - this.ObjectsEndpointUrl = this.Url + ObjectsEndpointSuffix; - this.PrefetchEndpointUrl = this.Url + PrefetchEndpointSuffix; - this.SizesEndpointUrl = this.Url + SizesEndpointSuffix; - } - } - - public string Url { get; } - public string Name { get; } - public bool GlobalDefault { get; } - - public string ObjectsEndpointUrl { get; } - public string PrefetchEndpointUrl { get; } - public string SizesEndpointUrl { get; } - - public bool HasValidUrl() - { - return Uri.IsWellFormedUriString(this.Url, UriKind.Absolute); - } - - public bool IsNone(string repoUrl) - { - return ReservedNames.None.Equals(this.Name, StringComparison.OrdinalIgnoreCase) - || this.Url?.StartsWith(repoUrl, StringComparison.OrdinalIgnoreCase) == true; - } - - public override string ToString() - { - if (string.IsNullOrWhiteSpace(this.Name)) - { - return this.Url; - } - - if (string.IsNullOrWhiteSpace(this.Url)) - { - return this.Name; - } - - return string.Format("{0} ({1})", this.Name, this.Url); - } - - public static class ReservedNames - { - public const string None = "None"; - public const string Default = "Default"; - public const string UserDefined = "User Defined"; - } - } -} +using Newtonsoft.Json; +using System; + +namespace Scalar.Common.Http +{ + public class CacheServerInfo + { + private const string ObjectsEndpointSuffix = "/gvfs/objects"; + private const string PrefetchEndpointSuffix = "/gvfs/prefetch"; + private const string SizesEndpointSuffix = "/gvfs/sizes"; + + [JsonConstructor] + public CacheServerInfo(string url, string name, bool globalDefault = false) + { + this.Url = url; + this.Name = name; + this.GlobalDefault = globalDefault; + + if (this.Url != null) + { + this.ObjectsEndpointUrl = this.Url + ObjectsEndpointSuffix; + this.PrefetchEndpointUrl = this.Url + PrefetchEndpointSuffix; + this.SizesEndpointUrl = this.Url + SizesEndpointSuffix; + } + } + + public string Url { get; } + public string Name { get; } + public bool GlobalDefault { get; } + + public string ObjectsEndpointUrl { get; } + public string PrefetchEndpointUrl { get; } + public string SizesEndpointUrl { get; } + + public bool HasValidUrl() + { + return Uri.IsWellFormedUriString(this.Url, UriKind.Absolute); + } + + public bool IsNone(string repoUrl) + { + return ReservedNames.None.Equals(this.Name, StringComparison.OrdinalIgnoreCase) + || this.Url?.StartsWith(repoUrl, StringComparison.OrdinalIgnoreCase) == true; + } + + public override string ToString() + { + if (string.IsNullOrWhiteSpace(this.Name)) + { + return this.Url; + } + + if (string.IsNullOrWhiteSpace(this.Url)) + { + return this.Name; + } + + return string.Format("{0} ({1})", this.Name, this.Url); + } + + public static class ReservedNames + { + public const string None = "None"; + public const string Default = "Default"; + public const string UserDefined = "User Defined"; + } + } +} diff --git a/Scalar.Common/Http/CacheServerResolver.cs b/Scalar.Common/Http/CacheServerResolver.cs index c959421ab2..8f69b7c829 100644 --- a/Scalar.Common/Http/CacheServerResolver.cs +++ b/Scalar.Common/Http/CacheServerResolver.cs @@ -1,168 +1,168 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Linq; - -namespace Scalar.Common.Http -{ - public class CacheServerResolver - { - private ITracer tracer; - private Enlistment enlistment; - - public CacheServerResolver( - ITracer tracer, - Enlistment enlistment) - { - this.tracer = tracer; - this.enlistment = enlistment; - } - - public static CacheServerInfo GetCacheServerFromConfig(Enlistment enlistment) - { - string url = GetUrlFromConfig(enlistment); - return new CacheServerInfo( - url, - url == enlistment.RepoUrl ? CacheServerInfo.ReservedNames.None : null); - } - - public static string GetUrlFromConfig(Enlistment enlistment) - { - GitProcess git = enlistment.CreateGitProcess(); - - // TODO 1057500: Remove support for encoded-repo-url cache config setting - return - GetValueFromConfig(git, ScalarConstants.GitConfig.CacheServer, localOnly: true) - ?? GetValueFromConfig(git, GetDeprecatedCacheConfigSettingName(enlistment), localOnly: false) - ?? enlistment.RepoUrl; - } - - public bool TryResolveUrlFromRemote( - string cacheServerName, - ServerScalarConfig serverScalarConfig, - out CacheServerInfo cacheServer, - out string error) - { - if (string.IsNullOrWhiteSpace(cacheServerName)) - { - throw new InvalidOperationException("An empty name is not supported"); - } - - cacheServer = null; - error = null; - - if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase)) - { - cacheServer = - serverScalarConfig.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) - ?? this.CreateNone(); - } - else - { - cacheServer = serverScalarConfig.CacheServers.FirstOrDefault(cache => - cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase)); - - if (cacheServer == null) - { - error = "No cache server found with name " + cacheServerName; - return false; - } - } - - return true; - } - - public CacheServerInfo ResolveNameFromRemote( - string cacheServerUrl, - ServerScalarConfig serverScalarConfig) - { - if (string.IsNullOrWhiteSpace(cacheServerUrl)) - { - throw new InvalidOperationException("An empty url is not supported"); - } - - if (this.InputMatchesEnlistmentUrl(cacheServerUrl)) - { - return this.CreateNone(); - } - - return - serverScalarConfig.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) - ?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined); - } - - public CacheServerInfo ParseUrlOrFriendlyName(string userInput) - { - if (userInput == null) - { - return new CacheServerInfo(null, CacheServerInfo.ReservedNames.Default); - } - - if (string.IsNullOrWhiteSpace(userInput)) - { - throw new InvalidOperationException("A missing input (null) is fine, but an empty input (empty string) is not supported"); - } - - if (this.InputMatchesEnlistmentUrl(userInput) || - userInput.Equals(CacheServerInfo.ReservedNames.None, StringComparison.OrdinalIgnoreCase)) - { - return this.CreateNone(); - } - - Uri uri; - if (Uri.TryCreate(userInput, UriKind.Absolute, out uri)) - { - return new CacheServerInfo(userInput, CacheServerInfo.ReservedNames.UserDefined); - } - else - { - return new CacheServerInfo(null, userInput); - } - } - - public bool TrySaveUrlToLocalConfig(CacheServerInfo cache, out string error) - { - GitProcess git = this.enlistment.CreateGitProcess(); - GitProcess.Result result = git.SetInLocalConfig(ScalarConstants.GitConfig.CacheServer, cache.Url, replaceAll: true); - - error = result.Errors; - return result.ExitCodeIsSuccess; - } - - private static string GetValueFromConfig(GitProcess git, string configName, bool localOnly) - { - GitProcess.ConfigResult result = - localOnly - ? git.GetFromLocalConfig(configName) - : git.GetFromConfig(configName); - - if (!result.TryParseAsString(out string value, out string error)) - { - throw new InvalidRepoException(error); - } - - return value; - } - - private static string GetDeprecatedCacheConfigSettingName(Enlistment enlistment) - { - string sectionUrl = - enlistment.RepoUrl.ToLowerInvariant() - .Replace("https://", string.Empty) - .Replace("http://", string.Empty) - .Replace('/', '.'); - - return ScalarConstants.GitConfig.ScalarPrefix + sectionUrl + ScalarConstants.GitConfig.DeprecatedCacheEndpointSuffix; - } - - private CacheServerInfo CreateNone() - { - return new CacheServerInfo(this.enlistment.RepoUrl, CacheServerInfo.ReservedNames.None); - } - - private bool InputMatchesEnlistmentUrl(string userInput) - { - return this.enlistment.RepoUrl.TrimEnd('/').Equals(userInput.TrimEnd('/'), StringComparison.OrdinalIgnoreCase); - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Linq; + +namespace Scalar.Common.Http +{ + public class CacheServerResolver + { + private ITracer tracer; + private Enlistment enlistment; + + public CacheServerResolver( + ITracer tracer, + Enlistment enlistment) + { + this.tracer = tracer; + this.enlistment = enlistment; + } + + public static CacheServerInfo GetCacheServerFromConfig(Enlistment enlistment) + { + string url = GetUrlFromConfig(enlistment); + return new CacheServerInfo( + url, + url == enlistment.RepoUrl ? CacheServerInfo.ReservedNames.None : null); + } + + public static string GetUrlFromConfig(Enlistment enlistment) + { + GitProcess git = enlistment.CreateGitProcess(); + + // TODO 1057500: Remove support for encoded-repo-url cache config setting + return + GetValueFromConfig(git, ScalarConstants.GitConfig.CacheServer, localOnly: true) + ?? GetValueFromConfig(git, GetDeprecatedCacheConfigSettingName(enlistment), localOnly: false) + ?? enlistment.RepoUrl; + } + + public bool TryResolveUrlFromRemote( + string cacheServerName, + ServerScalarConfig serverScalarConfig, + out CacheServerInfo cacheServer, + out string error) + { + if (string.IsNullOrWhiteSpace(cacheServerName)) + { + throw new InvalidOperationException("An empty name is not supported"); + } + + cacheServer = null; + error = null; + + if (cacheServerName.Equals(CacheServerInfo.ReservedNames.Default, StringComparison.OrdinalIgnoreCase)) + { + cacheServer = + serverScalarConfig.CacheServers.FirstOrDefault(cache => cache.GlobalDefault) + ?? this.CreateNone(); + } + else + { + cacheServer = serverScalarConfig.CacheServers.FirstOrDefault(cache => + cache.Name.Equals(cacheServerName, StringComparison.OrdinalIgnoreCase)); + + if (cacheServer == null) + { + error = "No cache server found with name " + cacheServerName; + return false; + } + } + + return true; + } + + public CacheServerInfo ResolveNameFromRemote( + string cacheServerUrl, + ServerScalarConfig serverScalarConfig) + { + if (string.IsNullOrWhiteSpace(cacheServerUrl)) + { + throw new InvalidOperationException("An empty url is not supported"); + } + + if (this.InputMatchesEnlistmentUrl(cacheServerUrl)) + { + return this.CreateNone(); + } + + return + serverScalarConfig.CacheServers.FirstOrDefault(cache => cache.Url.Equals(cacheServerUrl, StringComparison.OrdinalIgnoreCase)) + ?? new CacheServerInfo(cacheServerUrl, CacheServerInfo.ReservedNames.UserDefined); + } + + public CacheServerInfo ParseUrlOrFriendlyName(string userInput) + { + if (userInput == null) + { + return new CacheServerInfo(null, CacheServerInfo.ReservedNames.Default); + } + + if (string.IsNullOrWhiteSpace(userInput)) + { + throw new InvalidOperationException("A missing input (null) is fine, but an empty input (empty string) is not supported"); + } + + if (this.InputMatchesEnlistmentUrl(userInput) || + userInput.Equals(CacheServerInfo.ReservedNames.None, StringComparison.OrdinalIgnoreCase)) + { + return this.CreateNone(); + } + + Uri uri; + if (Uri.TryCreate(userInput, UriKind.Absolute, out uri)) + { + return new CacheServerInfo(userInput, CacheServerInfo.ReservedNames.UserDefined); + } + else + { + return new CacheServerInfo(null, userInput); + } + } + + public bool TrySaveUrlToLocalConfig(CacheServerInfo cache, out string error) + { + GitProcess git = this.enlistment.CreateGitProcess(); + GitProcess.Result result = git.SetInLocalConfig(ScalarConstants.GitConfig.CacheServer, cache.Url, replaceAll: true); + + error = result.Errors; + return result.ExitCodeIsSuccess; + } + + private static string GetValueFromConfig(GitProcess git, string configName, bool localOnly) + { + GitProcess.ConfigResult result = + localOnly + ? git.GetFromLocalConfig(configName) + : git.GetFromConfig(configName); + + if (!result.TryParseAsString(out string value, out string error)) + { + throw new InvalidRepoException(error); + } + + return value; + } + + private static string GetDeprecatedCacheConfigSettingName(Enlistment enlistment) + { + string sectionUrl = + enlistment.RepoUrl.ToLowerInvariant() + .Replace("https://", string.Empty) + .Replace("http://", string.Empty) + .Replace('/', '.'); + + return ScalarConstants.GitConfig.ScalarPrefix + sectionUrl + ScalarConstants.GitConfig.DeprecatedCacheEndpointSuffix; + } + + private CacheServerInfo CreateNone() + { + return new CacheServerInfo(this.enlistment.RepoUrl, CacheServerInfo.ReservedNames.None); + } + + private bool InputMatchesEnlistmentUrl(string userInput) + { + return this.enlistment.RepoUrl.TrimEnd('/').Equals(userInput.TrimEnd('/'), StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/Scalar.Common/Http/ConfigHttpRequestor.cs b/Scalar.Common/Http/ConfigHttpRequestor.cs index 124459e4a2..3867b5e7a7 100644 --- a/Scalar.Common/Http/ConfigHttpRequestor.cs +++ b/Scalar.Common/Http/ConfigHttpRequestor.cs @@ -1,106 +1,106 @@ -using Newtonsoft.Json; -using Scalar.Common.Tracing; -using System; -using System.Net; -using System.Net.Http; -using System.Threading; - -namespace Scalar.Common.Http -{ - public class ConfigHttpRequestor : HttpRequestor - { - private readonly string repoUrl; - - public ConfigHttpRequestor(ITracer tracer, Enlistment enlistment, RetryConfig retryConfig) - : base(tracer, retryConfig, enlistment) - { - this.repoUrl = enlistment.RepoUrl; - } - - public bool TryQueryScalarConfig(bool logErrors, out ServerScalarConfig serverScalarConfig, out HttpStatusCode? httpStatus, out string errorMessage) - { - serverScalarConfig = null; - httpStatus = null; - errorMessage = null; - - Uri scalarConfigEndpoint; - string scalarConfigEndpointString = this.repoUrl + ScalarConstants.Endpoints.ScalarConfig; - try - { - scalarConfigEndpoint = new Uri(scalarConfigEndpointString); - } - catch (UriFormatException e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Method", nameof(this.TryQueryScalarConfig)); - metadata.Add("Exception", e.ToString()); - metadata.Add("Url", scalarConfigEndpointString); - this.Tracer.RelatedError(metadata, "UriFormatException when constructing Uri", Keywords.Network); - - return false; - } - - long requestId = HttpRequestor.GetNewRequestId(); - RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); - - if (logErrors) - { - retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryGvfsConfig"); - } - - RetryWrapper.InvocationResult output = retrier.Invoke( - tryCount => - { - using (GitEndPointResponseData response = this.SendRequest( - requestId, - scalarConfigEndpoint, - HttpMethod.Get, - requestContent: null, - cancellationToken: CancellationToken.None)) - { - if (response.HasErrors) - { - return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); - } - - try - { - string configString = response.RetryableReadToEnd(); - ServerScalarConfig config = JsonConvert.DeserializeObject(configString); - return new RetryWrapper.CallbackResult(config); - } - catch (JsonReaderException e) - { - return new RetryWrapper.CallbackResult(e, shouldRetry: false); - } - } - }); - - if (output.Succeeded) - { - serverScalarConfig = output.Result; - httpStatus = HttpStatusCode.OK; - return true; - } - - GitObjectsHttpException httpException = output.Error as GitObjectsHttpException; - if (httpException != null) - { - httpStatus = httpException.StatusCode; - errorMessage = httpException.Message; - } - - if (logErrors) - { - this.Tracer.RelatedError( - new EventMetadata - { - { "Exception", output.Error.ToString() } - }, - $"{nameof(this.TryQueryScalarConfig)} failed"); - } - - return false; - } - } -} +using Newtonsoft.Json; +using Scalar.Common.Tracing; +using System; +using System.Net; +using System.Net.Http; +using System.Threading; + +namespace Scalar.Common.Http +{ + public class ConfigHttpRequestor : HttpRequestor + { + private readonly string repoUrl; + + public ConfigHttpRequestor(ITracer tracer, Enlistment enlistment, RetryConfig retryConfig) + : base(tracer, retryConfig, enlistment) + { + this.repoUrl = enlistment.RepoUrl; + } + + public bool TryQueryScalarConfig(bool logErrors, out ServerScalarConfig serverScalarConfig, out HttpStatusCode? httpStatus, out string errorMessage) + { + serverScalarConfig = null; + httpStatus = null; + errorMessage = null; + + Uri scalarConfigEndpoint; + string scalarConfigEndpointString = this.repoUrl + ScalarConstants.Endpoints.ScalarConfig; + try + { + scalarConfigEndpoint = new Uri(scalarConfigEndpointString); + } + catch (UriFormatException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Method", nameof(this.TryQueryScalarConfig)); + metadata.Add("Exception", e.ToString()); + metadata.Add("Url", scalarConfigEndpointString); + this.Tracer.RelatedError(metadata, "UriFormatException when constructing Uri", Keywords.Network); + + return false; + } + + long requestId = HttpRequestor.GetNewRequestId(); + RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); + + if (logErrors) + { + retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryGvfsConfig"); + } + + RetryWrapper.InvocationResult output = retrier.Invoke( + tryCount => + { + using (GitEndPointResponseData response = this.SendRequest( + requestId, + scalarConfigEndpoint, + HttpMethod.Get, + requestContent: null, + cancellationToken: CancellationToken.None)) + { + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); + } + + try + { + string configString = response.RetryableReadToEnd(); + ServerScalarConfig config = JsonConvert.DeserializeObject(configString); + return new RetryWrapper.CallbackResult(config); + } + catch (JsonReaderException e) + { + return new RetryWrapper.CallbackResult(e, shouldRetry: false); + } + } + }); + + if (output.Succeeded) + { + serverScalarConfig = output.Result; + httpStatus = HttpStatusCode.OK; + return true; + } + + GitObjectsHttpException httpException = output.Error as GitObjectsHttpException; + if (httpException != null) + { + httpStatus = httpException.StatusCode; + errorMessage = httpException.Message; + } + + if (logErrors) + { + this.Tracer.RelatedError( + new EventMetadata + { + { "Exception", output.Error.ToString() } + }, + $"{nameof(this.TryQueryScalarConfig)} failed"); + } + + return false; + } + } +} diff --git a/Scalar.Common/Http/GitEndPointResponseData.cs b/Scalar.Common/Http/GitEndPointResponseData.cs index 1f0eb36079..daf542812e 100644 --- a/Scalar.Common/Http/GitEndPointResponseData.cs +++ b/Scalar.Common/Http/GitEndPointResponseData.cs @@ -1,163 +1,163 @@ -using Scalar.Common.Git; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; - -namespace Scalar.Common.Http -{ - public class GitEndPointResponseData : IDisposable - { - private HttpResponseMessage message; - private Action onResponseDisposed; - - /// - /// Constructor used when GitEndPointResponseData contains an error response - /// - public GitEndPointResponseData(HttpStatusCode statusCode, Exception error, bool shouldRetry, HttpResponseMessage message, Action onResponseDisposed) - { - this.StatusCode = statusCode; - this.Error = error; - this.ShouldRetry = shouldRetry; - this.message = message; - this.onResponseDisposed = onResponseDisposed; - } - - /// - /// Constructor used when GitEndPointResponseData contains a successful response - /// - public GitEndPointResponseData(HttpStatusCode statusCode, string contentType, Stream responseStream, HttpResponseMessage message, Action onResponseDisposed) - : this(statusCode, null, false, message, onResponseDisposed) - { - this.Stream = responseStream; - this.ContentType = MapContentType(contentType); - } - - public Exception Error { get; } - - public bool ShouldRetry { get; } - - public HttpStatusCode StatusCode { get; } - - public Stream Stream { get; private set; } - - public bool HasErrors - { - get { return this.StatusCode != HttpStatusCode.OK; } - } - - public GitObjectContentType ContentType { get; } - - /// - /// Reads the underlying stream until it ends returning all content as a string. - /// - public string RetryableReadToEnd() - { - if (this.Stream == null) - { - throw new RetryableException("Stream is null (this could be a result of network flakiness), retrying."); - } - - if (!this.Stream.CanRead) - { - throw new RetryableException("Stream is not readable (this could be a result of network flakiness), retrying."); - } - - using (StreamReader contentStreamReader = new StreamReader(this.Stream)) - { - try - { - return contentStreamReader.ReadToEnd(); - } - catch (Exception ex) - { - // All exceptions potentially from network should be retried - throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); - } - } - } - - /// - /// Reads the stream until it ends returning each line as a string. - /// - public List RetryableReadAllLines() - { - using (StreamReader contentStreamReader = new StreamReader(this.Stream)) - { - List output = new List(); - - while (true) - { - string line; - try - { - if (contentStreamReader.EndOfStream) - { - break; - } - - line = contentStreamReader.ReadLine(); - } - catch (Exception ex) - { - // All exceptions potentially from network should be retried - throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); - } - - output.Add(line); - } - - return output; - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - public void Dispose(bool disposing) - { - if (disposing) - { - if (this.message != null) - { - this.message.Dispose(); - this.message = null; - } - - if (this.Stream != null) - { - this.Stream.Dispose(); - this.Stream = null; - } - - if (this.onResponseDisposed != null) - { - this.onResponseDisposed(); - this.onResponseDisposed = null; - } - } - } - - /// - /// Convert from a string-based Content-Type to - /// - private static GitObjectContentType MapContentType(string contentType) - { - switch (contentType) - { - case ScalarConstants.MediaTypes.LooseObjectMediaType: - return GitObjectContentType.LooseObject; - case ScalarConstants.MediaTypes.CustomLooseObjectsMediaType: - return GitObjectContentType.BatchedLooseObjects; - case ScalarConstants.MediaTypes.PackFileMediaType: - return GitObjectContentType.PackFile; - default: - return GitObjectContentType.None; - } - } - } -} +using Scalar.Common.Git; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; + +namespace Scalar.Common.Http +{ + public class GitEndPointResponseData : IDisposable + { + private HttpResponseMessage message; + private Action onResponseDisposed; + + /// + /// Constructor used when GitEndPointResponseData contains an error response + /// + public GitEndPointResponseData(HttpStatusCode statusCode, Exception error, bool shouldRetry, HttpResponseMessage message, Action onResponseDisposed) + { + this.StatusCode = statusCode; + this.Error = error; + this.ShouldRetry = shouldRetry; + this.message = message; + this.onResponseDisposed = onResponseDisposed; + } + + /// + /// Constructor used when GitEndPointResponseData contains a successful response + /// + public GitEndPointResponseData(HttpStatusCode statusCode, string contentType, Stream responseStream, HttpResponseMessage message, Action onResponseDisposed) + : this(statusCode, null, false, message, onResponseDisposed) + { + this.Stream = responseStream; + this.ContentType = MapContentType(contentType); + } + + public Exception Error { get; } + + public bool ShouldRetry { get; } + + public HttpStatusCode StatusCode { get; } + + public Stream Stream { get; private set; } + + public bool HasErrors + { + get { return this.StatusCode != HttpStatusCode.OK; } + } + + public GitObjectContentType ContentType { get; } + + /// + /// Reads the underlying stream until it ends returning all content as a string. + /// + public string RetryableReadToEnd() + { + if (this.Stream == null) + { + throw new RetryableException("Stream is null (this could be a result of network flakiness), retrying."); + } + + if (!this.Stream.CanRead) + { + throw new RetryableException("Stream is not readable (this could be a result of network flakiness), retrying."); + } + + using (StreamReader contentStreamReader = new StreamReader(this.Stream)) + { + try + { + return contentStreamReader.ReadToEnd(); + } + catch (Exception ex) + { + // All exceptions potentially from network should be retried + throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); + } + } + } + + /// + /// Reads the stream until it ends returning each line as a string. + /// + public List RetryableReadAllLines() + { + using (StreamReader contentStreamReader = new StreamReader(this.Stream)) + { + List output = new List(); + + while (true) + { + string line; + try + { + if (contentStreamReader.EndOfStream) + { + break; + } + + line = contentStreamReader.ReadLine(); + } + catch (Exception ex) + { + // All exceptions potentially from network should be retried + throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); + } + + output.Add(line); + } + + return output; + } + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + public void Dispose(bool disposing) + { + if (disposing) + { + if (this.message != null) + { + this.message.Dispose(); + this.message = null; + } + + if (this.Stream != null) + { + this.Stream.Dispose(); + this.Stream = null; + } + + if (this.onResponseDisposed != null) + { + this.onResponseDisposed(); + this.onResponseDisposed = null; + } + } + } + + /// + /// Convert from a string-based Content-Type to + /// + private static GitObjectContentType MapContentType(string contentType) + { + switch (contentType) + { + case ScalarConstants.MediaTypes.LooseObjectMediaType: + return GitObjectContentType.LooseObject; + case ScalarConstants.MediaTypes.CustomLooseObjectsMediaType: + return GitObjectContentType.BatchedLooseObjects; + case ScalarConstants.MediaTypes.PackFileMediaType: + return GitObjectContentType.PackFile; + default: + return GitObjectContentType.None; + } + } + } +} diff --git a/Scalar.Common/Http/GitObjectsHttpException.cs b/Scalar.Common/Http/GitObjectsHttpException.cs index 455dea81f8..afc4b499b9 100644 --- a/Scalar.Common/Http/GitObjectsHttpException.cs +++ b/Scalar.Common/Http/GitObjectsHttpException.cs @@ -1,15 +1,15 @@ -using System; -using System.Net; - -namespace Scalar.Common.Http -{ - public class GitObjectsHttpException : Exception - { - public GitObjectsHttpException(HttpStatusCode statusCode, string ex) : base(ex) - { - this.StatusCode = statusCode; - } - - public HttpStatusCode StatusCode { get; } - } -} +using System; +using System.Net; + +namespace Scalar.Common.Http +{ + public class GitObjectsHttpException : Exception + { + public GitObjectsHttpException(HttpStatusCode statusCode, string ex) : base(ex) + { + this.StatusCode = statusCode; + } + + public HttpStatusCode StatusCode { get; } + } +} diff --git a/Scalar.Common/Http/GitObjectsHttpRequestor.cs b/Scalar.Common/Http/GitObjectsHttpRequestor.cs index d65595a2ad..1316c520c2 100644 --- a/Scalar.Common/Http/GitObjectsHttpRequestor.cs +++ b/Scalar.Common/Http/GitObjectsHttpRequestor.cs @@ -1,374 +1,374 @@ -using Newtonsoft.Json; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; - -namespace Scalar.Common.Http -{ - public class GitObjectsHttpRequestor : HttpRequestor - { - private static readonly MediaTypeWithQualityHeaderValue CustomLooseObjectsHeader - = new MediaTypeWithQualityHeaderValue(ScalarConstants.MediaTypes.CustomLooseObjectsMediaType); - - private Enlistment enlistment; - - private DateTime nextCacheServerAttemptTime = DateTime.Now; - - public GitObjectsHttpRequestor(ITracer tracer, Enlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig) - : base(tracer, retryConfig, enlistment) - { - this.enlistment = enlistment; - this.CacheServer = cacheServer; - } - - public CacheServerInfo CacheServer { get; private set; } - - public virtual List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) - { - long requestId = HttpRequestor.GetNewRequestId(); - - string objectIdsJson = ToJsonList(objectIds); - Uri cacheServerEndpoint = new Uri(this.CacheServer.SizesEndpointUrl); - Uri originEndpoint = new Uri(this.enlistment.RepoUrl + ScalarConstants.Endpoints.ScalarSizes); - - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", requestId); - int objectIdCount = objectIds.Count(); - if (objectIdCount > 10) - { - metadata.Add("ObjectIdCount", objectIdCount); - } - else - { - metadata.Add("ObjectIdJson", objectIdsJson); - } - - this.Tracer.RelatedEvent(EventLevel.Informational, "QueryFileSizes", metadata, Keywords.Network); - - RetryWrapper> retrier = new RetryWrapper>(this.RetryConfig.MaxAttempts, cancellationToken); - retrier.OnFailure += RetryWrapper>.StandardErrorHandler(this.Tracer, requestId, "QueryFileSizes"); - - RetryWrapper>.InvocationResult requestTask = retrier.Invoke( - tryCount => - { - Uri scalarEndpoint; - if (this.nextCacheServerAttemptTime < DateTime.Now) - { - scalarEndpoint = cacheServerEndpoint; - } - else - { - scalarEndpoint = originEndpoint; - } - - using (GitEndPointResponseData response = this.SendRequest(requestId, scalarEndpoint, HttpMethod.Post, objectIdsJson, cancellationToken)) - { - if (response.StatusCode == HttpStatusCode.NotFound) - { - this.nextCacheServerAttemptTime = DateTime.Now.AddDays(1); - return new RetryWrapper>.CallbackResult(response.Error, true); - } - - if (response.HasErrors) - { - return new RetryWrapper>.CallbackResult(response.Error, response.ShouldRetry); - } - - string objectSizesString = response.RetryableReadToEnd(); - List objectSizes = JsonConvert.DeserializeObject>(objectSizesString); - return new RetryWrapper>.CallbackResult(objectSizes); - } - }); - - return requestTask.Result ?? new List(0); - } - - public virtual GitRefs QueryInfoRefs(string branch) - { - long requestId = HttpRequestor.GetNewRequestId(); - - Uri infoRefsEndpoint; - try - { - infoRefsEndpoint = new Uri(this.enlistment.RepoUrl + ScalarConstants.Endpoints.InfoRefs); - } - catch (UriFormatException) - { - return null; - } - - RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); - retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryInfoRefs"); - - RetryWrapper.InvocationResult output = retrier.Invoke( - tryCount => - { - using (GitEndPointResponseData response = this.SendRequest( - requestId, - infoRefsEndpoint, - HttpMethod.Get, - requestContent: null, - cancellationToken: CancellationToken.None)) - { - if (response.HasErrors) - { - return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); - } - - List infoRefsResponse = response.RetryableReadAllLines(); - return new RetryWrapper.CallbackResult(new GitRefs(infoRefsResponse, branch)); - } - }); - - return output.Result; - } - - public virtual RetryWrapper.InvocationResult TryDownloadLooseObject( - string objectId, - bool retryOnFailure, - CancellationToken cancellationToken, - string requestSource, - Func.CallbackResult> onSuccess) - { - long requestId = HttpRequestor.GetNewRequestId(); - EventMetadata metadata = new EventMetadata(); - metadata.Add("objectId", objectId); - metadata.Add("retryOnFailure", retryOnFailure); - metadata.Add("requestId", requestId); - metadata.Add("requestSource", requestSource); - this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadLooseObject", metadata, Keywords.Network); - - return this.TrySendProtocolRequest( - requestId, - onSuccess, - eArgs => this.HandleDownloadAndSaveObjectError(retryOnFailure, requestId, eArgs), - HttpMethod.Get, - new Uri(this.CacheServer.ObjectsEndpointUrl + "/" + objectId), - cancellationToken, - requestBody: null, - acceptType: null, - retryOnFailure: retryOnFailure); - } - - public virtual RetryWrapper.InvocationResult TryDownloadObjects( - Func> objectIdGenerator, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - // We pass the query generator in as a function because we don't want the consumer to know about JSON or network retry logic, - // but we still want the consumer to be able to change the query on each retry if we fail during their onSuccess handler. - long requestId = HttpRequestor.GetNewRequestId(); - return this.TrySendProtocolRequest( - requestId, - onSuccess, - onFailure, - HttpMethod.Post, - new Uri(this.CacheServer.ObjectsEndpointUrl), - CancellationToken.None, - () => this.ObjectIdsJsonGenerator(requestId, objectIdGenerator), - preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); - } - - public virtual RetryWrapper.InvocationResult TryDownloadObjects( - IEnumerable objectIds, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - long requestId = HttpRequestor.GetNewRequestId(); - - string objectIdsJson = CreateObjectIdJson(objectIds); - int objectCount = objectIds.Count(); - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", requestId); - if (objectCount < 10) - { - metadata.Add("ObjectIds", string.Join(", ", objectIds)); - } - else - { - metadata.Add("ObjectIdCount", objectCount); - } - - this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); - - return this.TrySendProtocolRequest( - requestId, - onSuccess, - onFailure, - HttpMethod.Post, - new Uri(this.CacheServer.ObjectsEndpointUrl), - CancellationToken.None, - objectIdsJson, - preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); - } - - public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( - long requestId, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - HttpMethod method, - Uri endPoint, - CancellationToken cancellationToken, - string requestBody = null, - MediaTypeWithQualityHeaderValue acceptType = null, - bool retryOnFailure = true) - { - return this.TrySendProtocolRequest( - requestId, - onSuccess, - onFailure, - method, - endPoint, - cancellationToken, - () => requestBody, - acceptType, - retryOnFailure); - } - - public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( - long requestId, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - HttpMethod method, - Uri endPoint, - CancellationToken cancellationToken, - Func requestBodyGenerator, - MediaTypeWithQualityHeaderValue acceptType = null, - bool retryOnFailure = true) - { - return this.TrySendProtocolRequest( - requestId, - onSuccess, - onFailure, - method, - () => endPoint, - requestBodyGenerator, - cancellationToken, - acceptType, - retryOnFailure); - } - - public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( - long requestId, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - HttpMethod method, - Func endPointGenerator, - Func requestBodyGenerator, - CancellationToken cancellationToken, - MediaTypeWithQualityHeaderValue acceptType = null, - bool retryOnFailure = true) - { - RetryWrapper retrier = new RetryWrapper( - retryOnFailure ? this.RetryConfig.MaxAttempts : 1, - cancellationToken); - if (onFailure != null) - { - retrier.OnFailure += onFailure; - } - - return retrier.Invoke( - tryCount => - { - using (GitEndPointResponseData response = this.SendRequest( - requestId, - endPointGenerator(), - method, - requestBodyGenerator(), - cancellationToken, - acceptType)) - { - if (response.HasErrors) - { - return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry, new GitObjectTaskResult(response.StatusCode)); - } - - return onSuccess(tryCount, response); - } - }); - } - - private static string ToJsonList(IEnumerable strings) - { - return "[\"" + string.Join("\",\"", strings) + "\"]"; - } - - private static string CreateObjectIdJson(IEnumerable strings) - { - return "{\"commitDepth\": 1, \"objectIds\":" + ToJsonList(strings) + "}"; - } - - private void HandleDownloadAndSaveObjectError(bool retryOnFailure, long requestId, RetryWrapper.ErrorEventArgs errorArgs) - { - // Silence logging 404's for object downloads. They are far more likely to be git checking for the - // previous existence of a new object than a truly missing object. - GitObjectsHttpException ex = errorArgs.Error as GitObjectsHttpException; - if (ex != null && ex.StatusCode == HttpStatusCode.NotFound) - { - return; - } - - // If the caller has requested that we not retry on failure, caller must handle logging errors - bool forceLogAsWarning = !retryOnFailure; - RetryWrapper.StandardErrorHandler(this.Tracer, requestId, nameof(this.TryDownloadLooseObject), forceLogAsWarning)(errorArgs); - } - - private string ObjectIdsJsonGenerator(long requestId, Func> objectIdGenerator) - { - IEnumerable objectIds = objectIdGenerator(); - string objectIdsJson = CreateObjectIdJson(objectIds); - int objectCount = objectIds.Count(); - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", requestId); - if (objectCount < 10) - { - metadata.Add("ObjectIds", string.Join(", ", objectIds)); - } - else - { - metadata.Add("ObjectIdCount", objectCount); - } - - this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); - return objectIdsJson; - } - - public class GitObjectSize - { - public readonly string Id; - public readonly long Size; - - [JsonConstructor] - public GitObjectSize(string id, long size) - { - this.Id = id; - this.Size = size; - } - } - - public class GitObjectTaskResult - { - public GitObjectTaskResult(bool success) - { - this.Success = success; - } - - public GitObjectTaskResult(HttpStatusCode statusCode) - : this(statusCode == HttpStatusCode.OK) - { - this.HttpStatusCodeResult = statusCode; - } - - public bool Success { get; } - public HttpStatusCode HttpStatusCodeResult { get; } - } - } +using Newtonsoft.Json; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; + +namespace Scalar.Common.Http +{ + public class GitObjectsHttpRequestor : HttpRequestor + { + private static readonly MediaTypeWithQualityHeaderValue CustomLooseObjectsHeader + = new MediaTypeWithQualityHeaderValue(ScalarConstants.MediaTypes.CustomLooseObjectsMediaType); + + private Enlistment enlistment; + + private DateTime nextCacheServerAttemptTime = DateTime.Now; + + public GitObjectsHttpRequestor(ITracer tracer, Enlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig) + : base(tracer, retryConfig, enlistment) + { + this.enlistment = enlistment; + this.CacheServer = cacheServer; + } + + public CacheServerInfo CacheServer { get; private set; } + + public virtual List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) + { + long requestId = HttpRequestor.GetNewRequestId(); + + string objectIdsJson = ToJsonList(objectIds); + Uri cacheServerEndpoint = new Uri(this.CacheServer.SizesEndpointUrl); + Uri originEndpoint = new Uri(this.enlistment.RepoUrl + ScalarConstants.Endpoints.ScalarSizes); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", requestId); + int objectIdCount = objectIds.Count(); + if (objectIdCount > 10) + { + metadata.Add("ObjectIdCount", objectIdCount); + } + else + { + metadata.Add("ObjectIdJson", objectIdsJson); + } + + this.Tracer.RelatedEvent(EventLevel.Informational, "QueryFileSizes", metadata, Keywords.Network); + + RetryWrapper> retrier = new RetryWrapper>(this.RetryConfig.MaxAttempts, cancellationToken); + retrier.OnFailure += RetryWrapper>.StandardErrorHandler(this.Tracer, requestId, "QueryFileSizes"); + + RetryWrapper>.InvocationResult requestTask = retrier.Invoke( + tryCount => + { + Uri scalarEndpoint; + if (this.nextCacheServerAttemptTime < DateTime.Now) + { + scalarEndpoint = cacheServerEndpoint; + } + else + { + scalarEndpoint = originEndpoint; + } + + using (GitEndPointResponseData response = this.SendRequest(requestId, scalarEndpoint, HttpMethod.Post, objectIdsJson, cancellationToken)) + { + if (response.StatusCode == HttpStatusCode.NotFound) + { + this.nextCacheServerAttemptTime = DateTime.Now.AddDays(1); + return new RetryWrapper>.CallbackResult(response.Error, true); + } + + if (response.HasErrors) + { + return new RetryWrapper>.CallbackResult(response.Error, response.ShouldRetry); + } + + string objectSizesString = response.RetryableReadToEnd(); + List objectSizes = JsonConvert.DeserializeObject>(objectSizesString); + return new RetryWrapper>.CallbackResult(objectSizes); + } + }); + + return requestTask.Result ?? new List(0); + } + + public virtual GitRefs QueryInfoRefs(string branch) + { + long requestId = HttpRequestor.GetNewRequestId(); + + Uri infoRefsEndpoint; + try + { + infoRefsEndpoint = new Uri(this.enlistment.RepoUrl + ScalarConstants.Endpoints.InfoRefs); + } + catch (UriFormatException) + { + return null; + } + + RetryWrapper retrier = new RetryWrapper(this.RetryConfig.MaxAttempts, CancellationToken.None); + retrier.OnFailure += RetryWrapper.StandardErrorHandler(this.Tracer, requestId, "QueryInfoRefs"); + + RetryWrapper.InvocationResult output = retrier.Invoke( + tryCount => + { + using (GitEndPointResponseData response = this.SendRequest( + requestId, + infoRefsEndpoint, + HttpMethod.Get, + requestContent: null, + cancellationToken: CancellationToken.None)) + { + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry); + } + + List infoRefsResponse = response.RetryableReadAllLines(); + return new RetryWrapper.CallbackResult(new GitRefs(infoRefsResponse, branch)); + } + }); + + return output.Result; + } + + public virtual RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + bool retryOnFailure, + CancellationToken cancellationToken, + string requestSource, + Func.CallbackResult> onSuccess) + { + long requestId = HttpRequestor.GetNewRequestId(); + EventMetadata metadata = new EventMetadata(); + metadata.Add("objectId", objectId); + metadata.Add("retryOnFailure", retryOnFailure); + metadata.Add("requestId", requestId); + metadata.Add("requestSource", requestSource); + this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadLooseObject", metadata, Keywords.Network); + + return this.TrySendProtocolRequest( + requestId, + onSuccess, + eArgs => this.HandleDownloadAndSaveObjectError(retryOnFailure, requestId, eArgs), + HttpMethod.Get, + new Uri(this.CacheServer.ObjectsEndpointUrl + "/" + objectId), + cancellationToken, + requestBody: null, + acceptType: null, + retryOnFailure: retryOnFailure); + } + + public virtual RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + // We pass the query generator in as a function because we don't want the consumer to know about JSON or network retry logic, + // but we still want the consumer to be able to change the query on each retry if we fail during their onSuccess handler. + long requestId = HttpRequestor.GetNewRequestId(); + return this.TrySendProtocolRequest( + requestId, + onSuccess, + onFailure, + HttpMethod.Post, + new Uri(this.CacheServer.ObjectsEndpointUrl), + CancellationToken.None, + () => this.ObjectIdsJsonGenerator(requestId, objectIdGenerator), + preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); + } + + public virtual RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + long requestId = HttpRequestor.GetNewRequestId(); + + string objectIdsJson = CreateObjectIdJson(objectIds); + int objectCount = objectIds.Count(); + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", requestId); + if (objectCount < 10) + { + metadata.Add("ObjectIds", string.Join(", ", objectIds)); + } + else + { + metadata.Add("ObjectIdCount", objectCount); + } + + this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); + + return this.TrySendProtocolRequest( + requestId, + onSuccess, + onFailure, + HttpMethod.Post, + new Uri(this.CacheServer.ObjectsEndpointUrl), + CancellationToken.None, + objectIdsJson, + preferBatchedLooseObjects ? CustomLooseObjectsHeader : null); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + long requestId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Uri endPoint, + CancellationToken cancellationToken, + string requestBody = null, + MediaTypeWithQualityHeaderValue acceptType = null, + bool retryOnFailure = true) + { + return this.TrySendProtocolRequest( + requestId, + onSuccess, + onFailure, + method, + endPoint, + cancellationToken, + () => requestBody, + acceptType, + retryOnFailure); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + long requestId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Uri endPoint, + CancellationToken cancellationToken, + Func requestBodyGenerator, + MediaTypeWithQualityHeaderValue acceptType = null, + bool retryOnFailure = true) + { + return this.TrySendProtocolRequest( + requestId, + onSuccess, + onFailure, + method, + () => endPoint, + requestBodyGenerator, + cancellationToken, + acceptType, + retryOnFailure); + } + + public virtual RetryWrapper.InvocationResult TrySendProtocolRequest( + long requestId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + HttpMethod method, + Func endPointGenerator, + Func requestBodyGenerator, + CancellationToken cancellationToken, + MediaTypeWithQualityHeaderValue acceptType = null, + bool retryOnFailure = true) + { + RetryWrapper retrier = new RetryWrapper( + retryOnFailure ? this.RetryConfig.MaxAttempts : 1, + cancellationToken); + if (onFailure != null) + { + retrier.OnFailure += onFailure; + } + + return retrier.Invoke( + tryCount => + { + using (GitEndPointResponseData response = this.SendRequest( + requestId, + endPointGenerator(), + method, + requestBodyGenerator(), + cancellationToken, + acceptType)) + { + if (response.HasErrors) + { + return new RetryWrapper.CallbackResult(response.Error, response.ShouldRetry, new GitObjectTaskResult(response.StatusCode)); + } + + return onSuccess(tryCount, response); + } + }); + } + + private static string ToJsonList(IEnumerable strings) + { + return "[\"" + string.Join("\",\"", strings) + "\"]"; + } + + private static string CreateObjectIdJson(IEnumerable strings) + { + return "{\"commitDepth\": 1, \"objectIds\":" + ToJsonList(strings) + "}"; + } + + private void HandleDownloadAndSaveObjectError(bool retryOnFailure, long requestId, RetryWrapper.ErrorEventArgs errorArgs) + { + // Silence logging 404's for object downloads. They are far more likely to be git checking for the + // previous existence of a new object than a truly missing object. + GitObjectsHttpException ex = errorArgs.Error as GitObjectsHttpException; + if (ex != null && ex.StatusCode == HttpStatusCode.NotFound) + { + return; + } + + // If the caller has requested that we not retry on failure, caller must handle logging errors + bool forceLogAsWarning = !retryOnFailure; + RetryWrapper.StandardErrorHandler(this.Tracer, requestId, nameof(this.TryDownloadLooseObject), forceLogAsWarning)(errorArgs); + } + + private string ObjectIdsJsonGenerator(long requestId, Func> objectIdGenerator) + { + IEnumerable objectIds = objectIdGenerator(); + string objectIdsJson = CreateObjectIdJson(objectIds); + int objectCount = objectIds.Count(); + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", requestId); + if (objectCount < 10) + { + metadata.Add("ObjectIds", string.Join(", ", objectIds)); + } + else + { + metadata.Add("ObjectIdCount", objectCount); + } + + this.Tracer.RelatedEvent(EventLevel.Informational, "DownloadObjects", metadata, Keywords.Network); + return objectIdsJson; + } + + public class GitObjectSize + { + public readonly string Id; + public readonly long Size; + + [JsonConstructor] + public GitObjectSize(string id, long size) + { + this.Id = id; + this.Size = size; + } + } + + public class GitObjectTaskResult + { + public GitObjectTaskResult(bool success) + { + this.Success = success; + } + + public GitObjectTaskResult(HttpStatusCode statusCode) + : this(statusCode == HttpStatusCode.OK) + { + this.HttpStatusCodeResult = statusCode; + } + + public bool Success { get; } + public HttpStatusCode HttpStatusCodeResult { get; } + } + } } diff --git a/Scalar.Common/Http/HttpRequestor.cs b/Scalar.Common/Http/HttpRequestor.cs index c37fad5262..9707635073 100644 --- a/Scalar.Common/Http/HttpRequestor.cs +++ b/Scalar.Common/Http/HttpRequestor.cs @@ -1,281 +1,281 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.Common.Http -{ - public abstract class HttpRequestor : IDisposable - { - private static long requestCount = 0; - private static SemaphoreSlim availableConnections; - - private readonly ProductInfoHeaderValue userAgentHeader; - - private readonly GitAuthentication authentication; - - private HttpClient client; - - static HttpRequestor() - { - ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; - ServicePointManager.DefaultConnectionLimit = Environment.ProcessorCount; - availableConnections = new SemaphoreSlim(ServicePointManager.DefaultConnectionLimit); - } - - protected HttpRequestor(ITracer tracer, RetryConfig retryConfig, Enlistment enlistment) - { - this.RetryConfig = retryConfig; - - this.authentication = enlistment.Authentication; - - this.Tracer = tracer; - - HttpClientHandler httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; - - this.authentication.ConfigureHttpClientHandlerSslIfNeeded(this.Tracer, httpClientHandler, enlistment.CreateGitProcess()); - - this.client = new HttpClient(httpClientHandler) - { - Timeout = retryConfig.Timeout - }; - - this.userAgentHeader = new ProductInfoHeaderValue(ProcessHelper.GetEntryClassName(), ProcessHelper.GetCurrentProcessVersion()); - } - - public RetryConfig RetryConfig { get; } - - protected ITracer Tracer { get; } - - public static long GetNewRequestId() - { - return Interlocked.Increment(ref requestCount); - } - - public void Dispose() - { - if (this.client != null) - { - this.client.Dispose(); - this.client = null; - } - } - - protected GitEndPointResponseData SendRequest( - long requestId, - Uri requestUri, - HttpMethod httpMethod, - string requestContent, - CancellationToken cancellationToken, - MediaTypeWithQualityHeaderValue acceptType = null) - { - string authString = null; - string errorMessage; - if (!this.authentication.IsAnonymous && - !this.authentication.TryGetCredentials(this.Tracer, out authString, out errorMessage)) - { - return new GitEndPointResponseData( - HttpStatusCode.Unauthorized, - new GitObjectsHttpException(HttpStatusCode.Unauthorized, errorMessage), - shouldRetry: true, - message: null, - onResponseDisposed: null); - } - - HttpRequestMessage request = new HttpRequestMessage(httpMethod, requestUri); - - // By default, VSTS auth failures result in redirects to SPS to reauthenticate. - // To provide more consistent behavior when using the GCM, have them send us 401s instead - request.Headers.Add("X-TFS-FedAuthRedirect", "Suppress"); - - request.Headers.UserAgent.Add(this.userAgentHeader); - - if (!this.authentication.IsAnonymous) - { - request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString); - } - - if (acceptType != null) - { - request.Headers.Accept.Add(acceptType); - } - - if (requestContent != null) - { - request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json"); - } - - EventMetadata responseMetadata = new EventMetadata(); - responseMetadata.Add("RequestId", requestId); - responseMetadata.Add("availableConnections", availableConnections.CurrentCount); - - Stopwatch requestStopwatch = Stopwatch.StartNew(); - availableConnections.Wait(cancellationToken); - TimeSpan connectionWaitTime = requestStopwatch.Elapsed; - - TimeSpan responseWaitTime = default(TimeSpan); - GitEndPointResponseData gitEndPointResponseData = null; - HttpResponseMessage response = null; - - try - { - requestStopwatch.Restart(); - - try - { - response = this.client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).GetAwaiter().GetResult(); - } - finally - { - responseWaitTime = requestStopwatch.Elapsed; - } - - responseMetadata.Add("CacheName", GetSingleHeaderOrEmpty(response.Headers, "X-Cache-Name")); - responseMetadata.Add("StatusCode", response.StatusCode); - - if (response.StatusCode == HttpStatusCode.OK) - { - string contentType = GetSingleHeaderOrEmpty(response.Content.Headers, "Content-Type"); - responseMetadata.Add("ContentType", contentType); - - if (!this.authentication.IsAnonymous) - { - this.authentication.ApproveCredentials(this.Tracer, authString); - } - - Stream responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); - - gitEndPointResponseData = new GitEndPointResponseData( - response.StatusCode, - contentType, - responseStream, - message: response, - onResponseDisposed: () => availableConnections.Release()); - } - else - { - errorMessage = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - int statusInt = (int)response.StatusCode; - - bool shouldRetry = ShouldRetry(response.StatusCode); - - if (response.StatusCode == HttpStatusCode.Unauthorized && - this.authentication.IsAnonymous) - { - shouldRetry = false; - errorMessage = "Anonymous request was rejected with a 401"; - } - else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.BadRequest || response.StatusCode == HttpStatusCode.Redirect) - { - this.authentication.RejectCredentials(this.Tracer, authString); - if (!this.authentication.IsBackingOff) - { - errorMessage = string.Format("Server returned error code {0} ({1}). Your PAT may be expired and we are asking for a new one. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); - } - else - { - errorMessage = string.Format("Server returned error code {0} ({1}) after successfully renewing your PAT. You may not have access to this repo. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); - } - } - else - { - errorMessage = string.Format("Server returned error code {0} ({1}). Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); - } - - gitEndPointResponseData = new GitEndPointResponseData( - response.StatusCode, - new GitObjectsHttpException(response.StatusCode, errorMessage), - shouldRetry, - message: response, - onResponseDisposed: () => availableConnections.Release()); - } - } - catch (TaskCanceledException) - { - cancellationToken.ThrowIfCancellationRequested(); - - errorMessage = string.Format("Request to {0} timed out", requestUri); - - gitEndPointResponseData = new GitEndPointResponseData( - HttpStatusCode.RequestTimeout, - new GitObjectsHttpException(HttpStatusCode.RequestTimeout, errorMessage), - shouldRetry: true, - message: response, - onResponseDisposed: () => availableConnections.Release()); - } - catch (HttpRequestException httpRequestException) when (httpRequestException.InnerException is System.Security.Authentication.AuthenticationException) - { - // This exception is thrown on OSX, when user declines to give permission to access certificate - gitEndPointResponseData = new GitEndPointResponseData( - HttpStatusCode.Unauthorized, - httpRequestException.InnerException, - shouldRetry: false, - message: response, - onResponseDisposed: () => availableConnections.Release()); - } - catch (WebException ex) - { - gitEndPointResponseData = new GitEndPointResponseData( - HttpStatusCode.InternalServerError, - ex, - shouldRetry: true, - message: response, - onResponseDisposed: () => availableConnections.Release()); - } - finally - { - responseMetadata.Add("connectionWaitTimeMS", $"{connectionWaitTime.TotalMilliseconds:F4}"); - responseMetadata.Add("responseWaitTimeMS", $"{responseWaitTime.TotalMilliseconds:F4}"); - - this.Tracer.RelatedEvent(EventLevel.Informational, "NetworkResponse", responseMetadata); - - if (gitEndPointResponseData == null) - { - // If gitEndPointResponseData is null there was an unhandled exception - if (response != null) - { - response.Dispose(); - } - - availableConnections.Release(); - } - } - - return gitEndPointResponseData; - } - - private static bool ShouldRetry(HttpStatusCode statusCode) - { - // Retry timeout, Unauthorized, and 5xx errors - int statusInt = (int)statusCode; - if (statusCode == HttpStatusCode.RequestTimeout || - statusCode == HttpStatusCode.Unauthorized || - (statusInt >= 500 && statusInt < 600)) - { - return true; - } - - return false; - } - - private static string GetSingleHeaderOrEmpty(HttpHeaders headers, string headerName) - { - IEnumerable values; - if (headers.TryGetValues(headerName, out values)) - { - return values.First(); - } - - return string.Empty; - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.Common.Http +{ + public abstract class HttpRequestor : IDisposable + { + private static long requestCount = 0; + private static SemaphoreSlim availableConnections; + + private readonly ProductInfoHeaderValue userAgentHeader; + + private readonly GitAuthentication authentication; + + private HttpClient client; + + static HttpRequestor() + { + ServicePointManager.SecurityProtocol = ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12; + ServicePointManager.DefaultConnectionLimit = Environment.ProcessorCount; + availableConnections = new SemaphoreSlim(ServicePointManager.DefaultConnectionLimit); + } + + protected HttpRequestor(ITracer tracer, RetryConfig retryConfig, Enlistment enlistment) + { + this.RetryConfig = retryConfig; + + this.authentication = enlistment.Authentication; + + this.Tracer = tracer; + + HttpClientHandler httpClientHandler = new HttpClientHandler() { UseDefaultCredentials = true }; + + this.authentication.ConfigureHttpClientHandlerSslIfNeeded(this.Tracer, httpClientHandler, enlistment.CreateGitProcess()); + + this.client = new HttpClient(httpClientHandler) + { + Timeout = retryConfig.Timeout + }; + + this.userAgentHeader = new ProductInfoHeaderValue(ProcessHelper.GetEntryClassName(), ProcessHelper.GetCurrentProcessVersion()); + } + + public RetryConfig RetryConfig { get; } + + protected ITracer Tracer { get; } + + public static long GetNewRequestId() + { + return Interlocked.Increment(ref requestCount); + } + + public void Dispose() + { + if (this.client != null) + { + this.client.Dispose(); + this.client = null; + } + } + + protected GitEndPointResponseData SendRequest( + long requestId, + Uri requestUri, + HttpMethod httpMethod, + string requestContent, + CancellationToken cancellationToken, + MediaTypeWithQualityHeaderValue acceptType = null) + { + string authString = null; + string errorMessage; + if (!this.authentication.IsAnonymous && + !this.authentication.TryGetCredentials(this.Tracer, out authString, out errorMessage)) + { + return new GitEndPointResponseData( + HttpStatusCode.Unauthorized, + new GitObjectsHttpException(HttpStatusCode.Unauthorized, errorMessage), + shouldRetry: true, + message: null, + onResponseDisposed: null); + } + + HttpRequestMessage request = new HttpRequestMessage(httpMethod, requestUri); + + // By default, VSTS auth failures result in redirects to SPS to reauthenticate. + // To provide more consistent behavior when using the GCM, have them send us 401s instead + request.Headers.Add("X-TFS-FedAuthRedirect", "Suppress"); + + request.Headers.UserAgent.Add(this.userAgentHeader); + + if (!this.authentication.IsAnonymous) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Basic", authString); + } + + if (acceptType != null) + { + request.Headers.Accept.Add(acceptType); + } + + if (requestContent != null) + { + request.Content = new StringContent(requestContent, Encoding.UTF8, "application/json"); + } + + EventMetadata responseMetadata = new EventMetadata(); + responseMetadata.Add("RequestId", requestId); + responseMetadata.Add("availableConnections", availableConnections.CurrentCount); + + Stopwatch requestStopwatch = Stopwatch.StartNew(); + availableConnections.Wait(cancellationToken); + TimeSpan connectionWaitTime = requestStopwatch.Elapsed; + + TimeSpan responseWaitTime = default(TimeSpan); + GitEndPointResponseData gitEndPointResponseData = null; + HttpResponseMessage response = null; + + try + { + requestStopwatch.Restart(); + + try + { + response = this.client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).GetAwaiter().GetResult(); + } + finally + { + responseWaitTime = requestStopwatch.Elapsed; + } + + responseMetadata.Add("CacheName", GetSingleHeaderOrEmpty(response.Headers, "X-Cache-Name")); + responseMetadata.Add("StatusCode", response.StatusCode); + + if (response.StatusCode == HttpStatusCode.OK) + { + string contentType = GetSingleHeaderOrEmpty(response.Content.Headers, "Content-Type"); + responseMetadata.Add("ContentType", contentType); + + if (!this.authentication.IsAnonymous) + { + this.authentication.ApproveCredentials(this.Tracer, authString); + } + + Stream responseStream = response.Content.ReadAsStreamAsync().GetAwaiter().GetResult(); + + gitEndPointResponseData = new GitEndPointResponseData( + response.StatusCode, + contentType, + responseStream, + message: response, + onResponseDisposed: () => availableConnections.Release()); + } + else + { + errorMessage = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + int statusInt = (int)response.StatusCode; + + bool shouldRetry = ShouldRetry(response.StatusCode); + + if (response.StatusCode == HttpStatusCode.Unauthorized && + this.authentication.IsAnonymous) + { + shouldRetry = false; + errorMessage = "Anonymous request was rejected with a 401"; + } + else if (response.StatusCode == HttpStatusCode.Unauthorized || response.StatusCode == HttpStatusCode.BadRequest || response.StatusCode == HttpStatusCode.Redirect) + { + this.authentication.RejectCredentials(this.Tracer, authString); + if (!this.authentication.IsBackingOff) + { + errorMessage = string.Format("Server returned error code {0} ({1}). Your PAT may be expired and we are asking for a new one. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); + } + else + { + errorMessage = string.Format("Server returned error code {0} ({1}) after successfully renewing your PAT. You may not have access to this repo. Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); + } + } + else + { + errorMessage = string.Format("Server returned error code {0} ({1}). Original error message from server: {2}", statusInt, response.StatusCode, errorMessage); + } + + gitEndPointResponseData = new GitEndPointResponseData( + response.StatusCode, + new GitObjectsHttpException(response.StatusCode, errorMessage), + shouldRetry, + message: response, + onResponseDisposed: () => availableConnections.Release()); + } + } + catch (TaskCanceledException) + { + cancellationToken.ThrowIfCancellationRequested(); + + errorMessage = string.Format("Request to {0} timed out", requestUri); + + gitEndPointResponseData = new GitEndPointResponseData( + HttpStatusCode.RequestTimeout, + new GitObjectsHttpException(HttpStatusCode.RequestTimeout, errorMessage), + shouldRetry: true, + message: response, + onResponseDisposed: () => availableConnections.Release()); + } + catch (HttpRequestException httpRequestException) when (httpRequestException.InnerException is System.Security.Authentication.AuthenticationException) + { + // This exception is thrown on OSX, when user declines to give permission to access certificate + gitEndPointResponseData = new GitEndPointResponseData( + HttpStatusCode.Unauthorized, + httpRequestException.InnerException, + shouldRetry: false, + message: response, + onResponseDisposed: () => availableConnections.Release()); + } + catch (WebException ex) + { + gitEndPointResponseData = new GitEndPointResponseData( + HttpStatusCode.InternalServerError, + ex, + shouldRetry: true, + message: response, + onResponseDisposed: () => availableConnections.Release()); + } + finally + { + responseMetadata.Add("connectionWaitTimeMS", $"{connectionWaitTime.TotalMilliseconds:F4}"); + responseMetadata.Add("responseWaitTimeMS", $"{responseWaitTime.TotalMilliseconds:F4}"); + + this.Tracer.RelatedEvent(EventLevel.Informational, "NetworkResponse", responseMetadata); + + if (gitEndPointResponseData == null) + { + // If gitEndPointResponseData is null there was an unhandled exception + if (response != null) + { + response.Dispose(); + } + + availableConnections.Release(); + } + } + + return gitEndPointResponseData; + } + + private static bool ShouldRetry(HttpStatusCode statusCode) + { + // Retry timeout, Unauthorized, and 5xx errors + int statusInt = (int)statusCode; + if (statusCode == HttpStatusCode.RequestTimeout || + statusCode == HttpStatusCode.Unauthorized || + (statusInt >= 500 && statusInt < 600)) + { + return true; + } + + return false; + } + + private static string GetSingleHeaderOrEmpty(HttpHeaders headers, string headerName) + { + IEnumerable values; + if (headers.TryGetValues(headerName, out values)) + { + return values.First(); + } + + return string.Empty; + } + } +} diff --git a/Scalar.Common/IDiskLayoutUpgradeData.cs b/Scalar.Common/IDiskLayoutUpgradeData.cs index 6a6171e758..ed7d87294c 100644 --- a/Scalar.Common/IDiskLayoutUpgradeData.cs +++ b/Scalar.Common/IDiskLayoutUpgradeData.cs @@ -1,11 +1,11 @@ -using Scalar.DiskLayoutUpgrades; -using System; - -namespace Scalar.Common -{ - public interface IDiskLayoutUpgradeData - { - DiskLayoutUpgrade[] Upgrades { get; } - DiskLayoutVersion Version { get; } - } -} +using Scalar.DiskLayoutUpgrades; +using System; + +namespace Scalar.Common +{ + public interface IDiskLayoutUpgradeData + { + DiskLayoutUpgrade[] Upgrades { get; } + DiskLayoutVersion Version { get; } + } +} diff --git a/Scalar.Common/IHeartBeatMetadataProvider.cs b/Scalar.Common/IHeartBeatMetadataProvider.cs index 2b18d34982..7389fa3716 100644 --- a/Scalar.Common/IHeartBeatMetadataProvider.cs +++ b/Scalar.Common/IHeartBeatMetadataProvider.cs @@ -1,9 +1,9 @@ -using Scalar.Common.Tracing; - -namespace Scalar.Common -{ - public interface IHeartBeatMetadataProvider - { - EventMetadata GetAndResetHeartBeatMetadata(out bool logToFile); - } -} +using Scalar.Common.Tracing; + +namespace Scalar.Common +{ + public interface IHeartBeatMetadataProvider + { + EventMetadata GetAndResetHeartBeatMetadata(out bool logToFile); + } +} diff --git a/Scalar.Common/InstallerPreRunChecker.cs b/Scalar.Common/InstallerPreRunChecker.cs index 140bf5dc8a..8a398c13ab 100644 --- a/Scalar.Common/InstallerPreRunChecker.cs +++ b/Scalar.Common/InstallerPreRunChecker.cs @@ -1,217 +1,217 @@ -using Scalar.Common; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.Upgrader -{ - public class InstallerPreRunChecker - { - private ITracer tracer; - - public InstallerPreRunChecker(ITracer tracer, string commandToRerun) - { - this.tracer = tracer; - this.CommandToRerun = commandToRerun; - } - - protected string CommandToRerun { private get; set; } - - public virtual bool TryRunPreUpgradeChecks(out string consoleError) - { - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryRunPreUpgradeChecks), EventLevel.Informational)) - { - if (this.IsUnattended()) - { - consoleError = $"{ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is not supported in unattended mode"; - this.tracer.RelatedWarning($"{nameof(this.TryRunPreUpgradeChecks)}: {consoleError}"); - return false; - } - - if (!this.IsScalarUpgradeAllowed(out consoleError)) - { - return false; - } - - activity.RelatedInfo($"Successfully finished pre upgrade checks. Okay to run {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade}."); - } - - consoleError = null; - return true; - } - - // TODO: Move repo mount calls to Scalar.Upgrader project. - // https://github.com/Microsoft/Scalar/issues/293 - public virtual bool TryMountAllScalarRepos(out string consoleError) - { - return this.TryRunScalarWithArgs("service --mount-all", out consoleError); - } - - public virtual bool TryUnmountAllScalarRepos(out string consoleError) - { - consoleError = null; - this.tracer.RelatedInfo("Unmounting any mounted Scalar repositories."); - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryUnmountAllScalarRepos), EventLevel.Informational)) - { - if (!this.TryRunScalarWithArgs("service --unmount-all", out consoleError)) - { - this.tracer.RelatedError($"{nameof(this.TryUnmountAllScalarRepos)}: {consoleError}"); - return false; - } - - activity.RelatedInfo("Successfully unmounted repositories."); - } - - return true; - } - - public virtual bool IsInstallationBlockedByRunningProcess(out string consoleError) - { - consoleError = null; - - // While checking for blocking processes like Scalar.Mount immediately after un-mounting, - // then sometimes Scalar.Mount shows up as running. But if the check is done after waiting - // for some time, then eventually Scalar.Mount goes away. The retry loop below is to help - // account for this delay between the time un-mount call returns and when Scalar.Mount - // actually quits. - this.tracer.RelatedInfo("Checking if Scalar or dependent processes are running."); - int retryCount = 10; - HashSet processList = null; - while (retryCount > 0) - { - if (!this.IsBlockingProcessRunning(out processList)) - { - break; - } - - Thread.Sleep(TimeSpan.FromMilliseconds(250)); - retryCount--; - } - - if (processList.Count > 0) - { - consoleError = string.Join( - Environment.NewLine, - "Blocking processes are running.", - $"Run {this.CommandToRerun} again after quitting these processes - " + string.Join(", ", processList.ToArray())); - this.tracer.RelatedWarning($"{nameof(this.IsInstallationBlockedByRunningProcess)}: {consoleError}"); - return false; - } - - return true; - } - - protected virtual bool IsElevated() - { - return ScalarPlatform.Instance.IsElevated(); - } - - protected virtual bool IsScalarUpgradeSupported() - { - return true; - } - - protected virtual bool IsServiceInstalledAndNotRunning() - { - ScalarPlatform.Instance.IsServiceInstalledAndRunning(ScalarConstants.Service.ServiceName, out bool isInstalled, out bool isRunning); - - return isInstalled && !isRunning; - } - - protected virtual bool IsUnattended() - { - return ScalarEnlistment.IsUnattended(this.tracer); - } - - protected virtual bool IsBlockingProcessRunning(out HashSet processes) - { - int currentProcessId = Process.GetCurrentProcess().Id; - Process[] allProcesses = Process.GetProcesses(); - HashSet matchingNames = new HashSet(); - - foreach (Process process in allProcesses) - { - if (process.Id == currentProcessId || !ScalarPlatform.Instance.Constants.UpgradeBlockingProcesses.Contains(process.ProcessName)) - { - continue; - } - - matchingNames.Add(process.ProcessName + " pid:" + process.Id); - } - - processes = matchingNames; - return processes.Count > 0; - } - - protected virtual bool TryRunScalarWithArgs(string args, out string consoleError) - { - string scalarDirectory = ProcessHelper.GetProgramLocation(ScalarPlatform.Instance.Constants.ProgramLocaterCommand, ScalarPlatform.Instance.Constants.ScalarExecutableName); - if (!string.IsNullOrEmpty(scalarDirectory)) - { - string scalarPath = Path.Combine(scalarDirectory, ScalarPlatform.Instance.Constants.ScalarExecutableName); - - ProcessResult processResult = ProcessHelper.Run(scalarPath, args); - if (processResult.ExitCode == 0) - { - consoleError = null; - return true; - } - else - { - consoleError = string.IsNullOrEmpty(processResult.Errors) ? $"`scalar {args}` failed." : processResult.Errors; - return false; - } - } - else - { - consoleError = $"Could not locate {ScalarPlatform.Instance.Constants.ScalarExecutableName}"; - return false; - } - } - - private bool IsScalarUpgradeAllowed(out string consoleError) - { - bool isConfirmed = string.Equals(this.CommandToRerun, ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm, StringComparison.OrdinalIgnoreCase); - string adviceText = null; - if (!this.IsElevated()) - { - adviceText = isConfirmed ? $"Run {this.CommandToRerun} again from an elevated command prompt." : $"To install, run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} from an elevated command prompt."; - consoleError = string.Join( - Environment.NewLine, - "The installer needs to be run from an elevated command prompt.", - adviceText); - this.tracer.RelatedWarning($"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}"); - return false; - } - - if (!this.IsScalarUpgradeSupported()) - { - consoleError = string.Join( - Environment.NewLine, - $"{ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is only supported after the \"Windows Projected File System\" optional feature has been enabled by a manual installation of Scalar, and only on versions of Windows that support this feature.", - "Check your team's documentation for how to upgrade."); - this.tracer.RelatedWarning(metadata: null, message: $"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}", keywords: Keywords.Telemetry); - return false; - } - - if (this.IsServiceInstalledAndNotRunning()) - { - adviceText = isConfirmed ? $"Run `sc start Scalar.Service` and run {this.CommandToRerun} again from an elevated command prompt." : $"To install, run `sc start Scalar.Service` and run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} from an elevated command prompt."; - consoleError = string.Join( - Environment.NewLine, - "Scalar Service is not running.", - adviceText); - this.tracer.RelatedWarning($"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}"); - return false; - } - - consoleError = null; - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.Upgrader +{ + public class InstallerPreRunChecker + { + private ITracer tracer; + + public InstallerPreRunChecker(ITracer tracer, string commandToRerun) + { + this.tracer = tracer; + this.CommandToRerun = commandToRerun; + } + + protected string CommandToRerun { private get; set; } + + public virtual bool TryRunPreUpgradeChecks(out string consoleError) + { + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryRunPreUpgradeChecks), EventLevel.Informational)) + { + if (this.IsUnattended()) + { + consoleError = $"{ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is not supported in unattended mode"; + this.tracer.RelatedWarning($"{nameof(this.TryRunPreUpgradeChecks)}: {consoleError}"); + return false; + } + + if (!this.IsScalarUpgradeAllowed(out consoleError)) + { + return false; + } + + activity.RelatedInfo($"Successfully finished pre upgrade checks. Okay to run {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade}."); + } + + consoleError = null; + return true; + } + + // TODO: Move repo mount calls to Scalar.Upgrader project. + // https://github.com/Microsoft/Scalar/issues/293 + public virtual bool TryMountAllScalarRepos(out string consoleError) + { + return this.TryRunScalarWithArgs("service --mount-all", out consoleError); + } + + public virtual bool TryUnmountAllScalarRepos(out string consoleError) + { + consoleError = null; + this.tracer.RelatedInfo("Unmounting any mounted Scalar repositories."); + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryUnmountAllScalarRepos), EventLevel.Informational)) + { + if (!this.TryRunScalarWithArgs("service --unmount-all", out consoleError)) + { + this.tracer.RelatedError($"{nameof(this.TryUnmountAllScalarRepos)}: {consoleError}"); + return false; + } + + activity.RelatedInfo("Successfully unmounted repositories."); + } + + return true; + } + + public virtual bool IsInstallationBlockedByRunningProcess(out string consoleError) + { + consoleError = null; + + // While checking for blocking processes like Scalar.Mount immediately after un-mounting, + // then sometimes Scalar.Mount shows up as running. But if the check is done after waiting + // for some time, then eventually Scalar.Mount goes away. The retry loop below is to help + // account for this delay between the time un-mount call returns and when Scalar.Mount + // actually quits. + this.tracer.RelatedInfo("Checking if Scalar or dependent processes are running."); + int retryCount = 10; + HashSet processList = null; + while (retryCount > 0) + { + if (!this.IsBlockingProcessRunning(out processList)) + { + break; + } + + Thread.Sleep(TimeSpan.FromMilliseconds(250)); + retryCount--; + } + + if (processList.Count > 0) + { + consoleError = string.Join( + Environment.NewLine, + "Blocking processes are running.", + $"Run {this.CommandToRerun} again after quitting these processes - " + string.Join(", ", processList.ToArray())); + this.tracer.RelatedWarning($"{nameof(this.IsInstallationBlockedByRunningProcess)}: {consoleError}"); + return false; + } + + return true; + } + + protected virtual bool IsElevated() + { + return ScalarPlatform.Instance.IsElevated(); + } + + protected virtual bool IsScalarUpgradeSupported() + { + return true; + } + + protected virtual bool IsServiceInstalledAndNotRunning() + { + ScalarPlatform.Instance.IsServiceInstalledAndRunning(ScalarConstants.Service.ServiceName, out bool isInstalled, out bool isRunning); + + return isInstalled && !isRunning; + } + + protected virtual bool IsUnattended() + { + return ScalarEnlistment.IsUnattended(this.tracer); + } + + protected virtual bool IsBlockingProcessRunning(out HashSet processes) + { + int currentProcessId = Process.GetCurrentProcess().Id; + Process[] allProcesses = Process.GetProcesses(); + HashSet matchingNames = new HashSet(); + + foreach (Process process in allProcesses) + { + if (process.Id == currentProcessId || !ScalarPlatform.Instance.Constants.UpgradeBlockingProcesses.Contains(process.ProcessName)) + { + continue; + } + + matchingNames.Add(process.ProcessName + " pid:" + process.Id); + } + + processes = matchingNames; + return processes.Count > 0; + } + + protected virtual bool TryRunScalarWithArgs(string args, out string consoleError) + { + string scalarDirectory = ProcessHelper.GetProgramLocation(ScalarPlatform.Instance.Constants.ProgramLocaterCommand, ScalarPlatform.Instance.Constants.ScalarExecutableName); + if (!string.IsNullOrEmpty(scalarDirectory)) + { + string scalarPath = Path.Combine(scalarDirectory, ScalarPlatform.Instance.Constants.ScalarExecutableName); + + ProcessResult processResult = ProcessHelper.Run(scalarPath, args); + if (processResult.ExitCode == 0) + { + consoleError = null; + return true; + } + else + { + consoleError = string.IsNullOrEmpty(processResult.Errors) ? $"`scalar {args}` failed." : processResult.Errors; + return false; + } + } + else + { + consoleError = $"Could not locate {ScalarPlatform.Instance.Constants.ScalarExecutableName}"; + return false; + } + } + + private bool IsScalarUpgradeAllowed(out string consoleError) + { + bool isConfirmed = string.Equals(this.CommandToRerun, ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm, StringComparison.OrdinalIgnoreCase); + string adviceText = null; + if (!this.IsElevated()) + { + adviceText = isConfirmed ? $"Run {this.CommandToRerun} again from an elevated command prompt." : $"To install, run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} from an elevated command prompt."; + consoleError = string.Join( + Environment.NewLine, + "The installer needs to be run from an elevated command prompt.", + adviceText); + this.tracer.RelatedWarning($"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}"); + return false; + } + + if (!this.IsScalarUpgradeSupported()) + { + consoleError = string.Join( + Environment.NewLine, + $"{ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is only supported after the \"Windows Projected File System\" optional feature has been enabled by a manual installation of Scalar, and only on versions of Windows that support this feature.", + "Check your team's documentation for how to upgrade."); + this.tracer.RelatedWarning(metadata: null, message: $"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}", keywords: Keywords.Telemetry); + return false; + } + + if (this.IsServiceInstalledAndNotRunning()) + { + adviceText = isConfirmed ? $"Run `sc start Scalar.Service` and run {this.CommandToRerun} again from an elevated command prompt." : $"To install, run `sc start Scalar.Service` and run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} from an elevated command prompt."; + consoleError = string.Join( + Environment.NewLine, + "Scalar Service is not running.", + adviceText); + this.tracer.RelatedWarning($"{nameof(this.IsScalarUpgradeAllowed)}: Upgrade is not installable. {consoleError}"); + return false; + } + + consoleError = null; + return true; + } + } +} diff --git a/Scalar.Common/InternalVerbParameters.cs b/Scalar.Common/InternalVerbParameters.cs index ac7a98cd5e..2783812752 100644 --- a/Scalar.Common/InternalVerbParameters.cs +++ b/Scalar.Common/InternalVerbParameters.cs @@ -1,34 +1,34 @@ -using Newtonsoft.Json; - -namespace Scalar.Common -{ - public class InternalVerbParameters - { - public InternalVerbParameters( - string serviceName = null, - bool startedByService = true, - string maintenanceJob = null, - string packfileMaintenanceBatchSize = null) - { - this.ServiceName = serviceName; - this.StartedByService = startedByService; - this.MaintenanceJob = maintenanceJob; - this.PackfileMaintenanceBatchSize = packfileMaintenanceBatchSize; - } - - public string ServiceName { get; private set; } - public bool StartedByService { get; private set; } - public string MaintenanceJob { get; private set; } - public string PackfileMaintenanceBatchSize { get; private set; } - - public static InternalVerbParameters FromJson(string json) - { - return JsonConvert.DeserializeObject(json); - } - - public string ToJson() - { - return JsonConvert.SerializeObject(this); - } - } -} +using Newtonsoft.Json; + +namespace Scalar.Common +{ + public class InternalVerbParameters + { + public InternalVerbParameters( + string serviceName = null, + bool startedByService = true, + string maintenanceJob = null, + string packfileMaintenanceBatchSize = null) + { + this.ServiceName = serviceName; + this.StartedByService = startedByService; + this.MaintenanceJob = maintenanceJob; + this.PackfileMaintenanceBatchSize = packfileMaintenanceBatchSize; + } + + public string ServiceName { get; private set; } + public bool StartedByService { get; private set; } + public string MaintenanceJob { get; private set; } + public string PackfileMaintenanceBatchSize { get; private set; } + + public static InternalVerbParameters FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + } +} diff --git a/Scalar.Common/InvalidRepoException.cs b/Scalar.Common/InvalidRepoException.cs index f50446c315..b78a64b54e 100644 --- a/Scalar.Common/InvalidRepoException.cs +++ b/Scalar.Common/InvalidRepoException.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Scalar.Common { diff --git a/Scalar.Common/LocalCacheResolver.cs b/Scalar.Common/LocalCacheResolver.cs index 43e1c5aa9a..7b28916d28 100644 --- a/Scalar.Common/LocalCacheResolver.cs +++ b/Scalar.Common/LocalCacheResolver.cs @@ -1,311 +1,311 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; - -namespace Scalar.Common -{ - public class LocalCacheResolver - { - private const string EtwArea = nameof(LocalCacheResolver); - private const string MappingFile = "mapping.dat"; - private const string MappingVersionKey = "Scalar_LocalCache_MappingVersion"; - private const string CurrentMappingDataVersion = "1"; - - private ScalarEnlistment enlistment; - private PhysicalFileSystem fileSystem; - - public LocalCacheResolver(ScalarEnlistment enlistment, PhysicalFileSystem fileSystem = null) - { - this.fileSystem = fileSystem ?? new PhysicalFileSystem(); - this.enlistment = enlistment; - } - - public static bool TryGetDefaultLocalCacheRoot(ScalarEnlistment enlistment, out string localCacheRoot, out string localCacheRootError) - { - if (ScalarEnlistment.IsUnattended(tracer: null)) - { - localCacheRoot = Path.Combine(enlistment.DotScalarRoot, ScalarConstants.DefaultScalarCacheFolderName); - localCacheRootError = null; - return true; - } - - return ScalarPlatform.Instance.TryGetDefaultLocalCacheRoot(enlistment.EnlistmentRoot, out localCacheRoot, out localCacheRootError); - } - - public bool TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( - ITracer tracer, - ServerScalarConfig serverScalarConfig, - CacheServerInfo currentCacheServer, - string localCacheRoot, - out string localCacheKey, - out string errorMessage) - { - if (serverScalarConfig == null) - { - throw new ArgumentNullException(nameof(serverScalarConfig)); - } - - localCacheKey = null; - errorMessage = string.Empty; - - try - { - // A lock is required because FileBasedDictionary is not multi-process safe, neither is the act of adding a new cache - string lockPath = Path.Combine(localCacheRoot, MappingFile + ".lock"); - this.fileSystem.CreateDirectory(localCacheRoot); - - using (FileBasedLock mappingLock = ScalarPlatform.Instance.CreateFileBasedLock( - this.fileSystem, - tracer, - lockPath)) - { - if (!this.TryAcquireLockWithRetries(tracer, mappingLock)) - { - errorMessage = "Failed to acquire lock file at " + lockPath; - tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); - return false; - } - - FileBasedDictionary mappingFile; - if (this.TryOpenMappingFile(tracer, localCacheRoot, out mappingFile, out errorMessage)) - { - try - { - string mappingDataVersion; - if (mappingFile.TryGetValue(MappingVersionKey, out mappingDataVersion)) - { - if (mappingDataVersion != CurrentMappingDataVersion) - { - errorMessage = string.Format("Mapping file has different version than expected: {0} Actual: {1}", CurrentMappingDataVersion, mappingDataVersion); - tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); - return false; - } - } - else - { - mappingFile.SetValueAndFlush(MappingVersionKey, CurrentMappingDataVersion); - } - - if (mappingFile.TryGetValue(this.ToMappingKey(this.enlistment.RepoUrl), out localCacheKey) || - (currentCacheServer.HasValidUrl() && mappingFile.TryGetValue(this.ToMappingKey(currentCacheServer.Url), out localCacheKey))) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("localCacheKey", localCacheKey); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Found existing local cache key"); - tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKey", metadata); - - return true; - } - else - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - - string getLocalCacheKeyError; - if (this.TryGetLocalCacheKeyFromRemoteCacheServers(tracer, serverScalarConfig, currentCacheServer, mappingFile, out localCacheKey, out getLocalCacheKeyError)) - { - metadata.Add("localCacheKey", localCacheKey); - metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Generated new local cache key"); - tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKey", metadata); - return true; - } - - metadata.Add("getLocalCacheKeyError", getLocalCacheKeyError); - tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": TryGetLocalCacheKeyFromRemoteCacheServers failed"); - - errorMessage = "Failed to generate local cache key"; - return false; - } - } - finally - { - mappingFile.Dispose(); - } - } - - return false; - } - } - catch (Exception e) - { - EventMetadata metadata = CreateEventMetadata(e); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Caught exception"); - - errorMessage = string.Format("Exception while getting local cache key: {0}", e.Message); - 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 bool TryOpenMappingFile(ITracer tracer, string localCacheRoot, out FileBasedDictionary mappingFile, out string errorMessage) - { - mappingFile = null; - errorMessage = string.Empty; - - string error; - string mappingFilePath = Path.Combine(localCacheRoot, MappingFile); - if (!FileBasedDictionary.TryCreate( - tracer, - mappingFilePath, - this.fileSystem, - out mappingFile, - out error)) - { - errorMessage = "Could not open mapping file for local cache: " + error; - - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("mappingFilePath", mappingFilePath); - metadata.Add("error", error); - tracer.RelatedError(metadata, "TryOpenMappingFile: Could not open mapping file for local cache"); - - return false; - } - - return true; - } - - private bool TryGetLocalCacheKeyFromRemoteCacheServers( - ITracer tracer, - ServerScalarConfig serverScalarConfig, - CacheServerInfo currentCacheServer, - FileBasedDictionary mappingFile, - out string localCacheKey, - out string error) - { - error = null; - localCacheKey = null; - - try - { - if (this.TryFindExistingLocalCacheKey(mappingFile, serverScalarConfig.CacheServers, out localCacheKey)) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - metadata.Add("localCacheKey", localCacheKey); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Found an existing a local key by cross referencing"); - tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKeyFromCrossReferencing", metadata); - } - else - { - localCacheKey = Guid.NewGuid().ToString("N"); - - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - metadata.Add("localCacheKey", localCacheKey); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Generated a new local key after cross referencing"); - tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKeyAfterCrossReferencing", metadata); - } - - List> mappingFileUpdates = new List>(); - - mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(this.enlistment.RepoUrl), localCacheKey)); - - if (currentCacheServer.HasValidUrl()) - { - mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(currentCacheServer.Url), localCacheKey)); - } - - foreach (CacheServerInfo cacheServer in serverScalarConfig.CacheServers) - { - string persistedLocalCacheKey; - if (mappingFile.TryGetValue(this.ToMappingKey(cacheServer.Url), out persistedLocalCacheKey)) - { - if (!string.Equals(persistedLocalCacheKey, localCacheKey, StringComparison.OrdinalIgnoreCase)) - { - EventMetadata metadata = CreateEventMetadata(); - metadata.Add("cacheServer", cacheServer.ToString()); - metadata.Add("persistedLocalCacheKey", persistedLocalCacheKey); - metadata.Add("localCacheKey", localCacheKey); - metadata.Add("currentCacheServer", currentCacheServer.ToString()); - metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); - tracer.RelatedWarning(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Overwriting persisted cache key with new value"); - - mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); - } - } - else - { - mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); - } - } - - mappingFile.SetValuesAndFlush(mappingFileUpdates); - } - catch (Exception e) - { - EventMetadata metadata = CreateEventMetadata(e); - tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Caught exception while getting local key"); - error = string.Format("Exception while getting local cache key: {0}", e.Message); - return false; - } - - return true; - } - - private bool TryAcquireLockWithRetries(ITracer tracer, FileBasedLock mappingLock) - { - const int NumRetries = 100; - const int WaitTimeMs = 100; - - for (int i = 0; i < NumRetries; ++i) - { - if (mappingLock.TryAcquireLock()) - { - return true; - } - else if (i < NumRetries - 1) - { - Thread.Sleep(WaitTimeMs); - - if (i % 20 == 0) - { - tracer.RelatedInfo("Waiting to acquire local cacke metadata lock file"); - } - } - } - - return false; - } - - private string ToMappingKey(string url) - { - return url.ToLowerInvariant(); - } - - private bool TryFindExistingLocalCacheKey(FileBasedDictionary mappings, IEnumerable cacheServers, out string localCacheKey) - { - foreach (CacheServerInfo cacheServer in cacheServers) - { - if (mappings.TryGetValue(this.ToMappingKey(cacheServer.Url), out localCacheKey)) - { - return true; - } - } - - localCacheKey = null; - return false; - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Scalar.Common +{ + public class LocalCacheResolver + { + private const string EtwArea = nameof(LocalCacheResolver); + private const string MappingFile = "mapping.dat"; + private const string MappingVersionKey = "Scalar_LocalCache_MappingVersion"; + private const string CurrentMappingDataVersion = "1"; + + private ScalarEnlistment enlistment; + private PhysicalFileSystem fileSystem; + + public LocalCacheResolver(ScalarEnlistment enlistment, PhysicalFileSystem fileSystem = null) + { + this.fileSystem = fileSystem ?? new PhysicalFileSystem(); + this.enlistment = enlistment; + } + + public static bool TryGetDefaultLocalCacheRoot(ScalarEnlistment enlistment, out string localCacheRoot, out string localCacheRootError) + { + if (ScalarEnlistment.IsUnattended(tracer: null)) + { + localCacheRoot = Path.Combine(enlistment.DotScalarRoot, ScalarConstants.DefaultScalarCacheFolderName); + localCacheRootError = null; + return true; + } + + return ScalarPlatform.Instance.TryGetDefaultLocalCacheRoot(enlistment.EnlistmentRoot, out localCacheRoot, out localCacheRootError); + } + + public bool TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( + ITracer tracer, + ServerScalarConfig serverScalarConfig, + CacheServerInfo currentCacheServer, + string localCacheRoot, + out string localCacheKey, + out string errorMessage) + { + if (serverScalarConfig == null) + { + throw new ArgumentNullException(nameof(serverScalarConfig)); + } + + localCacheKey = null; + errorMessage = string.Empty; + + try + { + // A lock is required because FileBasedDictionary is not multi-process safe, neither is the act of adding a new cache + string lockPath = Path.Combine(localCacheRoot, MappingFile + ".lock"); + this.fileSystem.CreateDirectory(localCacheRoot); + + using (FileBasedLock mappingLock = ScalarPlatform.Instance.CreateFileBasedLock( + this.fileSystem, + tracer, + lockPath)) + { + if (!this.TryAcquireLockWithRetries(tracer, mappingLock)) + { + errorMessage = "Failed to acquire lock file at " + lockPath; + tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); + return false; + } + + FileBasedDictionary mappingFile; + if (this.TryOpenMappingFile(tracer, localCacheRoot, out mappingFile, out errorMessage)) + { + try + { + string mappingDataVersion; + if (mappingFile.TryGetValue(MappingVersionKey, out mappingDataVersion)) + { + if (mappingDataVersion != CurrentMappingDataVersion) + { + errorMessage = string.Format("Mapping file has different version than expected: {0} Actual: {1}", CurrentMappingDataVersion, mappingDataVersion); + tracer.RelatedError(nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": " + errorMessage); + return false; + } + } + else + { + mappingFile.SetValueAndFlush(MappingVersionKey, CurrentMappingDataVersion); + } + + if (mappingFile.TryGetValue(this.ToMappingKey(this.enlistment.RepoUrl), out localCacheKey) || + (currentCacheServer.HasValidUrl() && mappingFile.TryGetValue(this.ToMappingKey(currentCacheServer.Url), out localCacheKey))) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("localCacheKey", localCacheKey); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Found existing local cache key"); + tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKey", metadata); + + return true; + } + else + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + + string getLocalCacheKeyError; + if (this.TryGetLocalCacheKeyFromRemoteCacheServers(tracer, serverScalarConfig, currentCacheServer, mappingFile, out localCacheKey, out getLocalCacheKeyError)) + { + metadata.Add("localCacheKey", localCacheKey); + metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Generated new local cache key"); + tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKey", metadata); + return true; + } + + metadata.Add("getLocalCacheKeyError", getLocalCacheKeyError); + tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": TryGetLocalCacheKeyFromRemoteCacheServers failed"); + + errorMessage = "Failed to generate local cache key"; + return false; + } + } + finally + { + mappingFile.Dispose(); + } + } + + return false; + } + } + catch (Exception e) + { + EventMetadata metadata = CreateEventMetadata(e); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers) + ": Caught exception"); + + errorMessage = string.Format("Exception while getting local cache key: {0}", e.Message); + 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 bool TryOpenMappingFile(ITracer tracer, string localCacheRoot, out FileBasedDictionary mappingFile, out string errorMessage) + { + mappingFile = null; + errorMessage = string.Empty; + + string error; + string mappingFilePath = Path.Combine(localCacheRoot, MappingFile); + if (!FileBasedDictionary.TryCreate( + tracer, + mappingFilePath, + this.fileSystem, + out mappingFile, + out error)) + { + errorMessage = "Could not open mapping file for local cache: " + error; + + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("mappingFilePath", mappingFilePath); + metadata.Add("error", error); + tracer.RelatedError(metadata, "TryOpenMappingFile: Could not open mapping file for local cache"); + + return false; + } + + return true; + } + + private bool TryGetLocalCacheKeyFromRemoteCacheServers( + ITracer tracer, + ServerScalarConfig serverScalarConfig, + CacheServerInfo currentCacheServer, + FileBasedDictionary mappingFile, + out string localCacheKey, + out string error) + { + error = null; + localCacheKey = null; + + try + { + if (this.TryFindExistingLocalCacheKey(mappingFile, serverScalarConfig.CacheServers, out localCacheKey)) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + metadata.Add("localCacheKey", localCacheKey); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Found an existing a local key by cross referencing"); + tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_ExistingKeyFromCrossReferencing", metadata); + } + else + { + localCacheKey = Guid.NewGuid().ToString("N"); + + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + metadata.Add("localCacheKey", localCacheKey); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + metadata.Add(TracingConstants.MessageKey.InfoMessage, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Generated a new local key after cross referencing"); + tracer.RelatedEvent(EventLevel.Informational, "LocalCacheResolver_NewKeyAfterCrossReferencing", metadata); + } + + List> mappingFileUpdates = new List>(); + + mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(this.enlistment.RepoUrl), localCacheKey)); + + if (currentCacheServer.HasValidUrl()) + { + mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(currentCacheServer.Url), localCacheKey)); + } + + foreach (CacheServerInfo cacheServer in serverScalarConfig.CacheServers) + { + string persistedLocalCacheKey; + if (mappingFile.TryGetValue(this.ToMappingKey(cacheServer.Url), out persistedLocalCacheKey)) + { + if (!string.Equals(persistedLocalCacheKey, localCacheKey, StringComparison.OrdinalIgnoreCase)) + { + EventMetadata metadata = CreateEventMetadata(); + metadata.Add("cacheServer", cacheServer.ToString()); + metadata.Add("persistedLocalCacheKey", persistedLocalCacheKey); + metadata.Add("localCacheKey", localCacheKey); + metadata.Add("currentCacheServer", currentCacheServer.ToString()); + metadata.Add("this.enlistment.RepoUrl", this.enlistment.RepoUrl); + tracer.RelatedWarning(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Overwriting persisted cache key with new value"); + + mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); + } + } + else + { + mappingFileUpdates.Add(new KeyValuePair(this.ToMappingKey(cacheServer.Url), localCacheKey)); + } + } + + mappingFile.SetValuesAndFlush(mappingFileUpdates); + } + catch (Exception e) + { + EventMetadata metadata = CreateEventMetadata(e); + tracer.RelatedError(metadata, nameof(this.TryGetLocalCacheKeyFromRemoteCacheServers) + ": Caught exception while getting local key"); + error = string.Format("Exception while getting local cache key: {0}", e.Message); + return false; + } + + return true; + } + + private bool TryAcquireLockWithRetries(ITracer tracer, FileBasedLock mappingLock) + { + const int NumRetries = 100; + const int WaitTimeMs = 100; + + for (int i = 0; i < NumRetries; ++i) + { + if (mappingLock.TryAcquireLock()) + { + return true; + } + else if (i < NumRetries - 1) + { + Thread.Sleep(WaitTimeMs); + + if (i % 20 == 0) + { + tracer.RelatedInfo("Waiting to acquire local cacke metadata lock file"); + } + } + } + + return false; + } + + private string ToMappingKey(string url) + { + return url.ToLowerInvariant(); + } + + private bool TryFindExistingLocalCacheKey(FileBasedDictionary mappings, IEnumerable cacheServers, out string localCacheKey) + { + foreach (CacheServerInfo cacheServer in cacheServers) + { + if (mappings.TryGetValue(this.ToMappingKey(cacheServer.Url), out localCacheKey)) + { + return true; + } + } + + localCacheKey = null; + return false; + } + } +} diff --git a/Scalar.Common/LocalScalarConfig.cs b/Scalar.Common/LocalScalarConfig.cs index f93f7ebf05..dbe2f60ab9 100644 --- a/Scalar.Common/LocalScalarConfig.cs +++ b/Scalar.Common/LocalScalarConfig.cs @@ -1,132 +1,132 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.Common -{ - public class LocalScalarConfig - { - public const string FileName = "scalar.config"; - private readonly string configFile; - private readonly PhysicalFileSystem fileSystem; - private FileBasedDictionary allSettings; - - public LocalScalarConfig() - { - this.configFile = ScalarPlatform.Instance.ScalarConfigPath; - this.fileSystem = new PhysicalFileSystem(); - } - - public virtual bool TryGetAllConfig(out Dictionary allConfig, out string error) - { - Dictionary configCopy = null; - if (!this.TryPerformAction( - () => configCopy = this.allSettings.GetAllKeysAndValues(), - out error)) - { - allConfig = null; - return false; - } - - allConfig = configCopy; - error = null; - return true; - } - - public virtual bool TryGetConfig( - string name, - out string value, - out string error) - { - string valueFromDict = null; - if (!this.TryPerformAction( - () => this.allSettings.TryGetValue(name, out valueFromDict), - out error)) - { - value = null; - error = $"Error reading config {name}. {error}"; - return false; - } - - value = valueFromDict; - return true; - } - - public virtual bool TrySetConfig( - string name, - string value, - out string error) - { - if (!this.TryPerformAction( - () => this.allSettings.SetValueAndFlush(name, value), - out error)) - { - error = $"Error writing config {name}={value}. {error}"; - return false; - } - - return true; - } - - public virtual bool TryRemoveConfig(string name, out string error) - { - if (!this.TryPerformAction( - () => this.allSettings.RemoveAndFlush(name), - out error)) - { - error = $"Error deleting config {name}. {error}"; - return false; - } - - return true; - } - - private bool TryPerformAction(Action action, out string error) - { - if (!this.TryLoadSettings(out error)) - { - error = $"Error loading config settings. {error}"; - return false; - } - - try - { - action(); - error = null; - return true; - } - catch (FileBasedCollectionException exception) - { - error = exception.Message; - } - - return false; - } - - private bool TryLoadSettings(out string error) - { - if (this.allSettings == null) - { - FileBasedDictionary config = null; - if (FileBasedDictionary.TryCreate( - tracer: null, - dictionaryPath: this.configFile, - fileSystem: this.fileSystem, - output: out config, - error: out error, - keyComparer: StringComparer.OrdinalIgnoreCase)) - { - this.allSettings = config; - return true; - } - - return false; - } - - error = null; - return true; - } - } +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.Common +{ + public class LocalScalarConfig + { + public const string FileName = "scalar.config"; + private readonly string configFile; + private readonly PhysicalFileSystem fileSystem; + private FileBasedDictionary allSettings; + + public LocalScalarConfig() + { + this.configFile = ScalarPlatform.Instance.ScalarConfigPath; + this.fileSystem = new PhysicalFileSystem(); + } + + public virtual bool TryGetAllConfig(out Dictionary allConfig, out string error) + { + Dictionary configCopy = null; + if (!this.TryPerformAction( + () => configCopy = this.allSettings.GetAllKeysAndValues(), + out error)) + { + allConfig = null; + return false; + } + + allConfig = configCopy; + error = null; + return true; + } + + public virtual bool TryGetConfig( + string name, + out string value, + out string error) + { + string valueFromDict = null; + if (!this.TryPerformAction( + () => this.allSettings.TryGetValue(name, out valueFromDict), + out error)) + { + value = null; + error = $"Error reading config {name}. {error}"; + return false; + } + + value = valueFromDict; + return true; + } + + public virtual bool TrySetConfig( + string name, + string value, + out string error) + { + if (!this.TryPerformAction( + () => this.allSettings.SetValueAndFlush(name, value), + out error)) + { + error = $"Error writing config {name}={value}. {error}"; + return false; + } + + return true; + } + + public virtual bool TryRemoveConfig(string name, out string error) + { + if (!this.TryPerformAction( + () => this.allSettings.RemoveAndFlush(name), + out error)) + { + error = $"Error deleting config {name}. {error}"; + return false; + } + + return true; + } + + private bool TryPerformAction(Action action, out string error) + { + if (!this.TryLoadSettings(out error)) + { + error = $"Error loading config settings. {error}"; + return false; + } + + try + { + action(); + error = null; + return true; + } + catch (FileBasedCollectionException exception) + { + error = exception.Message; + } + + return false; + } + + private bool TryLoadSettings(out string error) + { + if (this.allSettings == null) + { + FileBasedDictionary config = null; + if (FileBasedDictionary.TryCreate( + tracer: null, + dictionaryPath: this.configFile, + fileSystem: this.fileSystem, + output: out config, + error: out error, + keyComparer: StringComparer.OrdinalIgnoreCase)) + { + this.allSettings = config; + return true; + } + + return false; + } + + error = null; + return true; + } + } } diff --git a/Scalar.Common/Maintenance/GitMaintenanceQueue.cs b/Scalar.Common/Maintenance/GitMaintenanceQueue.cs index 3444e4f442..149e44443e 100644 --- a/Scalar.Common/Maintenance/GitMaintenanceQueue.cs +++ b/Scalar.Common/Maintenance/GitMaintenanceQueue.cs @@ -1,131 +1,131 @@ -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Threading; - -namespace Scalar.Common.Maintenance -{ - public class GitMaintenanceQueue - { - private readonly object queueLock = new object(); - private ScalarContext context; - private BlockingCollection queue = new BlockingCollection(); - private GitMaintenanceStep currentStep; - - public GitMaintenanceQueue(ScalarContext context) - { - this.context = context; - Thread worker = new Thread(() => this.RunQueue()); - worker.Name = "MaintenanceWorker"; - worker.IsBackground = true; - worker.Start(); - } - - public bool TryEnqueue(GitMaintenanceStep step) - { - try - { - lock (this.queueLock) - { - if (this.queue == null) - { - return false; - } - - this.queue.Add(step); - return true; - } - } - catch (InvalidOperationException) - { - // We called queue.CompleteAdding() - } - - return false; - } - - public void Stop() - { - lock (this.queueLock) - { - this.queue?.CompleteAdding(); - } - - this.currentStep?.Stop(); - } - - /// - /// This method is public for test purposes only. - /// - public bool EnlistmentRootReady() - { - // If a user locks their drive or disconnects an external drive while the mount process - // is running, then it will appear as if the directories below do not exist or throw - // a "Device is not ready" error. - try - { - return this.context.FileSystem.DirectoryExists(this.context.Enlistment.EnlistmentRoot) - && this.context.FileSystem.DirectoryExists(this.context.Enlistment.GitObjectsRoot); - } - catch (IOException) - { - return false; - } - } - - private void RunQueue() - { - while (true) - { - // We cannot take the lock here, as TryTake is blocking. - // However, this is the place to set 'this.queue' to null. - if (!this.queue.TryTake(out this.currentStep, Timeout.Infinite) - || this.queue.IsAddingCompleted) - { - lock (this.queueLock) - { - // A stop was requested - this.queue?.Dispose(); - this.queue = null; - return; - } - } - - if (this.EnlistmentRootReady()) - { - try - { - this.currentStep.Execute(); - } - catch (Exception e) - { - this.LogErrorAndExit( - area: nameof(GitMaintenanceQueue), - methodName: nameof(this.RunQueue), - exception: e); - } - } - } - } - - private void LogError(string area, string methodName, Exception exception) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", area); - metadata.Add("Method", methodName); - metadata.Add("ExceptionMessage", exception.Message); - metadata.Add("StackTrace", exception.StackTrace); - this.context.Tracer.RelatedError( - metadata: metadata, - message: area + ": Unexpected Exception while running maintenance steps (fatal): " + exception.Message, - keywords: Keywords.Telemetry); - } - - private void LogErrorAndExit(string area, string methodName, Exception exception) - { - this.LogError(area, methodName, exception); - Environment.Exit((int)ReturnCode.GenericError); - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Threading; + +namespace Scalar.Common.Maintenance +{ + public class GitMaintenanceQueue + { + private readonly object queueLock = new object(); + private ScalarContext context; + private BlockingCollection queue = new BlockingCollection(); + private GitMaintenanceStep currentStep; + + public GitMaintenanceQueue(ScalarContext context) + { + this.context = context; + Thread worker = new Thread(() => this.RunQueue()); + worker.Name = "MaintenanceWorker"; + worker.IsBackground = true; + worker.Start(); + } + + public bool TryEnqueue(GitMaintenanceStep step) + { + try + { + lock (this.queueLock) + { + if (this.queue == null) + { + return false; + } + + this.queue.Add(step); + return true; + } + } + catch (InvalidOperationException) + { + // We called queue.CompleteAdding() + } + + return false; + } + + public void Stop() + { + lock (this.queueLock) + { + this.queue?.CompleteAdding(); + } + + this.currentStep?.Stop(); + } + + /// + /// This method is public for test purposes only. + /// + public bool EnlistmentRootReady() + { + // If a user locks their drive or disconnects an external drive while the mount process + // is running, then it will appear as if the directories below do not exist or throw + // a "Device is not ready" error. + try + { + return this.context.FileSystem.DirectoryExists(this.context.Enlistment.EnlistmentRoot) + && this.context.FileSystem.DirectoryExists(this.context.Enlistment.GitObjectsRoot); + } + catch (IOException) + { + return false; + } + } + + private void RunQueue() + { + while (true) + { + // We cannot take the lock here, as TryTake is blocking. + // However, this is the place to set 'this.queue' to null. + if (!this.queue.TryTake(out this.currentStep, Timeout.Infinite) + || this.queue.IsAddingCompleted) + { + lock (this.queueLock) + { + // A stop was requested + this.queue?.Dispose(); + this.queue = null; + return; + } + } + + if (this.EnlistmentRootReady()) + { + try + { + this.currentStep.Execute(); + } + catch (Exception e) + { + this.LogErrorAndExit( + area: nameof(GitMaintenanceQueue), + methodName: nameof(this.RunQueue), + exception: e); + } + } + } + } + + private void LogError(string area, string methodName, Exception exception) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", area); + metadata.Add("Method", methodName); + metadata.Add("ExceptionMessage", exception.Message); + metadata.Add("StackTrace", exception.StackTrace); + this.context.Tracer.RelatedError( + metadata: metadata, + message: area + ": Unexpected Exception while running maintenance steps (fatal): " + exception.Message, + keywords: Keywords.Telemetry); + } + + private void LogErrorAndExit(string area, string methodName, Exception exception) + { + this.LogError(area, methodName, exception); + Environment.Exit((int)ReturnCode.GenericError); + } + } +} diff --git a/Scalar.Common/Maintenance/GitMaintenanceScheduler.cs b/Scalar.Common/Maintenance/GitMaintenanceScheduler.cs index a6267e5a9f..45a8cd6a03 100644 --- a/Scalar.Common/Maintenance/GitMaintenanceScheduler.cs +++ b/Scalar.Common/Maintenance/GitMaintenanceScheduler.cs @@ -1,80 +1,80 @@ -using Scalar.Common.Git; -using System; -using System.Collections.Generic; -using System.Threading; - -namespace Scalar.Common.Maintenance -{ - public class GitMaintenanceScheduler : IDisposable - { - private readonly TimeSpan looseObjectsDueTime = TimeSpan.FromMinutes(5); - private readonly TimeSpan looseObjectsPeriod = TimeSpan.FromHours(6); - - private readonly TimeSpan packfileDueTime = TimeSpan.FromMinutes(30); - private readonly TimeSpan packfilePeriod = TimeSpan.FromHours(12); - - private readonly TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); - - private List stepTimers; - private ScalarContext context; - private GitObjects gitObjects; - private GitMaintenanceQueue queue; - - public GitMaintenanceScheduler(ScalarContext context, GitObjects gitObjects) - { - this.context = context; - this.gitObjects = gitObjects; - this.stepTimers = new List(); - this.queue = new GitMaintenanceQueue(context); - - this.ScheduleRecurringSteps(); - } - - public void EnqueueOneTimeStep(GitMaintenanceStep step) - { - this.queue.TryEnqueue(step); - } - - public void Dispose() - { - this.queue.Stop(); - - foreach (Timer timer in this.stepTimers) - { - timer?.Dispose(); - } - - this.stepTimers = null; - } - - private void ScheduleRecurringSteps() - { - if (this.context.Unattended) - { - return; - } - - if (this.gitObjects.IsUsingCacheServer()) - { - TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); - this.stepTimers.Add(new Timer( - (state) => this.queue.TryEnqueue(new PrefetchStep(this.context, this.gitObjects)), - state: null, - dueTime: this.prefetchPeriod, - period: this.prefetchPeriod)); - } - - this.stepTimers.Add(new Timer( - (state) => this.queue.TryEnqueue(new LooseObjectsStep(this.context)), - state: null, - dueTime: this.looseObjectsDueTime, - period: this.looseObjectsPeriod)); - - this.stepTimers.Add(new Timer( - (state) => this.queue.TryEnqueue(new PackfileMaintenanceStep(this.context)), - state: null, - dueTime: this.packfileDueTime, - period: this.packfilePeriod)); - } - } -} +using Scalar.Common.Git; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Scalar.Common.Maintenance +{ + public class GitMaintenanceScheduler : IDisposable + { + private readonly TimeSpan looseObjectsDueTime = TimeSpan.FromMinutes(5); + private readonly TimeSpan looseObjectsPeriod = TimeSpan.FromHours(6); + + private readonly TimeSpan packfileDueTime = TimeSpan.FromMinutes(30); + private readonly TimeSpan packfilePeriod = TimeSpan.FromHours(12); + + private readonly TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); + + private List stepTimers; + private ScalarContext context; + private GitObjects gitObjects; + private GitMaintenanceQueue queue; + + public GitMaintenanceScheduler(ScalarContext context, GitObjects gitObjects) + { + this.context = context; + this.gitObjects = gitObjects; + this.stepTimers = new List(); + this.queue = new GitMaintenanceQueue(context); + + this.ScheduleRecurringSteps(); + } + + public void EnqueueOneTimeStep(GitMaintenanceStep step) + { + this.queue.TryEnqueue(step); + } + + public void Dispose() + { + this.queue.Stop(); + + foreach (Timer timer in this.stepTimers) + { + timer?.Dispose(); + } + + this.stepTimers = null; + } + + private void ScheduleRecurringSteps() + { + if (this.context.Unattended) + { + return; + } + + if (this.gitObjects.IsUsingCacheServer()) + { + TimeSpan prefetchPeriod = TimeSpan.FromMinutes(15); + this.stepTimers.Add(new Timer( + (state) => this.queue.TryEnqueue(new PrefetchStep(this.context, this.gitObjects)), + state: null, + dueTime: this.prefetchPeriod, + period: this.prefetchPeriod)); + } + + this.stepTimers.Add(new Timer( + (state) => this.queue.TryEnqueue(new LooseObjectsStep(this.context)), + state: null, + dueTime: this.looseObjectsDueTime, + period: this.looseObjectsPeriod)); + + this.stepTimers.Add(new Timer( + (state) => this.queue.TryEnqueue(new PackfileMaintenanceStep(this.context)), + state: null, + dueTime: this.packfileDueTime, + period: this.packfilePeriod)); + } + } +} diff --git a/Scalar.Common/Maintenance/GitMaintenanceStep.cs b/Scalar.Common/Maintenance/GitMaintenanceStep.cs index a17ae89385..485b91b204 100644 --- a/Scalar.Common/Maintenance/GitMaintenanceStep.cs +++ b/Scalar.Common/Maintenance/GitMaintenanceStep.cs @@ -1,111 +1,111 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.Common.Maintenance -{ - public abstract class GitMaintenanceStep - { - public const string ObjectCacheLock = "git-maintenance-step.lock"; - private readonly object gitProcessLock = new object(); - - public GitMaintenanceStep(ScalarContext context, bool requireObjectCacheLock, GitProcessChecker gitProcessChecker = null) - { - this.Context = context; - this.RequireObjectCacheLock = requireObjectCacheLock; - this.GitProcessChecker = gitProcessChecker ?? new GitProcessChecker(); - } - - public abstract string Area { get; } - protected virtual TimeSpan TimeBetweenRuns { get; } - protected virtual string LastRunTimeFilePath { get; set; } - protected ScalarContext Context { get; } - protected GitProcess MaintenanceGitProcess { get; private set; } - protected bool Stopping { get; private set; } - protected bool RequireObjectCacheLock { get; } - protected GitProcessChecker GitProcessChecker { get; } - - public void Execute() - { - try - { - if (this.RequireObjectCacheLock) - { - using (FileBasedLock cacheLock = ScalarPlatform.Instance.CreateFileBasedLock( - this.Context.FileSystem, - this.Context.Tracer, - Path.Combine(this.Context.Enlistment.GitObjectsRoot, ObjectCacheLock))) - { - if (!cacheLock.TryAcquireLock()) - { - this.Context.Tracer.RelatedInfo(this.Area + ": Skipping work since another process holds the lock"); - return; - } - - this.CreateProcessAndRun(); - } - } - else - { - this.CreateProcessAndRun(); - } - } - catch (IOException e) - { - this.Context.Tracer.RelatedWarning( - metadata: this.CreateEventMetadata(e), - message: "IOException while running action: " + e.Message, - keywords: Keywords.Telemetry); - } - catch (Exception e) - { - this.Context.Tracer.RelatedError( - metadata: this.CreateEventMetadata(e), - message: "Exception while running action: " + e.Message, - keywords: Keywords.Telemetry); - Environment.Exit((int)ReturnCode.GenericError); - } - } - - public void Stop() - { - lock (this.gitProcessLock) - { - this.Stopping = true; - - GitProcess process = this.MaintenanceGitProcess; - - if (process != null) - { - if (process.TryKillRunningProcess(out string processName, out int exitCode, out string error)) - { - this.Context.Tracer.RelatedEvent( - EventLevel.Informational, - string.Format( - "{0}: killed background process {1} during {2}", - this.Area, - processName, - nameof(this.Stop)), - metadata: null); - } - else - { - this.Context.Tracer.RelatedEvent( - EventLevel.Informational, - string.Format( - "{0}: failed to kill background process {1} during {2}. ExitCode:{3} Error:{4}", - this.Area, - processName, - nameof(this.Stop), - exitCode, - error), - metadata: null); - } - } - } +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.Common.Maintenance +{ + public abstract class GitMaintenanceStep + { + public const string ObjectCacheLock = "git-maintenance-step.lock"; + private readonly object gitProcessLock = new object(); + + public GitMaintenanceStep(ScalarContext context, bool requireObjectCacheLock, GitProcessChecker gitProcessChecker = null) + { + this.Context = context; + this.RequireObjectCacheLock = requireObjectCacheLock; + this.GitProcessChecker = gitProcessChecker ?? new GitProcessChecker(); + } + + public abstract string Area { get; } + protected virtual TimeSpan TimeBetweenRuns { get; } + protected virtual string LastRunTimeFilePath { get; set; } + protected ScalarContext Context { get; } + protected GitProcess MaintenanceGitProcess { get; private set; } + protected bool Stopping { get; private set; } + protected bool RequireObjectCacheLock { get; } + protected GitProcessChecker GitProcessChecker { get; } + + public void Execute() + { + try + { + if (this.RequireObjectCacheLock) + { + using (FileBasedLock cacheLock = ScalarPlatform.Instance.CreateFileBasedLock( + this.Context.FileSystem, + this.Context.Tracer, + Path.Combine(this.Context.Enlistment.GitObjectsRoot, ObjectCacheLock))) + { + if (!cacheLock.TryAcquireLock()) + { + this.Context.Tracer.RelatedInfo(this.Area + ": Skipping work since another process holds the lock"); + return; + } + + this.CreateProcessAndRun(); + } + } + else + { + this.CreateProcessAndRun(); + } + } + catch (IOException e) + { + this.Context.Tracer.RelatedWarning( + metadata: this.CreateEventMetadata(e), + message: "IOException while running action: " + e.Message, + keywords: Keywords.Telemetry); + } + catch (Exception e) + { + this.Context.Tracer.RelatedError( + metadata: this.CreateEventMetadata(e), + message: "Exception while running action: " + e.Message, + keywords: Keywords.Telemetry); + Environment.Exit((int)ReturnCode.GenericError); + } + } + + public void Stop() + { + lock (this.gitProcessLock) + { + this.Stopping = true; + + GitProcess process = this.MaintenanceGitProcess; + + if (process != null) + { + if (process.TryKillRunningProcess(out string processName, out int exitCode, out string error)) + { + this.Context.Tracer.RelatedEvent( + EventLevel.Informational, + string.Format( + "{0}: killed background process {1} during {2}", + this.Area, + processName, + nameof(this.Stop)), + metadata: null); + } + else + { + this.Context.Tracer.RelatedEvent( + EventLevel.Informational, + string.Format( + "{0}: failed to kill background process {1} during {2}. ExitCode:{3} Error:{4}", + this.Area, + processName, + nameof(this.Stop), + exitCode, + error), + metadata: null); + } + } + } } // public only for unit tests @@ -124,158 +124,158 @@ public void GetPackFilesInfo(out int count, out long size, out bool hasKeep) count++; size += info.Length; } - else if (string.Equals(extension, ".keep", StringComparison.OrdinalIgnoreCase)) - { - hasKeep = true; + else if (string.Equals(extension, ".keep", StringComparison.OrdinalIgnoreCase)) + { + hasKeep = true; + } + } + } + + /// + /// Implement this method perform the mainteance actions. If the object-cache lock is required + /// (as specified by ), then this step is not run unless we + /// hold the lock. + /// + protected abstract void PerformMaintenance(); + + protected GitProcess.Result RunGitCommand(Func work, string gitCommand) + { + EventMetadata metadata = this.CreateEventMetadata(); + metadata.Add("gitCommand", gitCommand); + + using (ITracer activity = this.Context.Tracer.StartActivity("RunGitCommand", EventLevel.Informational, metadata)) + { + if (this.Stopping) + { + this.Context.Tracer.RelatedWarning( + metadata: null, + message: $"{this.Area}: Not launching Git process {gitCommand} because the mount is stopping", + keywords: Keywords.Telemetry); + throw new StoppingException(); + } + + GitProcess.Result result = work.Invoke(this.MaintenanceGitProcess); + + if (this.Stopping) + { + throw new StoppingException(); + } + + if (result?.ExitCodeIsFailure == true) + { + string errorMessage = result?.Errors == null ? string.Empty : result.Errors; + if (errorMessage.Length > 1000) + { + // For large error messages, we show the first and last 500 chars + errorMessage = $"beginning: {errorMessage.Substring(0, 500)} ending: {errorMessage.Substring(errorMessage.Length - 500)}"; + } + + this.Context.Tracer.RelatedWarning( + metadata: null, + message: $"{this.Area}: Git process {gitCommand} failed with errors: {errorMessage}", + keywords: Keywords.Telemetry); + return result; } + + return result; } - } - - /// - /// Implement this method perform the mainteance actions. If the object-cache lock is required - /// (as specified by ), then this step is not run unless we - /// hold the lock. - /// - protected abstract void PerformMaintenance(); - - protected GitProcess.Result RunGitCommand(Func work, string gitCommand) - { - EventMetadata metadata = this.CreateEventMetadata(); - metadata.Add("gitCommand", gitCommand); - - using (ITracer activity = this.Context.Tracer.StartActivity("RunGitCommand", EventLevel.Informational, metadata)) - { - if (this.Stopping) - { - this.Context.Tracer.RelatedWarning( - metadata: null, - message: $"{this.Area}: Not launching Git process {gitCommand} because the mount is stopping", - keywords: Keywords.Telemetry); - throw new StoppingException(); - } - - GitProcess.Result result = work.Invoke(this.MaintenanceGitProcess); - - if (this.Stopping) - { - throw new StoppingException(); - } - - if (result?.ExitCodeIsFailure == true) - { - string errorMessage = result?.Errors == null ? string.Empty : result.Errors; - if (errorMessage.Length > 1000) - { - // For large error messages, we show the first and last 500 chars - errorMessage = $"beginning: {errorMessage.Substring(0, 500)} ending: {errorMessage.Substring(errorMessage.Length - 500)}"; - } - - this.Context.Tracer.RelatedWarning( - metadata: null, - message: $"{this.Area}: Git process {gitCommand} failed with errors: {errorMessage}", - keywords: Keywords.Telemetry); - return result; - } - - return result; - } - } - - protected EventMetadata CreateEventMetadata(Exception e = null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", this.Area); - - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - return metadata; - } - - protected bool EnoughTimeBetweenRuns() - { - if (!this.Context.FileSystem.FileExists(this.LastRunTimeFilePath)) - { - return true; - } - - string lastRunTime = this.Context.FileSystem.ReadAllText(this.LastRunTimeFilePath); - if (!long.TryParse(lastRunTime, out long result)) - { - this.Context.Tracer.RelatedError("Failed to parse long: {0}", lastRunTime); - return true; - } - - if (DateTime.UtcNow.Subtract(EpochConverter.FromUnixEpochSeconds(result)) >= this.TimeBetweenRuns) - { - return true; - } - - return false; - } - - protected void SaveLastRunTimeToFile() - { - if (!this.Context.FileSystem.TryWriteTempFileAndRename( - this.LastRunTimeFilePath, - EpochConverter.ToUnixEpochSeconds(DateTime.UtcNow).ToString(), - out Exception handledException)) - { - this.Context.Tracer.RelatedError(this.CreateEventMetadata(handledException), "Failed to record run time"); - } - } - - protected void LogErrorAndRewriteMultiPackIndex(ITracer activity) - { - EventMetadata errorMetadata = this.CreateEventMetadata(); - string multiPackIndexPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); - errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(multiPackIndexPath); - - GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); - errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; - - activity.RelatedWarning(errorMetadata, "multi-pack-index is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); - } - - protected void LogErrorAndRewriteCommitGraph(ITracer activity, List packs) - { - EventMetadata errorMetadata = this.CreateEventMetadata(); - string commitGraphPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graph"); - errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(commitGraphPath); - - GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, packs), nameof(GitProcess.WriteCommitGraph)); - errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; - - activity.RelatedWarning(errorMetadata, "commit-graph is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); - } - - private void CreateProcessAndRun() - { - lock (this.gitProcessLock) - { - if (this.Stopping) - { - return; - } - - this.MaintenanceGitProcess = this.Context.Enlistment.CreateGitProcess(); - this.MaintenanceGitProcess.LowerPriority = true; - } - - try - { - this.PerformMaintenance(); - } - catch (StoppingException) - { - // Part of shutdown, skipped commands have already been logged - } - } - - protected class StoppingException : Exception - { - } - } -} + } + + protected EventMetadata CreateEventMetadata(Exception e = null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", this.Area); + + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + protected bool EnoughTimeBetweenRuns() + { + if (!this.Context.FileSystem.FileExists(this.LastRunTimeFilePath)) + { + return true; + } + + string lastRunTime = this.Context.FileSystem.ReadAllText(this.LastRunTimeFilePath); + if (!long.TryParse(lastRunTime, out long result)) + { + this.Context.Tracer.RelatedError("Failed to parse long: {0}", lastRunTime); + return true; + } + + if (DateTime.UtcNow.Subtract(EpochConverter.FromUnixEpochSeconds(result)) >= this.TimeBetweenRuns) + { + return true; + } + + return false; + } + + protected void SaveLastRunTimeToFile() + { + if (!this.Context.FileSystem.TryWriteTempFileAndRename( + this.LastRunTimeFilePath, + EpochConverter.ToUnixEpochSeconds(DateTime.UtcNow).ToString(), + out Exception handledException)) + { + this.Context.Tracer.RelatedError(this.CreateEventMetadata(handledException), "Failed to record run time"); + } + } + + protected void LogErrorAndRewriteMultiPackIndex(ITracer activity) + { + EventMetadata errorMetadata = this.CreateEventMetadata(); + string multiPackIndexPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); + errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(multiPackIndexPath); + + GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); + errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; + + activity.RelatedWarning(errorMetadata, "multi-pack-index is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); + } + + protected void LogErrorAndRewriteCommitGraph(ITracer activity, List packs) + { + EventMetadata errorMetadata = this.CreateEventMetadata(); + string commitGraphPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graph"); + errorMetadata["TryDeleteFileResult"] = this.Context.FileSystem.TryDeleteFile(commitGraphPath); + + GitProcess.Result rewriteResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, packs), nameof(GitProcess.WriteCommitGraph)); + errorMetadata["RewriteResultExitCode"] = rewriteResult.ExitCode; + + activity.RelatedWarning(errorMetadata, "commit-graph is corrupt after write. Deleting and rewriting.", Keywords.Telemetry); + } + + private void CreateProcessAndRun() + { + lock (this.gitProcessLock) + { + if (this.Stopping) + { + return; + } + + this.MaintenanceGitProcess = this.Context.Enlistment.CreateGitProcess(); + this.MaintenanceGitProcess.LowerPriority = true; + } + + try + { + this.PerformMaintenance(); + } + catch (StoppingException) + { + // Part of shutdown, skipped commands have already been logged + } + } + + protected class StoppingException : Exception + { + } + } +} diff --git a/Scalar.Common/Maintenance/GitProcessChecker.cs b/Scalar.Common/Maintenance/GitProcessChecker.cs index db82187e9a..37f91ef567 100644 --- a/Scalar.Common/Maintenance/GitProcessChecker.cs +++ b/Scalar.Common/Maintenance/GitProcessChecker.cs @@ -1,18 +1,18 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace Scalar.Common.Maintenance -{ - public class GitProcessChecker - { - public virtual IEnumerable GetRunningGitProcessIds() - { - Process[] allProcesses = Process.GetProcesses(); - return allProcesses - .Where(x => x.ProcessName.Equals("git", StringComparison.OrdinalIgnoreCase)) - .Select(x => x.Id); - } - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Scalar.Common.Maintenance +{ + public class GitProcessChecker + { + public virtual IEnumerable GetRunningGitProcessIds() + { + Process[] allProcesses = Process.GetProcesses(); + return allProcesses + .Where(x => x.ProcessName.Equals("git", StringComparison.OrdinalIgnoreCase)) + .Select(x => x.Id); + } + } +} diff --git a/Scalar.Common/Maintenance/LooseObjectsStep.cs b/Scalar.Common/Maintenance/LooseObjectsStep.cs index 584b76f009..ea197ea66d 100644 --- a/Scalar.Common/Maintenance/LooseObjectsStep.cs +++ b/Scalar.Common/Maintenance/LooseObjectsStep.cs @@ -1,260 +1,260 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.Common.Maintenance -{ - // Performs LooseObject Maintenace - // 1. Removes loose objects that appear in packfiles - // 2. Packs loose objects into a packfile - public class LooseObjectsStep : GitMaintenanceStep - { - public const string LooseObjectsLastRunFileName = "loose-objects.time"; - private readonly bool forceRun; - - public LooseObjectsStep( - ScalarContext context, - bool requireCacheLock = true, - bool forceRun = false, - GitProcessChecker gitProcessChecker = null) - : base(context, requireCacheLock, gitProcessChecker) - { - this.forceRun = forceRun; - } - - public enum CreatePackResult - { - Succeess, - UnknownFailure, - CorruptObject - } - - public override string Area => nameof(LooseObjectsStep); - - // 50,000 was found to be the optimal time taking ~5 minutes - public int MaxLooseObjectsInPack { get; set; } = 50000; - - protected override string LastRunTimeFilePath => Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", LooseObjectsLastRunFileName); - protected override TimeSpan TimeBetweenRuns => TimeSpan.FromDays(1); - - public void CountLooseObjects(out int count, out long size) - { - count = 0; - size = 0; - - foreach (string directoryPath in this.Context.FileSystem.EnumerateDirectories(this.Context.Enlistment.GitObjectsRoot)) - { - string directoryName = directoryPath.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Last(); - - if (GitObjects.IsLooseObjectsDirectory(directoryName)) - { - string dirPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, directoryPath); - List dirItems = this.Context.FileSystem.ItemsInDirectory(dirPath).ToList(); - count += dirItems.Count; - size += dirItems.Sum(item => item.Length); - } - } - } - - public IEnumerable GetBatchOfLooseObjects(int batchSize) - { - // Find loose Objects - foreach (DirectoryItemInfo directoryItemInfo in this.Context.FileSystem.ItemsInDirectory(this.Context.Enlistment.GitObjectsRoot)) - { - if (directoryItemInfo.IsDirectory) - { - string directoryName = directoryItemInfo.Name; - - if (GitObjects.IsLooseObjectsDirectory(directoryName)) - { - string[] looseObjectFileNamesInDir = this.Context.FileSystem.GetFiles(directoryItemInfo.FullName, "*"); - - foreach (string filePath in looseObjectFileNamesInDir) - { - if (!this.TryGetLooseObjectId(directoryName, filePath, out string objectId)) - { - this.Context.Tracer.RelatedWarning($"Invalid ObjectId {objectId} using directory {directoryName} and path {filePath}"); - continue; - } - - batchSize--; - yield return objectId; - - if (batchSize <= 0) - { - yield break; - } - } - } - } - } - } - - /// - /// Writes loose object Ids to streamWriter - /// - /// Writer to which SHAs are written - /// The number of loose objects SHAs written to the stream - public int WriteLooseObjectIds(StreamWriter streamWriter) - { - int count = 0; - - foreach (string objectId in this.GetBatchOfLooseObjects(this.MaxLooseObjectsInPack)) - { - streamWriter.Write(objectId + "\n"); - count++; - } - - return count; - } - - public bool TryGetLooseObjectId(string directoryName, string filePath, out string objectId) - { - objectId = directoryName + Path.GetFileName(filePath); - if (!SHA1Util.IsValidShaFormat(objectId)) - { - return false; - } - - return true; - } - - /// - /// Creates a pack file from loose objects - /// - /// The number of loose objects added to the pack file - public CreatePackResult TryCreateLooseObjectsPackFile(out int objectsAddedToPack) - { - int localObjectCount = 0; - - GitProcess.Result result = this.RunGitCommand( - (process) => process.PackObjects( - "from-loose", - this.Context.Enlistment.GitObjectsRoot, - (StreamWriter writer) => localObjectCount = this.WriteLooseObjectIds(writer)), - nameof(GitProcess.PackObjects)); - - if (result.ExitCodeIsSuccess) - { - objectsAddedToPack = localObjectCount; - return CreatePackResult.Succeess; - } - else - { - objectsAddedToPack = 0; - - if (result.Errors.Contains("is corrupt")) - { - return CreatePackResult.CorruptObject; - } - - return CreatePackResult.UnknownFailure; - } - } - - public string GetLooseObjectFileName(string objectId) - { - return Path.Combine( - this.Context.Enlistment.GitObjectsRoot, - objectId.Substring(0, 2), - objectId.Substring(2, ScalarConstants.ShaStringLength - 2)); - } - - public void ClearCorruptLooseObjects(EventMetadata metadata) - { - int numDeletedObjects = 0; - int numFailedDeletes = 0; - - // Double the batch size to look beyond the current batch for bad objects, as there - // may be more bad objects in the next batch after deleting the corrupt objects. - foreach (string objectId in this.GetBatchOfLooseObjects(2 * this.MaxLooseObjectsInPack)) - { - if (!this.Context.Repository.ObjectExists(objectId)) - { - string objectFile = this.GetLooseObjectFileName(objectId); - - if (this.Context.FileSystem.TryDeleteFile(objectFile)) - { - numDeletedObjects++; - } - else - { - numFailedDeletes++; - } - } - } - - metadata.Add("RemovedCorruptObjects", numDeletedObjects); - metadata.Add("NumFailedDeletes", numFailedDeletes); - } - - protected override void PerformMaintenance() - { - using (ITracer activity = this.Context.Tracer.StartActivity(this.Area, EventLevel.Informational, Keywords.Telemetry, metadata: null)) - { - try - { - // forceRun is only currently true for functional tests - if (!this.forceRun) - { - if (!this.EnoughTimeBetweenRuns()) - { - activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to not enough time between runs"); - return; - } - - IEnumerable processIds = this.GitProcessChecker.GetRunningGitProcessIds(); - if (processIds.Any()) - { - activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to git pids {string.Join(",", processIds)}", Keywords.Telemetry); - return; - } - } - - this.CountLooseObjects(out int beforeLooseObjectsCount, out long beforeLooseObjectsSize); - this.GetPackFilesInfo(out int beforePackCount, out long beforePackSize, out bool _); - - GitProcess.Result gitResult = this.RunGitCommand((process) => process.PrunePacked(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.PrunePacked)); - CreatePackResult createPackResult = this.TryCreateLooseObjectsPackFile(out int objectsAddedToPack); - - this.CountLooseObjects(out int afterLooseObjectsCount, out long afterLooseObjectsSize); - this.GetPackFilesInfo(out int afterPackCount, out long afterPackSize, out bool _); - - EventMetadata metadata = new EventMetadata(); - metadata.Add("GitObjectsRoot", this.Context.Enlistment.GitObjectsRoot); - - metadata.Add("PrunedPackedExitCode", gitResult.ExitCode); - metadata.Add("StartingCount", beforeLooseObjectsCount); - metadata.Add("EndingCount", afterLooseObjectsCount); - metadata.Add("StartingPackCount", beforePackCount); - metadata.Add("EndingPackCount", afterPackCount); - - metadata.Add("StartingSize", beforeLooseObjectsSize); - metadata.Add("EndingSize", afterLooseObjectsSize); - metadata.Add("StartingPackSize", beforePackSize); - metadata.Add("EndingPackSize", afterPackSize); - - metadata.Add("RemovedCount", beforeLooseObjectsCount - afterLooseObjectsCount); - metadata.Add("LooseObjectsPutIntoPackFile", objectsAddedToPack); - metadata.Add("CreatePackResult", createPackResult.ToString()); - - if (createPackResult == CreatePackResult.CorruptObject) - { - this.ClearCorruptLooseObjects(metadata); - } - - activity.RelatedEvent(EventLevel.Informational, $"{this.Area}_{nameof(this.PerformMaintenance)}", metadata, Keywords.Telemetry); - this.SaveLastRunTimeToFile(); - } - catch (Exception e) - { - activity.RelatedWarning(this.CreateEventMetadata(e), "Failed to run LooseObjectsStep", Keywords.Telemetry); - } - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.Common.Maintenance +{ + // Performs LooseObject Maintenace + // 1. Removes loose objects that appear in packfiles + // 2. Packs loose objects into a packfile + public class LooseObjectsStep : GitMaintenanceStep + { + public const string LooseObjectsLastRunFileName = "loose-objects.time"; + private readonly bool forceRun; + + public LooseObjectsStep( + ScalarContext context, + bool requireCacheLock = true, + bool forceRun = false, + GitProcessChecker gitProcessChecker = null) + : base(context, requireCacheLock, gitProcessChecker) + { + this.forceRun = forceRun; + } + + public enum CreatePackResult + { + Succeess, + UnknownFailure, + CorruptObject + } + + public override string Area => nameof(LooseObjectsStep); + + // 50,000 was found to be the optimal time taking ~5 minutes + public int MaxLooseObjectsInPack { get; set; } = 50000; + + protected override string LastRunTimeFilePath => Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", LooseObjectsLastRunFileName); + protected override TimeSpan TimeBetweenRuns => TimeSpan.FromDays(1); + + public void CountLooseObjects(out int count, out long size) + { + count = 0; + size = 0; + + foreach (string directoryPath in this.Context.FileSystem.EnumerateDirectories(this.Context.Enlistment.GitObjectsRoot)) + { + string directoryName = directoryPath.TrimEnd(Path.DirectorySeparatorChar).Split(Path.DirectorySeparatorChar).Last(); + + if (GitObjects.IsLooseObjectsDirectory(directoryName)) + { + string dirPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, directoryPath); + List dirItems = this.Context.FileSystem.ItemsInDirectory(dirPath).ToList(); + count += dirItems.Count; + size += dirItems.Sum(item => item.Length); + } + } + } + + public IEnumerable GetBatchOfLooseObjects(int batchSize) + { + // Find loose Objects + foreach (DirectoryItemInfo directoryItemInfo in this.Context.FileSystem.ItemsInDirectory(this.Context.Enlistment.GitObjectsRoot)) + { + if (directoryItemInfo.IsDirectory) + { + string directoryName = directoryItemInfo.Name; + + if (GitObjects.IsLooseObjectsDirectory(directoryName)) + { + string[] looseObjectFileNamesInDir = this.Context.FileSystem.GetFiles(directoryItemInfo.FullName, "*"); + + foreach (string filePath in looseObjectFileNamesInDir) + { + if (!this.TryGetLooseObjectId(directoryName, filePath, out string objectId)) + { + this.Context.Tracer.RelatedWarning($"Invalid ObjectId {objectId} using directory {directoryName} and path {filePath}"); + continue; + } + + batchSize--; + yield return objectId; + + if (batchSize <= 0) + { + yield break; + } + } + } + } + } + } + + /// + /// Writes loose object Ids to streamWriter + /// + /// Writer to which SHAs are written + /// The number of loose objects SHAs written to the stream + public int WriteLooseObjectIds(StreamWriter streamWriter) + { + int count = 0; + + foreach (string objectId in this.GetBatchOfLooseObjects(this.MaxLooseObjectsInPack)) + { + streamWriter.Write(objectId + "\n"); + count++; + } + + return count; + } + + public bool TryGetLooseObjectId(string directoryName, string filePath, out string objectId) + { + objectId = directoryName + Path.GetFileName(filePath); + if (!SHA1Util.IsValidShaFormat(objectId)) + { + return false; + } + + return true; + } + + /// + /// Creates a pack file from loose objects + /// + /// The number of loose objects added to the pack file + public CreatePackResult TryCreateLooseObjectsPackFile(out int objectsAddedToPack) + { + int localObjectCount = 0; + + GitProcess.Result result = this.RunGitCommand( + (process) => process.PackObjects( + "from-loose", + this.Context.Enlistment.GitObjectsRoot, + (StreamWriter writer) => localObjectCount = this.WriteLooseObjectIds(writer)), + nameof(GitProcess.PackObjects)); + + if (result.ExitCodeIsSuccess) + { + objectsAddedToPack = localObjectCount; + return CreatePackResult.Succeess; + } + else + { + objectsAddedToPack = 0; + + if (result.Errors.Contains("is corrupt")) + { + return CreatePackResult.CorruptObject; + } + + return CreatePackResult.UnknownFailure; + } + } + + public string GetLooseObjectFileName(string objectId) + { + return Path.Combine( + this.Context.Enlistment.GitObjectsRoot, + objectId.Substring(0, 2), + objectId.Substring(2, ScalarConstants.ShaStringLength - 2)); + } + + public void ClearCorruptLooseObjects(EventMetadata metadata) + { + int numDeletedObjects = 0; + int numFailedDeletes = 0; + + // Double the batch size to look beyond the current batch for bad objects, as there + // may be more bad objects in the next batch after deleting the corrupt objects. + foreach (string objectId in this.GetBatchOfLooseObjects(2 * this.MaxLooseObjectsInPack)) + { + if (!this.Context.Repository.ObjectExists(objectId)) + { + string objectFile = this.GetLooseObjectFileName(objectId); + + if (this.Context.FileSystem.TryDeleteFile(objectFile)) + { + numDeletedObjects++; + } + else + { + numFailedDeletes++; + } + } + } + + metadata.Add("RemovedCorruptObjects", numDeletedObjects); + metadata.Add("NumFailedDeletes", numFailedDeletes); + } + + protected override void PerformMaintenance() + { + using (ITracer activity = this.Context.Tracer.StartActivity(this.Area, EventLevel.Informational, Keywords.Telemetry, metadata: null)) + { + try + { + // forceRun is only currently true for functional tests + if (!this.forceRun) + { + if (!this.EnoughTimeBetweenRuns()) + { + activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to not enough time between runs"); + return; + } + + IEnumerable processIds = this.GitProcessChecker.GetRunningGitProcessIds(); + if (processIds.Any()) + { + activity.RelatedWarning($"Skipping {nameof(LooseObjectsStep)} due to git pids {string.Join(",", processIds)}", Keywords.Telemetry); + return; + } + } + + this.CountLooseObjects(out int beforeLooseObjectsCount, out long beforeLooseObjectsSize); + this.GetPackFilesInfo(out int beforePackCount, out long beforePackSize, out bool _); + + GitProcess.Result gitResult = this.RunGitCommand((process) => process.PrunePacked(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.PrunePacked)); + CreatePackResult createPackResult = this.TryCreateLooseObjectsPackFile(out int objectsAddedToPack); + + this.CountLooseObjects(out int afterLooseObjectsCount, out long afterLooseObjectsSize); + this.GetPackFilesInfo(out int afterPackCount, out long afterPackSize, out bool _); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("GitObjectsRoot", this.Context.Enlistment.GitObjectsRoot); + + metadata.Add("PrunedPackedExitCode", gitResult.ExitCode); + metadata.Add("StartingCount", beforeLooseObjectsCount); + metadata.Add("EndingCount", afterLooseObjectsCount); + metadata.Add("StartingPackCount", beforePackCount); + metadata.Add("EndingPackCount", afterPackCount); + + metadata.Add("StartingSize", beforeLooseObjectsSize); + metadata.Add("EndingSize", afterLooseObjectsSize); + metadata.Add("StartingPackSize", beforePackSize); + metadata.Add("EndingPackSize", afterPackSize); + + metadata.Add("RemovedCount", beforeLooseObjectsCount - afterLooseObjectsCount); + metadata.Add("LooseObjectsPutIntoPackFile", objectsAddedToPack); + metadata.Add("CreatePackResult", createPackResult.ToString()); + + if (createPackResult == CreatePackResult.CorruptObject) + { + this.ClearCorruptLooseObjects(metadata); + } + + activity.RelatedEvent(EventLevel.Informational, $"{this.Area}_{nameof(this.PerformMaintenance)}", metadata, Keywords.Telemetry); + this.SaveLastRunTimeToFile(); + } + catch (Exception e) + { + activity.RelatedWarning(this.CreateEventMetadata(e), "Failed to run LooseObjectsStep", Keywords.Telemetry); + } + } + } + } +} diff --git a/Scalar.Common/Maintenance/PackfileMaintenanceStep.cs b/Scalar.Common/Maintenance/PackfileMaintenanceStep.cs index 93c3dacf9a..dc87facb86 100644 --- a/Scalar.Common/Maintenance/PackfileMaintenanceStep.cs +++ b/Scalar.Common/Maintenance/PackfileMaintenanceStep.cs @@ -1,4 +1,4 @@ -using Scalar.Common.FileSystem; +using Scalar.Common.FileSystem; using Scalar.Common.Git; using Scalar.Common.Tracing; using System; @@ -27,7 +27,7 @@ namespace Scalar.Common.Maintenance public class PackfileMaintenanceStep : GitMaintenanceStep { public const string PackfileLastRunFileName = "pack-maintenance.time"; - public const string DefaultBatchSize = "2g"; + public const string DefaultBatchSize = "2g"; private const string MultiPackIndexLock = "multi-pack-index.lock"; private readonly bool forceRun; private readonly string batchSize; @@ -106,51 +106,51 @@ protected override void PerformMaintenance() activity.RelatedWarning($"Skipping {nameof(PackfileMaintenanceStep)} due to git pids {string.Join(",", processIds)}", Keywords.Telemetry); return; } - } - - this.GetPackFilesInfo(out int beforeCount, out long beforeSize, out bool hasKeep); - - if (!hasKeep) - { - activity.RelatedWarning(this.CreateEventMetadata(), "Skipping pack maintenance due to no .keep file."); - return; } - - string multiPackIndexLockPath = Path.Combine(this.Context.Enlistment.GitPackRoot, MultiPackIndexLock); - this.Context.FileSystem.TryDeleteFile(multiPackIndexLockPath); - - this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); - - // If a LibGit2Repo is active, then it may hold handles to the .idx and .pack files we want - // to delete during the 'git multi-pack-index expire' step. If one starts during the step, - // then it can still block those deletions, but we will clean them up in the next run. By - // running CloseActiveRepos() here, we ensure that we do not run twice with the same - // LibGit2Repo active across two calls. A "new" repo should not hold handles to .idx files - // that do not have corresponding .pack files, so we will clean them up in CleanStaleIdxFiles(). - this.Context.Repository.CloseActiveRepo(); - - GitProcess.Result expireResult = this.RunGitCommand((process) => process.MultiPackIndexExpire(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.MultiPackIndexExpire)); - - this.Context.Repository.OpenRepo(); - + + this.GetPackFilesInfo(out int beforeCount, out long beforeSize, out bool hasKeep); + + if (!hasKeep) + { + activity.RelatedWarning(this.CreateEventMetadata(), "Skipping pack maintenance due to no .keep file."); + return; + } + + string multiPackIndexLockPath = Path.Combine(this.Context.Enlistment.GitPackRoot, MultiPackIndexLock); + this.Context.FileSystem.TryDeleteFile(multiPackIndexLockPath); + + this.RunGitCommand((process) => process.WriteMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.WriteMultiPackIndex)); + + // If a LibGit2Repo is active, then it may hold handles to the .idx and .pack files we want + // to delete during the 'git multi-pack-index expire' step. If one starts during the step, + // then it can still block those deletions, but we will clean them up in the next run. By + // running CloseActiveRepos() here, we ensure that we do not run twice with the same + // LibGit2Repo active across two calls. A "new" repo should not hold handles to .idx files + // that do not have corresponding .pack files, so we will clean them up in CleanStaleIdxFiles(). + this.Context.Repository.CloseActiveRepo(); + + GitProcess.Result expireResult = this.RunGitCommand((process) => process.MultiPackIndexExpire(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.MultiPackIndexExpire)); + + this.Context.Repository.OpenRepo(); + List staleIdxFiles = this.CleanStaleIdxFiles(out int numDeletionBlocked); - this.GetPackFilesInfo(out int expireCount, out long expireSize, out hasKeep); - + this.GetPackFilesInfo(out int expireCount, out long expireSize, out hasKeep); + GitProcess.Result verifyAfterExpire = this.RunGitCommand((process) => process.VerifyMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyMultiPackIndex)); - if (!this.Stopping && verifyAfterExpire.ExitCodeIsFailure) - { - this.LogErrorAndRewriteMultiPackIndex(activity); + if (!this.Stopping && verifyAfterExpire.ExitCodeIsFailure) + { + this.LogErrorAndRewriteMultiPackIndex(activity); } GitProcess.Result repackResult = this.RunGitCommand((process) => process.MultiPackIndexRepack(this.Context.Enlistment.GitObjectsRoot, this.batchSize), nameof(GitProcess.MultiPackIndexRepack)); - this.GetPackFilesInfo(out int afterCount, out long afterSize, out hasKeep); - - GitProcess.Result verifyAfterRepack = this.RunGitCommand((process) => process.VerifyMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyMultiPackIndex)); + this.GetPackFilesInfo(out int afterCount, out long afterSize, out hasKeep); + + GitProcess.Result verifyAfterRepack = this.RunGitCommand((process) => process.VerifyMultiPackIndex(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyMultiPackIndex)); - if (!this.Stopping && verifyAfterRepack.ExitCodeIsFailure) - { - this.LogErrorAndRewriteMultiPackIndex(activity); + if (!this.Stopping && verifyAfterRepack.ExitCodeIsFailure) + { + this.LogErrorAndRewriteMultiPackIndex(activity); } EventMetadata metadata = new EventMetadata(); diff --git a/Scalar.Common/Maintenance/PostFetchStep.cs b/Scalar.Common/Maintenance/PostFetchStep.cs index a39d62aebb..f9a370e890 100644 --- a/Scalar.Common/Maintenance/PostFetchStep.cs +++ b/Scalar.Common/Maintenance/PostFetchStep.cs @@ -1,66 +1,66 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Scalar.Common.Maintenance -{ - public class PostFetchStep : GitMaintenanceStep - { - private const string CommitGraphChainLock = "commit-graph-chain.lock"; - private List packIndexes; - - public PostFetchStep(ScalarContext context, List packIndexes, bool requireObjectCacheLock = true) - : base(context, requireObjectCacheLock) - { - this.packIndexes = packIndexes; - } - - public override string Area => "PostFetchMaintenanceStep"; - - protected override void PerformMaintenance() - { - if (this.packIndexes == null || this.packIndexes.Count == 0) - { - this.Context.Tracer.RelatedInfo(this.Area + ": Skipping commit-graph write due to no new packfiles"); - return; - } - - using (ITracer activity = this.Context.Tracer.StartActivity("TryWriteGitCommitGraph", EventLevel.Informational)) - { - string commitGraphLockPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs", CommitGraphChainLock); - this.Context.FileSystem.TryDeleteFile(commitGraphLockPath); - - GitProcess.Result writeResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, this.packIndexes), nameof(GitProcess.WriteCommitGraph)); - - StringBuilder sb = new StringBuilder(); - string commitGraphsDir = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs"); - - if (this.Context.FileSystem.DirectoryExists(commitGraphsDir)) - { - foreach (DirectoryItemInfo info in this.Context.FileSystem.ItemsInDirectory(commitGraphsDir)) - { - sb.Append(info.Name); - sb.Append(";"); - } - } - - activity.RelatedInfo($"commit-graph list after write: {sb}"); - - if (writeResult.ExitCodeIsFailure) - { - this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); - } - - GitProcess.Result verifyResult = this.RunGitCommand((process) => process.VerifyCommitGraph(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyCommitGraph)); - - if (!this.Stopping && verifyResult.ExitCodeIsFailure) - { - this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); - } - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Scalar.Common.Maintenance +{ + public class PostFetchStep : GitMaintenanceStep + { + private const string CommitGraphChainLock = "commit-graph-chain.lock"; + private List packIndexes; + + public PostFetchStep(ScalarContext context, List packIndexes, bool requireObjectCacheLock = true) + : base(context, requireObjectCacheLock) + { + this.packIndexes = packIndexes; + } + + public override string Area => "PostFetchMaintenanceStep"; + + protected override void PerformMaintenance() + { + if (this.packIndexes == null || this.packIndexes.Count == 0) + { + this.Context.Tracer.RelatedInfo(this.Area + ": Skipping commit-graph write due to no new packfiles"); + return; + } + + using (ITracer activity = this.Context.Tracer.StartActivity("TryWriteGitCommitGraph", EventLevel.Informational)) + { + string commitGraphLockPath = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs", CommitGraphChainLock); + this.Context.FileSystem.TryDeleteFile(commitGraphLockPath); + + GitProcess.Result writeResult = this.RunGitCommand((process) => process.WriteCommitGraph(this.Context.Enlistment.GitObjectsRoot, this.packIndexes), nameof(GitProcess.WriteCommitGraph)); + + StringBuilder sb = new StringBuilder(); + string commitGraphsDir = Path.Combine(this.Context.Enlistment.GitObjectsRoot, "info", "commit-graphs"); + + if (this.Context.FileSystem.DirectoryExists(commitGraphsDir)) + { + foreach (DirectoryItemInfo info in this.Context.FileSystem.ItemsInDirectory(commitGraphsDir)) + { + sb.Append(info.Name); + sb.Append(";"); + } + } + + activity.RelatedInfo($"commit-graph list after write: {sb}"); + + if (writeResult.ExitCodeIsFailure) + { + this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); + } + + GitProcess.Result verifyResult = this.RunGitCommand((process) => process.VerifyCommitGraph(this.Context.Enlistment.GitObjectsRoot), nameof(GitProcess.VerifyCommitGraph)); + + if (!this.Stopping && verifyResult.ExitCodeIsFailure) + { + this.LogErrorAndRewriteCommitGraph(activity, this.packIndexes); + } + } + } + } +} diff --git a/Scalar.Common/Maintenance/PrefetchStep.cs b/Scalar.Common/Maintenance/PrefetchStep.cs index 7ec5ace453..8011f776b5 100644 --- a/Scalar.Common/Maintenance/PrefetchStep.cs +++ b/Scalar.Common/Maintenance/PrefetchStep.cs @@ -1,369 +1,369 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.Common.Maintenance -{ - public class PrefetchStep : GitMaintenanceStep - { - private const int IoFailureRetryDelayMS = 50; - private const int LockWaitTimeMs = 100; - private const int WaitingOnLockLogThreshold = 50; - private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; - private readonly TimeSpan timeBetweenPrefetches = TimeSpan.FromMinutes(70); - - public PrefetchStep(ScalarContext context, GitObjects gitObjects, bool requireCacheLock = true) - : base(context, requireCacheLock) - { - this.GitObjects = gitObjects; - } - - public override string Area => "PrefetchStep"; - - protected GitObjects GitObjects { get; } - - public bool TryPrefetchCommitsAndTrees(out string error, GitProcess gitProcess = null) - { - if (gitProcess == null) - { - gitProcess = new GitProcess(this.Context.Enlistment); - } - - List packIndexes; - - // We take our own lock here to keep background and foreground prefetches - // from running at the same time. - using (FileBasedLock prefetchLock = ScalarPlatform.Instance.CreateFileBasedLock( - this.Context.FileSystem, - this.Context.Tracer, - Path.Combine(this.Context.Enlistment.GitPackRoot, PrefetchCommitsAndTreesLock))) - { - WaitUntilLockIsAcquired(this.Context.Tracer, prefetchLock); - long maxGoodTimeStamp; - - this.GitObjects.DeleteStaleTempPrefetchPackAndIdxs(); - this.GitObjects.DeleteTemporaryFiles(); - - if (!this.TryGetMaxGoodPrefetchTimestamp(out maxGoodTimeStamp, out error)) - { - return false; - } - - if (!this.GitObjects.TryDownloadPrefetchPacks(gitProcess, maxGoodTimeStamp, out packIndexes)) - { - error = "Failed to download prefetch packs"; - return false; - } - - this.UpdateKeepPacks(); - } - - this.SchedulePostFetchJob(packIndexes); - - return true; - } - - protected override void PerformMaintenance() - { - long last; - string error = null; - - if (!this.TryGetMaxGoodPrefetchTimestamp(out last, out error)) - { - this.Context.Tracer.RelatedError(error); - return; - } - - DateTime lastDateTime = EpochConverter.FromUnixEpochSeconds(last); - DateTime now = DateTime.UtcNow; - - if (now <= lastDateTime + this.timeBetweenPrefetches) - { - this.Context.Tracer.RelatedInfo(this.Area + ": Skipping prefetch since most-recent prefetch ({0}) is too close to now ({1})", lastDateTime, now); - return; - } - - this.RunGitCommand( - process => - { - this.TryPrefetchCommitsAndTrees(out error, process); - return null; - }, - nameof(this.TryPrefetchCommitsAndTrees)); - - if (!string.IsNullOrEmpty(error)) - { - this.Context.Tracer.RelatedWarning( - metadata: this.CreateEventMetadata(), - message: $"{nameof(this.TryPrefetchCommitsAndTrees)} failed with error '{error}'", - keywords: Keywords.Telemetry); - } - } - - private static long? GetTimestamp(string packName) - { - string filename = Path.GetFileName(packName); - if (!filename.StartsWith(ScalarConstants.PrefetchPackPrefix)) - { - return null; - } - - string[] parts = filename.Split('-'); - long parsed; - if (parts.Length > 1 && long.TryParse(parts[1], out parsed)) - { - return parsed; - } - - return null; - } - - private static void WaitUntilLockIsAcquired(ITracer tracer, FileBasedLock fileBasedLock) - { - int attempt = 0; - while (!fileBasedLock.TryAcquireLock()) - { - Thread.Sleep(LockWaitTimeMs); - ++attempt; - if (attempt == WaitingOnLockLogThreshold) - { - attempt = 0; - tracer.RelatedInfo("WaitUntilLockIsAcquired: Waiting to acquire prefetch lock"); - } - } - } - - private bool TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimestamp, out string error) - { - this.Context.FileSystem.CreateDirectory(this.Context.Enlistment.GitPackRoot); - - string[] packs = this.GitObjects.ReadPackFileNames(this.Context.Enlistment.GitPackRoot, ScalarConstants.PrefetchPackPrefix); - List orderedPacks = packs - .Where(pack => GetTimestamp(pack).HasValue) - .Select(pack => new PrefetchPackInfo(GetTimestamp(pack).Value, pack)) - .OrderBy(packInfo => packInfo.Timestamp) - .ToList(); - - maxGoodTimestamp = -1; - - int firstBadPack = -1; - for (int i = 0; i < orderedPacks.Count; ++i) - { - long timestamp = orderedPacks[i].Timestamp; - string packPath = orderedPacks[i].Path; - string idxPath = Path.ChangeExtension(packPath, ".idx"); - if (!this.Context.FileSystem.FileExists(idxPath)) - { - EventMetadata metadata = this.CreateEventMetadata(); - metadata.Add("pack", packPath); - metadata.Add("idxPath", idxPath); - metadata.Add("timestamp", timestamp); - GitProcess.Result indexResult = this.RunGitCommand(process => this.GitObjects.IndexPackFile(packPath, process), nameof(this.GitObjects.IndexPackFile)); - - if (indexResult.ExitCodeIsFailure) - { - firstBadPack = i; - - this.Context.Tracer.RelatedWarning(metadata, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and failed to regenerate idx"); - break; - } - else - { - maxGoodTimestamp = timestamp; - - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and regenerated idx"); - this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_RebuildIdx", metadata); - } - } - else - { - maxGoodTimestamp = timestamp; - } - } - - if (this.Stopping) - { - throw new StoppingException(); - } - - if (firstBadPack != -1) - { - const int MaxDeleteRetries = 200; // 200 * IoFailureRetryDelayMS (50ms) = 10 seconds - const int RetryLoggingThreshold = 40; // 40 * IoFailureRetryDelayMS (50ms) = 2 seconds - - // Before we delete _any_ pack-files, we need to delete the multi-pack-index, which - // may refer to those packs. - - EventMetadata metadata = this.CreateEventMetadata(); - string midxPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); - metadata.Add("path", midxPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting multi-pack-index"); - this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteMultiPack_index", metadata); - - if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, midxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) - { - error = $"Unable to delete {midxPath}"; - return false; - } - - // Delete packs and indexes in reverse order so that if prefetch is killed, subseqeuent prefetch commands will - // find the right starting spot. - for (int i = orderedPacks.Count - 1; i >= firstBadPack; --i) - { - if (this.Stopping) - { - throw new StoppingException(); - } - - string packPath = orderedPacks[i].Path; - string idxPath = Path.ChangeExtension(packPath, ".idx"); - - metadata = this.CreateEventMetadata(); - metadata.Add("path", idxPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad idx file"); - this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadIdx", metadata); - - // We need to close the LibGit2 repo data in order to delete .idx files. - // Close inside the loop to only close if necessary, reopen outside the loop - // to minimize initializations. - this.Context.Repository.CloseActiveRepo(); - - if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, idxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) - { - error = $"Unable to delete {idxPath}"; - return false; - } - - metadata = this.CreateEventMetadata(); - metadata.Add("path", packPath); - metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad pack file"); - this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadPack", metadata); - - if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, packPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) - { - error = $"Unable to delete {packPath}"; - return false; - } - } - - this.Context.Repository.OpenRepo(); - } - - error = null; - return true; - } - - private void SchedulePostFetchJob(List packIndexes) - { - if (packIndexes.Count == 0) - { - return; - } - - // We make a best-effort request to run MIDX and commit-graph writes - using (NamedPipeClient pipeClient = new NamedPipeClient(this.Context.Enlistment.NamedPipeName)) - { - if (!pipeClient.Connect()) - { - this.Context.Tracer.RelatedWarning( - metadata: this.CreateEventMetadata(), - message: "Failed to connect to Scalar.Mount process. Skipping post-fetch job request.", - keywords: Keywords.Telemetry); - return; - } - - NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(packIndexes); - if (pipeClient.TrySendRequest(request.CreateMessage())) - { - NamedPipeMessages.Message response; - - if (pipeClient.TryReadResponse(out response)) - { - this.Context.Tracer.RelatedInfo("Requested post-fetch job with resonse '{0}'", response.Header); - } - else - { - this.Context.Tracer.RelatedWarning( - metadata: this.CreateEventMetadata(), - message: "Requested post-fetch job failed to respond", - keywords: Keywords.Telemetry); - } - } - else - { - this.Context.Tracer.RelatedWarning( - metadata: this.CreateEventMetadata(), - message: "Message to named pipe failed to send, skipping post-fetch job request.", - keywords: Keywords.Telemetry); - } - } - } - - /// - /// Ensure the prefetch pack with most-recent timestamp has an associated - /// ".keep" file. This prevents any Git command from deleting the pack. - /// - /// Delete the previous ".keep" file(s) so that pack can be deleted when they - /// are not the most-recent pack. - /// - private void UpdateKeepPacks() - { - if (!this.TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimeStamp, out string error)) - { - return; - } - - string prefix = $"prefetch-{maxGoodTimeStamp}-"; - - DirectoryItemInfo info = this.Context - .FileSystem - .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) - .Where(item => item.Name.StartsWith(prefix) - && string.Equals(Path.GetExtension(item.Name), ".pack", StringComparison.OrdinalIgnoreCase)) - .FirstOrDefault(); - if (info == null) - { - this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Could not find latest prefetch pack, starting with {prefix}"); - return; - } - - string newKeepFile = Path.ChangeExtension(info.FullName, ".keep"); - - if (!this.Context.FileSystem.TryWriteAllText(newKeepFile, string.Empty)) - { - this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Failed to create .keep file at {newKeepFile}"); - return; - } - - foreach (string keepFile in this.Context - .FileSystem - .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) - .Where(item => item.Name.EndsWith(".keep")) - .Select(item => item.FullName)) - { - if (!keepFile.Equals(newKeepFile)) - { - this.Context.FileSystem.TryDeleteFile(keepFile); - } - } - } - - private class PrefetchPackInfo - { - public PrefetchPackInfo(long timestamp, string path) - { - this.Timestamp = timestamp; - this.Path = path; - } - - public long Timestamp { get; } - public string Path { get; } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.Common.Maintenance +{ + public class PrefetchStep : GitMaintenanceStep + { + private const int IoFailureRetryDelayMS = 50; + private const int LockWaitTimeMs = 100; + private const int WaitingOnLockLogThreshold = 50; + private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; + private readonly TimeSpan timeBetweenPrefetches = TimeSpan.FromMinutes(70); + + public PrefetchStep(ScalarContext context, GitObjects gitObjects, bool requireCacheLock = true) + : base(context, requireCacheLock) + { + this.GitObjects = gitObjects; + } + + public override string Area => "PrefetchStep"; + + protected GitObjects GitObjects { get; } + + public bool TryPrefetchCommitsAndTrees(out string error, GitProcess gitProcess = null) + { + if (gitProcess == null) + { + gitProcess = new GitProcess(this.Context.Enlistment); + } + + List packIndexes; + + // We take our own lock here to keep background and foreground prefetches + // from running at the same time. + using (FileBasedLock prefetchLock = ScalarPlatform.Instance.CreateFileBasedLock( + this.Context.FileSystem, + this.Context.Tracer, + Path.Combine(this.Context.Enlistment.GitPackRoot, PrefetchCommitsAndTreesLock))) + { + WaitUntilLockIsAcquired(this.Context.Tracer, prefetchLock); + long maxGoodTimeStamp; + + this.GitObjects.DeleteStaleTempPrefetchPackAndIdxs(); + this.GitObjects.DeleteTemporaryFiles(); + + if (!this.TryGetMaxGoodPrefetchTimestamp(out maxGoodTimeStamp, out error)) + { + return false; + } + + if (!this.GitObjects.TryDownloadPrefetchPacks(gitProcess, maxGoodTimeStamp, out packIndexes)) + { + error = "Failed to download prefetch packs"; + return false; + } + + this.UpdateKeepPacks(); + } + + this.SchedulePostFetchJob(packIndexes); + + return true; + } + + protected override void PerformMaintenance() + { + long last; + string error = null; + + if (!this.TryGetMaxGoodPrefetchTimestamp(out last, out error)) + { + this.Context.Tracer.RelatedError(error); + return; + } + + DateTime lastDateTime = EpochConverter.FromUnixEpochSeconds(last); + DateTime now = DateTime.UtcNow; + + if (now <= lastDateTime + this.timeBetweenPrefetches) + { + this.Context.Tracer.RelatedInfo(this.Area + ": Skipping prefetch since most-recent prefetch ({0}) is too close to now ({1})", lastDateTime, now); + return; + } + + this.RunGitCommand( + process => + { + this.TryPrefetchCommitsAndTrees(out error, process); + return null; + }, + nameof(this.TryPrefetchCommitsAndTrees)); + + if (!string.IsNullOrEmpty(error)) + { + this.Context.Tracer.RelatedWarning( + metadata: this.CreateEventMetadata(), + message: $"{nameof(this.TryPrefetchCommitsAndTrees)} failed with error '{error}'", + keywords: Keywords.Telemetry); + } + } + + private static long? GetTimestamp(string packName) + { + string filename = Path.GetFileName(packName); + if (!filename.StartsWith(ScalarConstants.PrefetchPackPrefix)) + { + return null; + } + + string[] parts = filename.Split('-'); + long parsed; + if (parts.Length > 1 && long.TryParse(parts[1], out parsed)) + { + return parsed; + } + + return null; + } + + private static void WaitUntilLockIsAcquired(ITracer tracer, FileBasedLock fileBasedLock) + { + int attempt = 0; + while (!fileBasedLock.TryAcquireLock()) + { + Thread.Sleep(LockWaitTimeMs); + ++attempt; + if (attempt == WaitingOnLockLogThreshold) + { + attempt = 0; + tracer.RelatedInfo("WaitUntilLockIsAcquired: Waiting to acquire prefetch lock"); + } + } + } + + private bool TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimestamp, out string error) + { + this.Context.FileSystem.CreateDirectory(this.Context.Enlistment.GitPackRoot); + + string[] packs = this.GitObjects.ReadPackFileNames(this.Context.Enlistment.GitPackRoot, ScalarConstants.PrefetchPackPrefix); + List orderedPacks = packs + .Where(pack => GetTimestamp(pack).HasValue) + .Select(pack => new PrefetchPackInfo(GetTimestamp(pack).Value, pack)) + .OrderBy(packInfo => packInfo.Timestamp) + .ToList(); + + maxGoodTimestamp = -1; + + int firstBadPack = -1; + for (int i = 0; i < orderedPacks.Count; ++i) + { + long timestamp = orderedPacks[i].Timestamp; + string packPath = orderedPacks[i].Path; + string idxPath = Path.ChangeExtension(packPath, ".idx"); + if (!this.Context.FileSystem.FileExists(idxPath)) + { + EventMetadata metadata = this.CreateEventMetadata(); + metadata.Add("pack", packPath); + metadata.Add("idxPath", idxPath); + metadata.Add("timestamp", timestamp); + GitProcess.Result indexResult = this.RunGitCommand(process => this.GitObjects.IndexPackFile(packPath, process), nameof(this.GitObjects.IndexPackFile)); + + if (indexResult.ExitCodeIsFailure) + { + firstBadPack = i; + + this.Context.Tracer.RelatedWarning(metadata, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and failed to regenerate idx"); + break; + } + else + { + maxGoodTimestamp = timestamp; + + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}: Found pack file that's missing idx file, and regenerated idx"); + this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_RebuildIdx", metadata); + } + } + else + { + maxGoodTimestamp = timestamp; + } + } + + if (this.Stopping) + { + throw new StoppingException(); + } + + if (firstBadPack != -1) + { + const int MaxDeleteRetries = 200; // 200 * IoFailureRetryDelayMS (50ms) = 10 seconds + const int RetryLoggingThreshold = 40; // 40 * IoFailureRetryDelayMS (50ms) = 2 seconds + + // Before we delete _any_ pack-files, we need to delete the multi-pack-index, which + // may refer to those packs. + + EventMetadata metadata = this.CreateEventMetadata(); + string midxPath = Path.Combine(this.Context.Enlistment.GitPackRoot, "multi-pack-index"); + metadata.Add("path", midxPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting multi-pack-index"); + this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteMultiPack_index", metadata); + + if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, midxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) + { + error = $"Unable to delete {midxPath}"; + return false; + } + + // Delete packs and indexes in reverse order so that if prefetch is killed, subseqeuent prefetch commands will + // find the right starting spot. + for (int i = orderedPacks.Count - 1; i >= firstBadPack; --i) + { + if (this.Stopping) + { + throw new StoppingException(); + } + + string packPath = orderedPacks[i].Path; + string idxPath = Path.ChangeExtension(packPath, ".idx"); + + metadata = this.CreateEventMetadata(); + metadata.Add("path", idxPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad idx file"); + this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadIdx", metadata); + + // We need to close the LibGit2 repo data in order to delete .idx files. + // Close inside the loop to only close if necessary, reopen outside the loop + // to minimize initializations. + this.Context.Repository.CloseActiveRepo(); + + if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, idxPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) + { + error = $"Unable to delete {idxPath}"; + return false; + } + + metadata = this.CreateEventMetadata(); + metadata.Add("path", packPath); + metadata.Add(TracingConstants.MessageKey.InfoMessage, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)} deleting bad pack file"); + this.Context.Tracer.RelatedEvent(EventLevel.Informational, $"{nameof(this.TryGetMaxGoodPrefetchTimestamp)}_DeleteBadPack", metadata); + + if (!this.Context.FileSystem.TryWaitForDelete(this.Context.Tracer, packPath, IoFailureRetryDelayMS, MaxDeleteRetries, RetryLoggingThreshold)) + { + error = $"Unable to delete {packPath}"; + return false; + } + } + + this.Context.Repository.OpenRepo(); + } + + error = null; + return true; + } + + private void SchedulePostFetchJob(List packIndexes) + { + if (packIndexes.Count == 0) + { + return; + } + + // We make a best-effort request to run MIDX and commit-graph writes + using (NamedPipeClient pipeClient = new NamedPipeClient(this.Context.Enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + this.Context.Tracer.RelatedWarning( + metadata: this.CreateEventMetadata(), + message: "Failed to connect to Scalar.Mount process. Skipping post-fetch job request.", + keywords: Keywords.Telemetry); + return; + } + + NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(packIndexes); + if (pipeClient.TrySendRequest(request.CreateMessage())) + { + NamedPipeMessages.Message response; + + if (pipeClient.TryReadResponse(out response)) + { + this.Context.Tracer.RelatedInfo("Requested post-fetch job with resonse '{0}'", response.Header); + } + else + { + this.Context.Tracer.RelatedWarning( + metadata: this.CreateEventMetadata(), + message: "Requested post-fetch job failed to respond", + keywords: Keywords.Telemetry); + } + } + else + { + this.Context.Tracer.RelatedWarning( + metadata: this.CreateEventMetadata(), + message: "Message to named pipe failed to send, skipping post-fetch job request.", + keywords: Keywords.Telemetry); + } + } + } + + /// + /// Ensure the prefetch pack with most-recent timestamp has an associated + /// ".keep" file. This prevents any Git command from deleting the pack. + /// + /// Delete the previous ".keep" file(s) so that pack can be deleted when they + /// are not the most-recent pack. + /// + private void UpdateKeepPacks() + { + if (!this.TryGetMaxGoodPrefetchTimestamp(out long maxGoodTimeStamp, out string error)) + { + return; + } + + string prefix = $"prefetch-{maxGoodTimeStamp}-"; + + DirectoryItemInfo info = this.Context + .FileSystem + .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) + .Where(item => item.Name.StartsWith(prefix) + && string.Equals(Path.GetExtension(item.Name), ".pack", StringComparison.OrdinalIgnoreCase)) + .FirstOrDefault(); + if (info == null) + { + this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Could not find latest prefetch pack, starting with {prefix}"); + return; + } + + string newKeepFile = Path.ChangeExtension(info.FullName, ".keep"); + + if (!this.Context.FileSystem.TryWriteAllText(newKeepFile, string.Empty)) + { + this.Context.Tracer.RelatedWarning(this.CreateEventMetadata(), $"Failed to create .keep file at {newKeepFile}"); + return; + } + + foreach (string keepFile in this.Context + .FileSystem + .ItemsInDirectory(this.Context.Enlistment.GitPackRoot) + .Where(item => item.Name.EndsWith(".keep")) + .Select(item => item.FullName)) + { + if (!keepFile.Equals(newKeepFile)) + { + this.Context.FileSystem.TryDeleteFile(keepFile); + } + } + } + + private class PrefetchPackInfo + { + public PrefetchPackInfo(long timestamp, string path) + { + this.Timestamp = timestamp; + this.Path = path; + } + + public long Timestamp { get; } + public string Path { get; } + } + } +} diff --git a/Scalar.Common/NamedPipes/BrokenPipeException.cs b/Scalar.Common/NamedPipes/BrokenPipeException.cs index 0b1e1f692f..5a958e2196 100644 --- a/Scalar.Common/NamedPipes/BrokenPipeException.cs +++ b/Scalar.Common/NamedPipes/BrokenPipeException.cs @@ -1,13 +1,13 @@ -using System; -using System.IO; - -namespace Scalar.Common.NamedPipes -{ - public class BrokenPipeException : Exception - { - public BrokenPipeException(string message, IOException innerException) - : base(message, innerException) - { - } - } -} +using System; +using System.IO; + +namespace Scalar.Common.NamedPipes +{ + public class BrokenPipeException : Exception + { + public BrokenPipeException(string message, IOException innerException) + : base(message, innerException) + { + } + } +} diff --git a/Scalar.Common/NamedPipes/NamedPipeClient.cs b/Scalar.Common/NamedPipes/NamedPipeClient.cs index 2dc81ebc63..ddf7428462 100644 --- a/Scalar.Common/NamedPipes/NamedPipeClient.cs +++ b/Scalar.Common/NamedPipes/NamedPipeClient.cs @@ -1,138 +1,138 @@ -using System; -using System.IO; -using System.IO.Pipes; - -namespace Scalar.Common.NamedPipes -{ - public class NamedPipeClient : IDisposable - { - private string pipeName; - private NamedPipeClientStream clientStream; - private NamedPipeStreamReader reader; - private NamedPipeStreamWriter writer; - - public NamedPipeClient(string pipeName) - { - this.pipeName = pipeName; - } - - public bool Connect(int timeoutMilliseconds = 3000) - { - if (this.clientStream != null) - { - throw new InvalidOperationException(); - } - - try - { - this.clientStream = new NamedPipeClientStream(this.pipeName); - this.clientStream.Connect(timeoutMilliseconds); - } - catch (TimeoutException) - { - return false; - } - catch (IOException) - { - return false; - } - - this.reader = new NamedPipeStreamReader(this.clientStream); - this.writer = new NamedPipeStreamWriter(this.clientStream); - - return true; - } - - public bool TrySendRequest(NamedPipeMessages.Message message) - { - try - { - this.SendRequest(message); - return true; - } - catch (BrokenPipeException) - { - } - - return false; - } - - public void SendRequest(NamedPipeMessages.Message message) - { - this.SendRequest(message.ToString()); - } - - public void SendRequest(string message) - { - this.ValidateConnection(); - - try - { - this.writer.WriteMessage(message); - } - catch (IOException e) - { - throw new BrokenPipeException("Unable to send: " + message, e); - } - } - - public string ReadRawResponse() - { - try - { - string response = this.reader.ReadMessage(); - if (response == null) - { - throw new BrokenPipeException("Unable to read from pipe", null); - } - - return response; - } - catch (IOException e) - { - throw new BrokenPipeException("Unable to read from pipe", e); - } - } - - public NamedPipeMessages.Message ReadResponse() - { - return NamedPipeMessages.Message.FromString(this.ReadRawResponse()); - } - - public bool TryReadResponse(out NamedPipeMessages.Message message) - { - try - { - message = NamedPipeMessages.Message.FromString(this.ReadRawResponse()); - return true; - } - catch (BrokenPipeException) - { - message = null; - return false; - } - } - - public void Dispose() - { - this.ValidateConnection(); - - if (this.clientStream != null) - { - this.clientStream.Dispose(); - this.clientStream = null; - } - - this.reader = null; - this.writer = null; - } - - private void ValidateConnection() - { - if (this.clientStream == null) - { - throw new InvalidOperationException("There is no connection"); - } - } - } -} +using System; +using System.IO; +using System.IO.Pipes; + +namespace Scalar.Common.NamedPipes +{ + public class NamedPipeClient : IDisposable + { + private string pipeName; + private NamedPipeClientStream clientStream; + private NamedPipeStreamReader reader; + private NamedPipeStreamWriter writer; + + public NamedPipeClient(string pipeName) + { + this.pipeName = pipeName; + } + + public bool Connect(int timeoutMilliseconds = 3000) + { + if (this.clientStream != null) + { + throw new InvalidOperationException(); + } + + try + { + this.clientStream = new NamedPipeClientStream(this.pipeName); + this.clientStream.Connect(timeoutMilliseconds); + } + catch (TimeoutException) + { + return false; + } + catch (IOException) + { + return false; + } + + this.reader = new NamedPipeStreamReader(this.clientStream); + this.writer = new NamedPipeStreamWriter(this.clientStream); + + return true; + } + + public bool TrySendRequest(NamedPipeMessages.Message message) + { + try + { + this.SendRequest(message); + return true; + } + catch (BrokenPipeException) + { + } + + return false; + } + + public void SendRequest(NamedPipeMessages.Message message) + { + this.SendRequest(message.ToString()); + } + + public void SendRequest(string message) + { + this.ValidateConnection(); + + try + { + this.writer.WriteMessage(message); + } + catch (IOException e) + { + throw new BrokenPipeException("Unable to send: " + message, e); + } + } + + public string ReadRawResponse() + { + try + { + string response = this.reader.ReadMessage(); + if (response == null) + { + throw new BrokenPipeException("Unable to read from pipe", null); + } + + return response; + } + catch (IOException e) + { + throw new BrokenPipeException("Unable to read from pipe", e); + } + } + + public NamedPipeMessages.Message ReadResponse() + { + return NamedPipeMessages.Message.FromString(this.ReadRawResponse()); + } + + public bool TryReadResponse(out NamedPipeMessages.Message message) + { + try + { + message = NamedPipeMessages.Message.FromString(this.ReadRawResponse()); + return true; + } + catch (BrokenPipeException) + { + message = null; + return false; + } + } + + public void Dispose() + { + this.ValidateConnection(); + + if (this.clientStream != null) + { + this.clientStream.Dispose(); + this.clientStream = null; + } + + this.reader = null; + this.writer = null; + } + + private void ValidateConnection() + { + if (this.clientStream == null) + { + throw new InvalidOperationException("There is no connection"); + } + } + } +} diff --git a/Scalar.Common/NamedPipes/NamedPipeMessages.cs b/Scalar.Common/NamedPipes/NamedPipeMessages.cs index d6629ba513..75860142ec 100644 --- a/Scalar.Common/NamedPipes/NamedPipeMessages.cs +++ b/Scalar.Common/NamedPipes/NamedPipeMessages.cs @@ -1,400 +1,400 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Generic; - -namespace Scalar.Common.NamedPipes -{ - /// - /// Define messages used to communicate via the named-pipe in Scalar. - /// - /// - /// This class is defined as partial so that Scalar.Hooks - /// can compile the portions of it that it cares about (see LockedNamedPipeMessages). - /// - public static partial class NamedPipeMessages - { - public const string UnknownRequest = "UnknownRequest"; - public const string UnknownScalarState = "UnknownScalarState"; - public const string MountNotReadyResult = "MountNotReady"; - - private const string ResponseSuffix = "Response"; - private const char MessageSeparator = '|'; - - public enum CompletionState - { - NotCompleted, - Success, - Failure - } - - public static class GetStatus - { - public const string Request = "GetStatus"; - public const string Mounting = "Mounting"; - public const string Ready = "Ready"; - public const string Unmounting = "Unmounting"; - public const string MountFailed = "MountFailed"; - - public class Response - { - public string MountStatus { get; set; } - public string EnlistmentRoot { get; set; } - public string LocalCacheRoot { get; set; } - public string RepoUrl { get; set; } - public string CacheServer { get; set; } - public int BackgroundOperationCount { get; set; } - public string DiskLayoutVersion { get; set; } - - public static Response FromJson(string json) - { - return JsonConvert.DeserializeObject(json); - } - - public string ToJson() - { - return JsonConvert.SerializeObject(this); - } - } - } - - public static class Unmount - { - public const string Request = "Unmount"; - public const string NotMounted = "NotMounted"; - public const string Acknowledged = "Ack"; - public const string Completed = "Complete"; - public const string AlreadyUnmounting = "AlreadyUnmounting"; - public const string MountFailed = "MountFailed"; - } - - public static class ModifiedPaths - { - public const string ListRequest = "MPL"; - public const string InvalidVersion = "InvalidVersion"; - public const string SuccessResult = "S"; - public const string CurrentVersion = "1"; - - public class Request - { - public Request(Message message) - { - this.Version = message.Body; - } - - public string Version { get; } - } - - public class Response - { - public Response(string result, string data = "") - { - this.Result = result; - this.Data = data; - } - - public string Result { get; } - public string Data { get; } - - public Message CreateMessage() - { - return new Message(this.Result, this.Data); - } - } - } - - public static class DownloadObject - { - public const string DownloadRequest = "DLO"; - public const string SuccessResult = "S"; - public const string DownloadFailed = "F"; - public const string InvalidSHAResult = "InvalidSHA"; - - public class Request - { - public Request(Message message) - { - this.RequestSha = message.Body; - } - - public string RequestSha { get; } - - public Message CreateMessage() - { - return new Message(DownloadRequest, this.RequestSha); - } - } - - public class Response - { - public Response(string result) - { - this.Result = result; - } - - public string Result { get; } - - public Message CreateMessage() - { - return new Message(this.Result, null); - } - } - } - - public static class PostIndexChanged - { - public const string NotificationRequest = "PICN"; - public const string SuccessResult = "S"; - public const string FailureResult = "F"; - - public class Request - { - public Request(Message message) - { - if (message.Body.Length != 2) - { - throw new InvalidOperationException($"Invalid PostIndexChanged message. Expected 2 characters, got: {message.Body.Length} from message: '{message.Body}'"); - } - - this.UpdatedWorkingDirectory = message.Body[0] == '1'; - this.UpdatedSkipWorktreeBits = message.Body[1] == '1'; - } - - public bool UpdatedWorkingDirectory { get; } - - public bool UpdatedSkipWorktreeBits { get; } - } - - public class Response - { - public Response(string result) - { - this.Result = result; - } - - public string Result { get; } - - public Message CreateMessage() - { - return new Message(this.Result, null); - } - } - } - - public static class RunPostFetchJob - { - public const string PostFetchJob = "PostFetch"; - public const string QueuedResult = "Queued"; - public const string MountNotReadyResult = "MountNotReady"; - - public class Request - { - public Request(List packIndexes) - { - this.PackIndexList = JsonConvert.SerializeObject(packIndexes); - } - - public Request(Message message) - { - this.PackIndexList = message.Body; - } - - /// - /// The PackIndexList data is a JSON-formatted list of strings, - /// where each string is the name of an IDX file in the shared - /// object cache. - /// - public string PackIndexList { get; set; } - - public Message CreateMessage() - { - return new Message(PostFetchJob, this.PackIndexList); - } - } - - public class Response - { - public Response(string result) - { - this.Result = result; - } - - public string Result { get; } - - public Message CreateMessage() - { - return new Message(this.Result, null); - } - } - } - - public static class Notification - { - public class Request - { - public const string Header = nameof(Notification); - - public enum Identifier - { - AutomountStart, - MountSuccess, - MountFailure - } - - public Identifier Id { get; set; } - - public string Title { get; set; } - - public string Message { get; set; } - - public string Enlistment { get; set; } - - public int EnlistmentCount { get; set; } - - public static Request FromMessage(Message message) - { - return JsonConvert.DeserializeObject(message.Body); - } - - public Message ToMessage() - { - return new Message(Header, JsonConvert.SerializeObject(this)); - } - } - } - - public class Message - { - public Message(string header, string body) - { - this.Header = header; - this.Body = body; - } - - public string Header { get; } - - public string Body { get; } - - public static Message FromString(string message) - { - string header = null; - string body = null; - if (!string.IsNullOrEmpty(message)) - { - string[] parts = message.Split(new[] { NamedPipeMessages.MessageSeparator }, count: 2); - header = parts[0]; - if (parts.Length > 1) - { - body = parts[1]; - } - } - - return new Message(header, body); - } - - public override string ToString() - { - string result = string.Empty; - if (!string.IsNullOrEmpty(this.Header)) - { - result = this.Header; - } - - if (this.Body != null) - { - result = result + NamedPipeMessages.MessageSeparator + this.Body; - } - - return result; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace Scalar.Common.NamedPipes +{ + /// + /// Define messages used to communicate via the named-pipe in Scalar. + /// + /// + /// This class is defined as partial so that Scalar.Hooks + /// can compile the portions of it that it cares about (see LockedNamedPipeMessages). + /// + public static partial class NamedPipeMessages + { + public const string UnknownRequest = "UnknownRequest"; + public const string UnknownScalarState = "UnknownScalarState"; + public const string MountNotReadyResult = "MountNotReady"; + + private const string ResponseSuffix = "Response"; + private const char MessageSeparator = '|'; + + public enum CompletionState + { + NotCompleted, + Success, + Failure + } + + public static class GetStatus + { + public const string Request = "GetStatus"; + public const string Mounting = "Mounting"; + public const string Ready = "Ready"; + public const string Unmounting = "Unmounting"; + public const string MountFailed = "MountFailed"; + + public class Response + { + public string MountStatus { get; set; } + public string EnlistmentRoot { get; set; } + public string LocalCacheRoot { get; set; } + public string RepoUrl { get; set; } + public string CacheServer { get; set; } + public int BackgroundOperationCount { get; set; } + public string DiskLayoutVersion { get; set; } + + public static Response FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } } } - 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; - - public CompletionState State { get; set; } - public string ErrorMessage { get; set; } - - public Message ToMessage() - { - return new Message(Header, JsonConvert.SerializeObject(this)); - } - } - } -} + public static class Unmount + { + public const string Request = "Unmount"; + public const string NotMounted = "NotMounted"; + public const string Acknowledged = "Ack"; + public const string Completed = "Complete"; + public const string AlreadyUnmounting = "AlreadyUnmounting"; + public const string MountFailed = "MountFailed"; + } + + public static class ModifiedPaths + { + public const string ListRequest = "MPL"; + public const string InvalidVersion = "InvalidVersion"; + public const string SuccessResult = "S"; + public const string CurrentVersion = "1"; + + public class Request + { + public Request(Message message) + { + this.Version = message.Body; + } + + public string Version { get; } + } + + public class Response + { + public Response(string result, string data = "") + { + this.Result = result; + this.Data = data; + } + + public string Result { get; } + public string Data { get; } + + public Message CreateMessage() + { + return new Message(this.Result, this.Data); + } + } + } + + public static class DownloadObject + { + public const string DownloadRequest = "DLO"; + public const string SuccessResult = "S"; + public const string DownloadFailed = "F"; + public const string InvalidSHAResult = "InvalidSHA"; + + public class Request + { + public Request(Message message) + { + this.RequestSha = message.Body; + } + + public string RequestSha { get; } + + public Message CreateMessage() + { + return new Message(DownloadRequest, this.RequestSha); + } + } + + public class Response + { + public Response(string result) + { + this.Result = result; + } + + public string Result { get; } + + public Message CreateMessage() + { + return new Message(this.Result, null); + } + } + } + + public static class PostIndexChanged + { + public const string NotificationRequest = "PICN"; + public const string SuccessResult = "S"; + public const string FailureResult = "F"; + + public class Request + { + public Request(Message message) + { + if (message.Body.Length != 2) + { + throw new InvalidOperationException($"Invalid PostIndexChanged message. Expected 2 characters, got: {message.Body.Length} from message: '{message.Body}'"); + } + + this.UpdatedWorkingDirectory = message.Body[0] == '1'; + this.UpdatedSkipWorktreeBits = message.Body[1] == '1'; + } + + public bool UpdatedWorkingDirectory { get; } + + public bool UpdatedSkipWorktreeBits { get; } + } + + public class Response + { + public Response(string result) + { + this.Result = result; + } + + public string Result { get; } + + public Message CreateMessage() + { + return new Message(this.Result, null); + } + } + } + + public static class RunPostFetchJob + { + public const string PostFetchJob = "PostFetch"; + public const string QueuedResult = "Queued"; + public const string MountNotReadyResult = "MountNotReady"; + + public class Request + { + public Request(List packIndexes) + { + this.PackIndexList = JsonConvert.SerializeObject(packIndexes); + } + + public Request(Message message) + { + this.PackIndexList = message.Body; + } + + /// + /// The PackIndexList data is a JSON-formatted list of strings, + /// where each string is the name of an IDX file in the shared + /// object cache. + /// + public string PackIndexList { get; set; } + + public Message CreateMessage() + { + return new Message(PostFetchJob, this.PackIndexList); + } + } + + public class Response + { + public Response(string result) + { + this.Result = result; + } + + public string Result { get; } + + public Message CreateMessage() + { + return new Message(this.Result, null); + } + } + } + + public static class Notification + { + public class Request + { + public const string Header = nameof(Notification); + + public enum Identifier + { + AutomountStart, + MountSuccess, + MountFailure + } + + public Identifier Id { get; set; } + + public string Title { get; set; } + + public string Message { get; set; } + + public string Enlistment { get; set; } + + public int EnlistmentCount { get; set; } + + public static Request FromMessage(Message message) + { + return JsonConvert.DeserializeObject(message.Body); + } + + public Message ToMessage() + { + return new Message(Header, JsonConvert.SerializeObject(this)); + } + } + } + + public class Message + { + public Message(string header, string body) + { + this.Header = header; + this.Body = body; + } + + public string Header { get; } + + public string Body { get; } + + public static Message FromString(string message) + { + string header = null; + string body = null; + if (!string.IsNullOrEmpty(message)) + { + string[] parts = message.Split(new[] { NamedPipeMessages.MessageSeparator }, count: 2); + header = parts[0]; + if (parts.Length > 1) + { + body = parts[1]; + } + } + + return new Message(header, body); + } + + public override string ToString() + { + string result = string.Empty; + if (!string.IsNullOrEmpty(this.Header)) + { + result = this.Header; + } + + if (this.Body != null) + { + result = result + NamedPipeMessages.MessageSeparator + this.Body; + } + + return result; + } + } + + 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; + + public CompletionState State { get; set; } + public string ErrorMessage { get; set; } + + public Message ToMessage() + { + return new Message(Header, JsonConvert.SerializeObject(this)); + } + } + } +} diff --git a/Scalar.Common/NamedPipes/NamedPipeServer.cs b/Scalar.Common/NamedPipes/NamedPipeServer.cs index c6e96075b7..2a009c1530 100644 --- a/Scalar.Common/NamedPipes/NamedPipeServer.cs +++ b/Scalar.Common/NamedPipes/NamedPipeServer.cs @@ -1,250 +1,250 @@ -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.IO.Pipes; -using System.Threading; - -namespace Scalar.Common.NamedPipes -{ - /// - /// The server side of a Named Pipe used for interprocess communication. - /// - /// Named Pipe protocol: - /// The client / server process sends a "message" (or line) of data as a - /// sequence of bytes terminated by a 0x3 byte (ASCII control code for - /// End of text). Text is encoded as UTF-8 to be sent as bytes across the wire. - /// - /// This format was chosen so that: - /// 1) A reasonable range of values can be transmitted across the pipe, - /// including null and bytes that represent newline characters. - /// 2) It would be easy to implement in multiple places, as we - /// have managed and native implementations. - /// - public class NamedPipeServer : IDisposable - { - private bool isStopping; - private string pipeName; - private Action handleConnection; - private ITracer tracer; - - private NamedPipeServerStream listeningPipe; - - private NamedPipeServer(string pipeName, ITracer tracer, Action handleConnection) - { - this.pipeName = pipeName; - this.tracer = tracer; - this.handleConnection = handleConnection; - this.isStopping = false; - } - - public static NamedPipeServer StartNewServer(string pipeName, ITracer tracer, Action handleRequest) - { - if (pipeName.Length > ScalarPlatform.Instance.Constants.MaxPipePathLength) - { - throw new PipeNameLengthException(string.Format("The pipe name ({0}) exceeds the max length allowed({1})", pipeName, ScalarPlatform.Instance.Constants.MaxPipePathLength)); - } - - NamedPipeServer pipeServer = new NamedPipeServer(pipeName, tracer, connection => HandleConnection(tracer, connection, handleRequest)); - pipeServer.OpenListeningPipe(); - - return pipeServer; - } - - public void Dispose() - { - this.isStopping = true; - - NamedPipeServerStream pipe = Interlocked.Exchange(ref this.listeningPipe, null); - if (pipe != null) - { - pipe.Dispose(); - } - } - - private static void HandleConnection(ITracer tracer, Connection connection, Action handleRequest) - { - while (connection.IsConnected) - { - string request = connection.ReadRequest(); - - if (request == null || - !connection.IsConnected) - { - break; - } - - handleRequest(tracer, request, connection); - } - } - - private void OpenListeningPipe() - { - try - { - if (this.listeningPipe != null) - { - throw new InvalidOperationException("There is already a pipe listening for a connection"); - } - - this.listeningPipe = ScalarPlatform.Instance.CreatePipeByName(this.pipeName); - this.listeningPipe.BeginWaitForConnection(this.OnNewConnection, this.listeningPipe); - } - catch (Exception e) - { - this.LogErrorAndExit("OpenListeningPipe caught unhandled exception, exiting process", e); - } - } - - private void OnNewConnection(IAsyncResult ar) - { - if (!this.isStopping) - { - this.OnNewConnection(ar, createNewThreadIfSynchronous: true); - } - } - - private void OnNewConnection(IAsyncResult ar, bool createNewThreadIfSynchronous) - { - if (createNewThreadIfSynchronous && - ar.CompletedSynchronously) - { - // if this callback got called synchronously, we must not do any blocking IO on this thread - // or we will block the original caller. Moving to a new thread so that it will be safe - // to call a blocking Read on the NamedPipeServerStream - - new Thread(() => this.OnNewConnection(ar, createNewThreadIfSynchronous: false)).Start(); - return; - } - - this.listeningPipe = null; - bool connectionBroken = false; - - NamedPipeServerStream pipe = (NamedPipeServerStream)ar.AsyncState; - try - { - try - { - pipe.EndWaitForConnection(ar); - } - catch (IOException e) - { - connectionBroken = true; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "NamedPipeServer"); - metadata.Add("Exception", e.ToString()); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "OnNewConnection: Connection broken"); - this.tracer.RelatedEvent(EventLevel.Warning, "OnNewConnectionn_EndWaitForConnection_IOException", metadata); - } - catch (Exception e) - { - this.LogErrorAndExit("OnNewConnection caught unhandled exception, exiting process", e); - } - - if (!this.isStopping) - { - this.OpenListeningPipe(); - - if (!connectionBroken) - { - try - { - this.handleConnection(new Connection(pipe, this.tracer, () => this.isStopping)); - } - catch (Exception e) - { - this.LogErrorAndExit("Unhandled exception in connection handler", e); - } - } - } - } - finally - { - pipe.Dispose(); - } - } - - private void LogErrorAndExit(string message, Exception e) - { - if (this.tracer != null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "NamedPipeServer"); - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - this.tracer.RelatedError(metadata, message); - } - - Environment.Exit((int)ReturnCode.GenericError); - } - - public class Connection - { - private NamedPipeServerStream serverStream; - private NamedPipeStreamReader reader; - private NamedPipeStreamWriter writer; - private ITracer tracer; - private Func isStopping; - - public Connection(NamedPipeServerStream serverStream, ITracer tracer, Func isStopping) - { - this.serverStream = serverStream; - this.tracer = tracer; - this.isStopping = isStopping; - this.reader = new NamedPipeStreamReader(this.serverStream); - this.writer = new NamedPipeStreamWriter(this.serverStream); - } - - public bool IsConnected - { - get { return !this.isStopping() && this.serverStream.IsConnected; } - } - - public NamedPipeMessages.Message ReadMessage() - { - return NamedPipeMessages.Message.FromString(this.ReadRequest()); - } - - public string ReadRequest() - { - try - { - return this.reader.ReadMessage(); - } - catch (IOException e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("ExceptionMessage", e.Message); - metadata.Add("StackTrace", e.StackTrace); - this.tracer.RelatedWarning( - metadata: metadata, - message: $"Error reading message from NamedPipe: {e.Message}", - keywords: Keywords.Telemetry); - - return null; - } - } - - public virtual bool TrySendResponse(string message) - { - try - { - this.writer.WriteMessage(message); - return true; - } - catch (IOException) - { - return false; - } - } - - public bool TrySendResponse(NamedPipeMessages.Message message) - { - return this.TrySendResponse(message.ToString()); - } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; + +namespace Scalar.Common.NamedPipes +{ + /// + /// The server side of a Named Pipe used for interprocess communication. + /// + /// Named Pipe protocol: + /// The client / server process sends a "message" (or line) of data as a + /// sequence of bytes terminated by a 0x3 byte (ASCII control code for + /// End of text). Text is encoded as UTF-8 to be sent as bytes across the wire. + /// + /// This format was chosen so that: + /// 1) A reasonable range of values can be transmitted across the pipe, + /// including null and bytes that represent newline characters. + /// 2) It would be easy to implement in multiple places, as we + /// have managed and native implementations. + /// + public class NamedPipeServer : IDisposable + { + private bool isStopping; + private string pipeName; + private Action handleConnection; + private ITracer tracer; + + private NamedPipeServerStream listeningPipe; + + private NamedPipeServer(string pipeName, ITracer tracer, Action handleConnection) + { + this.pipeName = pipeName; + this.tracer = tracer; + this.handleConnection = handleConnection; + this.isStopping = false; + } + + public static NamedPipeServer StartNewServer(string pipeName, ITracer tracer, Action handleRequest) + { + if (pipeName.Length > ScalarPlatform.Instance.Constants.MaxPipePathLength) + { + throw new PipeNameLengthException(string.Format("The pipe name ({0}) exceeds the max length allowed({1})", pipeName, ScalarPlatform.Instance.Constants.MaxPipePathLength)); + } + + NamedPipeServer pipeServer = new NamedPipeServer(pipeName, tracer, connection => HandleConnection(tracer, connection, handleRequest)); + pipeServer.OpenListeningPipe(); + + return pipeServer; + } + + public void Dispose() + { + this.isStopping = true; + + NamedPipeServerStream pipe = Interlocked.Exchange(ref this.listeningPipe, null); + if (pipe != null) + { + pipe.Dispose(); + } + } + + private static void HandleConnection(ITracer tracer, Connection connection, Action handleRequest) + { + while (connection.IsConnected) + { + string request = connection.ReadRequest(); + + if (request == null || + !connection.IsConnected) + { + break; + } + + handleRequest(tracer, request, connection); + } + } + + private void OpenListeningPipe() + { + try + { + if (this.listeningPipe != null) + { + throw new InvalidOperationException("There is already a pipe listening for a connection"); + } + + this.listeningPipe = ScalarPlatform.Instance.CreatePipeByName(this.pipeName); + this.listeningPipe.BeginWaitForConnection(this.OnNewConnection, this.listeningPipe); + } + catch (Exception e) + { + this.LogErrorAndExit("OpenListeningPipe caught unhandled exception, exiting process", e); + } + } + + private void OnNewConnection(IAsyncResult ar) + { + if (!this.isStopping) + { + this.OnNewConnection(ar, createNewThreadIfSynchronous: true); + } + } + + private void OnNewConnection(IAsyncResult ar, bool createNewThreadIfSynchronous) + { + if (createNewThreadIfSynchronous && + ar.CompletedSynchronously) + { + // if this callback got called synchronously, we must not do any blocking IO on this thread + // or we will block the original caller. Moving to a new thread so that it will be safe + // to call a blocking Read on the NamedPipeServerStream + + new Thread(() => this.OnNewConnection(ar, createNewThreadIfSynchronous: false)).Start(); + return; + } + + this.listeningPipe = null; + bool connectionBroken = false; + + NamedPipeServerStream pipe = (NamedPipeServerStream)ar.AsyncState; + try + { + try + { + pipe.EndWaitForConnection(ar); + } + catch (IOException e) + { + connectionBroken = true; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "NamedPipeServer"); + metadata.Add("Exception", e.ToString()); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "OnNewConnection: Connection broken"); + this.tracer.RelatedEvent(EventLevel.Warning, "OnNewConnectionn_EndWaitForConnection_IOException", metadata); + } + catch (Exception e) + { + this.LogErrorAndExit("OnNewConnection caught unhandled exception, exiting process", e); + } + + if (!this.isStopping) + { + this.OpenListeningPipe(); + + if (!connectionBroken) + { + try + { + this.handleConnection(new Connection(pipe, this.tracer, () => this.isStopping)); + } + catch (Exception e) + { + this.LogErrorAndExit("Unhandled exception in connection handler", e); + } + } + } + } + finally + { + pipe.Dispose(); + } + } + + private void LogErrorAndExit(string message, Exception e) + { + if (this.tracer != null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "NamedPipeServer"); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + this.tracer.RelatedError(metadata, message); + } + + Environment.Exit((int)ReturnCode.GenericError); + } + + public class Connection + { + private NamedPipeServerStream serverStream; + private NamedPipeStreamReader reader; + private NamedPipeStreamWriter writer; + private ITracer tracer; + private Func isStopping; + + public Connection(NamedPipeServerStream serverStream, ITracer tracer, Func isStopping) + { + this.serverStream = serverStream; + this.tracer = tracer; + this.isStopping = isStopping; + this.reader = new NamedPipeStreamReader(this.serverStream); + this.writer = new NamedPipeStreamWriter(this.serverStream); + } + + public bool IsConnected + { + get { return !this.isStopping() && this.serverStream.IsConnected; } + } + + public NamedPipeMessages.Message ReadMessage() + { + return NamedPipeMessages.Message.FromString(this.ReadRequest()); + } + + public string ReadRequest() + { + try + { + return this.reader.ReadMessage(); + } + catch (IOException e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ExceptionMessage", e.Message); + metadata.Add("StackTrace", e.StackTrace); + this.tracer.RelatedWarning( + metadata: metadata, + message: $"Error reading message from NamedPipe: {e.Message}", + keywords: Keywords.Telemetry); + + return null; + } + } + + public virtual bool TrySendResponse(string message) + { + try + { + this.writer.WriteMessage(message); + return true; + } + catch (IOException) + { + return false; + } + } + + public bool TrySendResponse(NamedPipeMessages.Message message) + { + return this.TrySendResponse(message.ToString()); + } + } + } +} diff --git a/Scalar.Common/NamedPipes/NamedPipeStreamReader.cs b/Scalar.Common/NamedPipes/NamedPipeStreamReader.cs index eb79370a14..dc8a9e540c 100644 --- a/Scalar.Common/NamedPipes/NamedPipeStreamReader.cs +++ b/Scalar.Common/NamedPipes/NamedPipeStreamReader.cs @@ -1,77 +1,77 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.Common.NamedPipes -{ - /// - /// Implements the NamedPipe protocol as described in NamedPipeServer. - /// - public class NamedPipeStreamReader - { - private const int InitialListSize = 1024; - private const byte TerminatorByte = 0x3; - private readonly byte[] buffer; - - private Stream stream; - - public NamedPipeStreamReader(Stream stream) - { - this.stream = stream; - this.buffer = new byte[1]; - } - - /// - /// Read a message from the stream. - /// - /// The message read from the stream, or null if the end of the input stream has been reached. - public string ReadMessage() - { - byte currentByte; - - bool streamOpen = this.TryReadByte(out currentByte); - if (!streamOpen) - { - // The end of the stream has been reached - return null to indicate this. - return null; - } - - List bytes = new List(InitialListSize); - - do - { - bytes.Add(currentByte); - streamOpen = this.TryReadByte(out currentByte); - - if (!streamOpen) - { - // We have read a partial message (the last byte received does not indicate that - // this was the end of the message), but the stream has been closed. Throw an exception - // and let upper layer deal with this condition. - - throw new IOException("Incomplete message read from stream. The end of the stream was reached without the expected terminating byte."); - } - } - while (currentByte != TerminatorByte); - - return Encoding.UTF8.GetString(bytes.ToArray()); - } - - /// - /// Read a byte from the stream. - /// - /// The byte read from the stream - /// True if byte read, false if end of stream has been reached - private bool TryReadByte(out byte readByte) - { - this.buffer[0] = 0; - - int numBytesRead = this.stream.Read(this.buffer, 0, 1); - readByte = this.buffer[0]; - - return numBytesRead == 1; - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.Common.NamedPipes +{ + /// + /// Implements the NamedPipe protocol as described in NamedPipeServer. + /// + public class NamedPipeStreamReader + { + private const int InitialListSize = 1024; + private const byte TerminatorByte = 0x3; + private readonly byte[] buffer; + + private Stream stream; + + public NamedPipeStreamReader(Stream stream) + { + this.stream = stream; + this.buffer = new byte[1]; + } + + /// + /// Read a message from the stream. + /// + /// The message read from the stream, or null if the end of the input stream has been reached. + public string ReadMessage() + { + byte currentByte; + + bool streamOpen = this.TryReadByte(out currentByte); + if (!streamOpen) + { + // The end of the stream has been reached - return null to indicate this. + return null; + } + + List bytes = new List(InitialListSize); + + do + { + bytes.Add(currentByte); + streamOpen = this.TryReadByte(out currentByte); + + if (!streamOpen) + { + // We have read a partial message (the last byte received does not indicate that + // this was the end of the message), but the stream has been closed. Throw an exception + // and let upper layer deal with this condition. + + throw new IOException("Incomplete message read from stream. The end of the stream was reached without the expected terminating byte."); + } + } + while (currentByte != TerminatorByte); + + return Encoding.UTF8.GetString(bytes.ToArray()); + } + + /// + /// Read a byte from the stream. + /// + /// The byte read from the stream + /// True if byte read, false if end of stream has been reached + private bool TryReadByte(out byte readByte) + { + this.buffer[0] = 0; + + int numBytesRead = this.stream.Read(this.buffer, 0, 1); + readByte = this.buffer[0]; + + return numBytesRead == 1; + } + } +} diff --git a/Scalar.Common/NamedPipes/NamedPipeStreamWriter.cs b/Scalar.Common/NamedPipes/NamedPipeStreamWriter.cs index 012e6f814b..2ad3cc1b5e 100644 --- a/Scalar.Common/NamedPipes/NamedPipeStreamWriter.cs +++ b/Scalar.Common/NamedPipes/NamedPipeStreamWriter.cs @@ -1,24 +1,24 @@ -using System.IO; -using System.Text; - -namespace Scalar.Common.NamedPipes -{ - public class NamedPipeStreamWriter - { - private const byte TerminatorByte = 0x3; - private const string TerminatorByteString = "\x3"; - private Stream stream; - - public NamedPipeStreamWriter(Stream stream) - { - this.stream = stream; - } - - public void WriteMessage(string message) - { - byte[] byteBuffer = Encoding.UTF8.GetBytes(message + TerminatorByteString); - this.stream.Write(byteBuffer, 0, byteBuffer.Length); - this.stream.Flush(); - } - } -} +using System.IO; +using System.Text; + +namespace Scalar.Common.NamedPipes +{ + public class NamedPipeStreamWriter + { + private const byte TerminatorByte = 0x3; + private const string TerminatorByteString = "\x3"; + private Stream stream; + + public NamedPipeStreamWriter(Stream stream) + { + this.stream = stream; + } + + public void WriteMessage(string message) + { + byte[] byteBuffer = Encoding.UTF8.GetBytes(message + TerminatorByteString); + this.stream.Write(byteBuffer, 0, byteBuffer.Length); + this.stream.Flush(); + } + } +} diff --git a/Scalar.Common/NamedPipes/PipeNameLengthException.cs b/Scalar.Common/NamedPipes/PipeNameLengthException.cs index a2f1d0c292..798f339812 100644 --- a/Scalar.Common/NamedPipes/PipeNameLengthException.cs +++ b/Scalar.Common/NamedPipes/PipeNameLengthException.cs @@ -1,12 +1,12 @@ -using System; - -namespace Scalar.Common.NamedPipes -{ - public class PipeNameLengthException : Exception - { - public PipeNameLengthException(string message) - : base(message) - { - } - } -} +using System; + +namespace Scalar.Common.NamedPipes +{ + public class PipeNameLengthException : Exception + { + public PipeNameLengthException(string message) + : base(message) + { + } + } +} diff --git a/Scalar.Common/NativeMethods.Shared.cs b/Scalar.Common/NativeMethods.Shared.cs index 1ee263040d..03f11832fa 100644 --- a/Scalar.Common/NativeMethods.Shared.cs +++ b/Scalar.Common/NativeMethods.Shared.cs @@ -1,178 +1,178 @@ -using Microsoft.Win32.SafeHandles; -using System; -using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace Scalar.Common -{ - public static partial class NativeMethods - { - public enum FileAttributes : uint - { - FILE_ATTRIBUTE_READONLY = 1, - FILE_ATTRIBUTE_HIDDEN = 2, - FILE_ATTRIBUTE_SYSTEM = 4, - FILE_ATTRIBUTE_DIRECTORY = 16, - FILE_ATTRIBUTE_ARCHIVE = 32, - FILE_ATTRIBUTE_DEVICE = 64, - FILE_ATTRIBUTE_NORMAL = 128, - FILE_ATTRIBUTE_TEMPORARY = 256, - FILE_ATTRIBUTE_SPARSEFILE = 512, - FILE_ATTRIBUTE_REPARSEPOINT = 1024, - FILE_ATTRIBUTE_COMPRESSED = 2048, - FILE_ATTRIBUTE_OFFLINE = 4096, - FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192, - FILE_ATTRIBUTE_ENCRYPTED = 16384, - FILE_FLAG_FIRST_PIPE_INSTANCE = 524288, - FILE_FLAG_OPEN_NO_RECALL = 1048576, - FILE_FLAG_OPEN_REPARSE_POINT = 2097152, - FILE_FLAG_POSIX_SEMANTICS = 16777216, - FILE_FLAG_BACKUP_SEMANTICS = 33554432, - FILE_FLAG_DELETE_ON_CLOSE = 67108864, - FILE_FLAG_SEQUENTIAL_SCAN = 134217728, - FILE_FLAG_RANDOM_ACCESS = 268435456, - FILE_FLAG_NO_BUFFERING = 536870912, - FILE_FLAG_OVERLAPPED = 1073741824, - FILE_FLAG_WRITE_THROUGH = 2147483648 - } - - public enum FileAccess : uint - { - FILE_READ_DATA = 1, - FILE_LIST_DIRECTORY = 1, - FILE_WRITE_DATA = 2, - FILE_ADD_FILE = 2, - FILE_APPEND_DATA = 4, - FILE_ADD_SUBDIRECTORY = 4, - FILE_CREATE_PIPE_INSTANCE = 4, - FILE_READ_EA = 8, - FILE_WRITE_EA = 16, - FILE_EXECUTE = 32, - FILE_TRAVERSE = 32, - FILE_DELETE_CHILD = 64, - FILE_READ_ATTRIBUTES = 128, - FILE_WRITE_ATTRIBUTES = 256, - SPECIFIC_RIGHTS_ALL = 65535, - DELETE = 65536, - READ_CONTROL = 131072, - STANDARD_RIGHTS_READ = 131072, - STANDARD_RIGHTS_WRITE = 131072, - STANDARD_RIGHTS_EXECUTE = 131072, - WRITE_DAC = 262144, - WRITE_OWNER = 524288, - STANDARD_RIGHTS_REQUIRED = 983040, - SYNCHRONIZE = 1048576, - FILE_GENERIC_READ = 1179785, - FILE_GENERIC_EXECUTE = 1179808, - FILE_GENERIC_WRITE = 1179926, - STANDARD_RIGHTS_ALL = 2031616, - FILE_ALL_ACCESS = 2032127, - ACCESS_SYSTEM_SECURITY = 16777216, - MAXIMUM_ALLOWED = 33554432, - GENERIC_ALL = 268435456, - GENERIC_EXECUTE = 536870912, - GENERIC_WRITE = 1073741824, - GENERIC_READ = 2147483648 - } - - [Flags] - public enum ProcessAccessFlags : uint - { - All = 0x001F0FFF, - Terminate = 0x00000001, - CreateThread = 0x00000002, - VirtualMemoryOperation = 0x00000008, - VirtualMemoryRead = 0x00000010, - VirtualMemoryWrite = 0x00000020, - DuplicateHandle = 0x00000040, - CreateProcess = 0x000000080, - SetQuota = 0x00000100, - SetInformation = 0x00000200, - QueryInformation = 0x00000400, - QueryLimitedInformation = 0x00001000, - Synchronize = 0x00100000 - } - - public static string GetFinalPathName(string path) - { - // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path - // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx, - // we must set this flag to obtain a handle to a directory - using (SafeFileHandle fileHandle = CreateFile( - path, - FileAccess.FILE_READ_ATTRIBUTES, - FileShare.ReadWrite, - IntPtr.Zero, - FileMode.Open, - FileAttributes.FILE_FLAG_BACKUP_SEMANTICS, - IntPtr.Zero)) - { - if (fileHandle.IsInvalid) - { - ThrowLastWin32Exception($"Invalid file handle for {path}"); - } - - int finalPathSize = GetFinalPathNameByHandle(fileHandle, null, 0, 0); - StringBuilder finalPath = new StringBuilder(finalPathSize + 1); - - // GetFinalPathNameByHandle buffer size should not include a NULL termination character - finalPathSize = GetFinalPathNameByHandle(fileHandle, finalPath, finalPathSize, 0); - if (finalPathSize == 0) - { - ThrowLastWin32Exception($"Failed to get final path size for {finalPath}"); - } - - string pathString = finalPath.ToString(); - - // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\" - // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx - const string PathPrefix = @"\\?\"; - const string UncPrefix = @"\\?\UNC\"; - if (pathString.StartsWith(UncPrefix, StringComparison.Ordinal)) - { - pathString = @"\\" + pathString.Substring(UncPrefix.Length); - } - else if (pathString.StartsWith(PathPrefix, StringComparison.Ordinal)) - { - pathString = pathString.Substring(PathPrefix.Length); - } - - return pathString; - } - } - - public static void ThrowLastWin32Exception(string message) - { - throw new Win32Exception(Marshal.GetLastWin32Error(), message); - } - - [DllImport("kernel32.dll", SetLastError = true)] - public static extern SafeFileHandle OpenProcess( - ProcessAccessFlags processAccess, - bool bInheritHandle, - int processId); - - [DllImport("kernel32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool GetExitCodeProcess(SafeFileHandle hProcess, out uint lpExitCode); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern SafeFileHandle CreateFile( - [In] string lpFileName, - [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, - FileShare dwShareMode, - [In] IntPtr lpSecurityAttributes, - [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, - [MarshalAs(UnmanagedType.U4)]FileAttributes dwFlagsAndAttributes, - [In] IntPtr hTemplateFile); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern int GetFinalPathNameByHandle( - SafeFileHandle hFile, - [Out] StringBuilder lpszFilePath, - int cchFilePath, - int dwFlags); - } -} +using Microsoft.Win32.SafeHandles; +using System; +using System.ComponentModel; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Scalar.Common +{ + public static partial class NativeMethods + { + public enum FileAttributes : uint + { + FILE_ATTRIBUTE_READONLY = 1, + FILE_ATTRIBUTE_HIDDEN = 2, + FILE_ATTRIBUTE_SYSTEM = 4, + FILE_ATTRIBUTE_DIRECTORY = 16, + FILE_ATTRIBUTE_ARCHIVE = 32, + FILE_ATTRIBUTE_DEVICE = 64, + FILE_ATTRIBUTE_NORMAL = 128, + FILE_ATTRIBUTE_TEMPORARY = 256, + FILE_ATTRIBUTE_SPARSEFILE = 512, + FILE_ATTRIBUTE_REPARSEPOINT = 1024, + FILE_ATTRIBUTE_COMPRESSED = 2048, + FILE_ATTRIBUTE_OFFLINE = 4096, + FILE_ATTRIBUTE_NOT_CONTENT_INDEXED = 8192, + FILE_ATTRIBUTE_ENCRYPTED = 16384, + FILE_FLAG_FIRST_PIPE_INSTANCE = 524288, + FILE_FLAG_OPEN_NO_RECALL = 1048576, + FILE_FLAG_OPEN_REPARSE_POINT = 2097152, + FILE_FLAG_POSIX_SEMANTICS = 16777216, + FILE_FLAG_BACKUP_SEMANTICS = 33554432, + FILE_FLAG_DELETE_ON_CLOSE = 67108864, + FILE_FLAG_SEQUENTIAL_SCAN = 134217728, + FILE_FLAG_RANDOM_ACCESS = 268435456, + FILE_FLAG_NO_BUFFERING = 536870912, + FILE_FLAG_OVERLAPPED = 1073741824, + FILE_FLAG_WRITE_THROUGH = 2147483648 + } + + public enum FileAccess : uint + { + FILE_READ_DATA = 1, + FILE_LIST_DIRECTORY = 1, + FILE_WRITE_DATA = 2, + FILE_ADD_FILE = 2, + FILE_APPEND_DATA = 4, + FILE_ADD_SUBDIRECTORY = 4, + FILE_CREATE_PIPE_INSTANCE = 4, + FILE_READ_EA = 8, + FILE_WRITE_EA = 16, + FILE_EXECUTE = 32, + FILE_TRAVERSE = 32, + FILE_DELETE_CHILD = 64, + FILE_READ_ATTRIBUTES = 128, + FILE_WRITE_ATTRIBUTES = 256, + SPECIFIC_RIGHTS_ALL = 65535, + DELETE = 65536, + READ_CONTROL = 131072, + STANDARD_RIGHTS_READ = 131072, + STANDARD_RIGHTS_WRITE = 131072, + STANDARD_RIGHTS_EXECUTE = 131072, + WRITE_DAC = 262144, + WRITE_OWNER = 524288, + STANDARD_RIGHTS_REQUIRED = 983040, + SYNCHRONIZE = 1048576, + FILE_GENERIC_READ = 1179785, + FILE_GENERIC_EXECUTE = 1179808, + FILE_GENERIC_WRITE = 1179926, + STANDARD_RIGHTS_ALL = 2031616, + FILE_ALL_ACCESS = 2032127, + ACCESS_SYSTEM_SECURITY = 16777216, + MAXIMUM_ALLOWED = 33554432, + GENERIC_ALL = 268435456, + GENERIC_EXECUTE = 536870912, + GENERIC_WRITE = 1073741824, + GENERIC_READ = 2147483648 + } + + [Flags] + public enum ProcessAccessFlags : uint + { + All = 0x001F0FFF, + Terminate = 0x00000001, + CreateThread = 0x00000002, + VirtualMemoryOperation = 0x00000008, + VirtualMemoryRead = 0x00000010, + VirtualMemoryWrite = 0x00000020, + DuplicateHandle = 0x00000040, + CreateProcess = 0x000000080, + SetQuota = 0x00000100, + SetInformation = 0x00000200, + QueryInformation = 0x00000400, + QueryLimitedInformation = 0x00001000, + Synchronize = 0x00100000 + } + + public static string GetFinalPathName(string path) + { + // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path + // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx, + // we must set this flag to obtain a handle to a directory + using (SafeFileHandle fileHandle = CreateFile( + path, + FileAccess.FILE_READ_ATTRIBUTES, + FileShare.ReadWrite, + IntPtr.Zero, + FileMode.Open, + FileAttributes.FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero)) + { + if (fileHandle.IsInvalid) + { + ThrowLastWin32Exception($"Invalid file handle for {path}"); + } + + int finalPathSize = GetFinalPathNameByHandle(fileHandle, null, 0, 0); + StringBuilder finalPath = new StringBuilder(finalPathSize + 1); + + // GetFinalPathNameByHandle buffer size should not include a NULL termination character + finalPathSize = GetFinalPathNameByHandle(fileHandle, finalPath, finalPathSize, 0); + if (finalPathSize == 0) + { + ThrowLastWin32Exception($"Failed to get final path size for {finalPath}"); + } + + string pathString = finalPath.ToString(); + + // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\" + // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx + const string PathPrefix = @"\\?\"; + const string UncPrefix = @"\\?\UNC\"; + if (pathString.StartsWith(UncPrefix, StringComparison.Ordinal)) + { + pathString = @"\\" + pathString.Substring(UncPrefix.Length); + } + else if (pathString.StartsWith(PathPrefix, StringComparison.Ordinal)) + { + pathString = pathString.Substring(PathPrefix.Length); + } + + return pathString; + } + } + + public static void ThrowLastWin32Exception(string message) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), message); + } + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern SafeFileHandle OpenProcess( + ProcessAccessFlags processAccess, + bool bInheritHandle, + int processId); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetExitCodeProcess(SafeFileHandle hProcess, out uint lpExitCode); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern SafeFileHandle CreateFile( + [In] string lpFileName, + [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, + FileShare dwShareMode, + [In] IntPtr lpSecurityAttributes, + [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, + [MarshalAs(UnmanagedType.U4)]FileAttributes dwFlagsAndAttributes, + [In] IntPtr hTemplateFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern int GetFinalPathNameByHandle( + SafeFileHandle hFile, + [Out] StringBuilder lpszFilePath, + int cchFilePath, + int dwFlags); + } +} diff --git a/Scalar.Common/NativeMethods.cs b/Scalar.Common/NativeMethods.cs index 981549a495..baa90999a4 100644 --- a/Scalar.Common/NativeMethods.cs +++ b/Scalar.Common/NativeMethods.cs @@ -1,274 +1,274 @@ -using Microsoft.Win32.SafeHandles; -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace Scalar.Common -{ - public static partial class NativeMethods - { - private const uint EVENT_TRACE_CONTROL_FLUSH = 3; - - private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; - private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; - private const uint FSCTL_GET_REPARSE_POINT = 0x000900a8; - - private const int ReparseDataPathBufferLength = 1000; - - [Flags] - public enum MoveFileFlags : uint - { - MoveFileReplaceExisting = 0x00000001, // MOVEFILE_REPLACE_EXISTING - MoveFileCopyAllowed = 0x00000002, // MOVEFILE_COPY_ALLOWED - MoveFileDelayUntilReboot = 0x00000004, // MOVEFILE_DELAY_UNTIL_REBOOT - MoveFileWriteThrough = 0x00000008, // MOVEFILE_WRITE_THROUGH - MoveFileCreateHardlink = 0x00000010, // MOVEFILE_CREATE_HARDLINK - MoveFileFailIfNotTrackable = 0x00000020, // MOVEFILE_FAIL_IF_NOT_TRACKABLE - } - - [Flags] - public enum FileSystemFlags : uint - { - FILE_RETURNS_CLEANUP_RESULT_INFO = 0x00000200 - } - - public static void FlushFileBuffers(string path) - { - using (SafeFileHandle fileHandle = CreateFile( - path, - FileAccess.GENERIC_WRITE, - FileShare.ReadWrite, - IntPtr.Zero, - FileMode.Open, - FileAttributes.FILE_ATTRIBUTE_NORMAL, - IntPtr.Zero)) - { - if (fileHandle.IsInvalid) - { - ThrowLastWin32Exception($"Invalid handle for '{path}'"); - } - - if (!FlushFileBuffers(fileHandle)) - { - ThrowLastWin32Exception($"Failed to flush buffers for '{path}'"); - } - } - } - - public static bool IsFeatureSupportedByVolume(string volumeRoot, FileSystemFlags flags) - { - uint volumeSerialNumber; - uint maximumComponentLength; - uint fileSystemFlags; - - if (!GetVolumeInformation( - volumeRoot, - null, - 0, - out volumeSerialNumber, - out maximumComponentLength, - out fileSystemFlags, - null, - 0)) - { - ThrowLastWin32Exception($"Failed to get volume information for '{volumeRoot}'"); - } - - return (fileSystemFlags & (uint)flags) == (uint)flags; - } - - public static uint FlushTraceLogger(string sessionName, string sessionGuid, out string logfileName) - { - EventTraceProperties properties = new EventTraceProperties(); - properties.Wnode.BufferSize = (uint)Marshal.SizeOf(properties); - properties.Wnode.Guid = new Guid(sessionGuid); - properties.LoggerNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LoggerName"); - properties.LogFileNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LogFileName"); - uint result = ControlTrace(0, sessionName, ref properties, EVENT_TRACE_CONTROL_FLUSH); - logfileName = properties.LogFileName; - return result; - } - - public static void MoveFile(string existingFileName, string newFileName, MoveFileFlags flags) - { - if (!MoveFileEx(existingFileName, newFileName, (uint)flags)) - { - ThrowLastWin32Exception($"Failed to move '{existingFileName}' to '{newFileName}'"); - } - } - - /// - /// Get the build number of the OS - /// - /// Build number - /// - /// For this method to work correctly, the calling application must have a manifest file - /// that indicates the application supports Windows 10. - /// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details - /// - public static uint GetWindowsBuildNumber() - { - OSVersionInfo versionInfo = new OSVersionInfo(); - versionInfo.OSVersionInfoSize = (uint)Marshal.SizeOf(versionInfo); - if (!GetVersionEx(ref versionInfo)) - { - ThrowLastWin32Exception($"Failed to get OS version info"); - } - - return versionInfo.BuildNumber; - } - - public static bool IsSymLink(string path) - { - using (SafeFileHandle output = CreateFile( - path, - FileAccess.FILE_READ_ATTRIBUTES, - FileShare.Read, - IntPtr.Zero, - FileMode.Open, - FileAttributes.FILE_FLAG_BACKUP_SEMANTICS | FileAttributes.FILE_FLAG_OPEN_REPARSE_POINT, - IntPtr.Zero)) - { - if (output.IsInvalid) - { - ThrowLastWin32Exception($"Invalid handle for '{path}' as symlink"); - } - - REPARSE_DATA_BUFFER reparseData = new REPARSE_DATA_BUFFER(); - reparseData.ReparseDataLength = (4 * sizeof(ushort)) + ReparseDataPathBufferLength; - uint bytesReturned; - if (!DeviceIoControl(output, FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0, out reparseData, (uint)Marshal.SizeOf(reparseData), out bytesReturned, IntPtr.Zero)) - { - ThrowLastWin32Exception($"Failed to place reparse point for '{path}'"); - } - - return reparseData.ReparseTag == IO_REPARSE_TAG_SYMLINK || reparseData.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT; - } - } - - public static DateTime GetLastRebootTime() - { - // GetTickCount64 is a native call and returns the number - // of milliseconds since the system was started (and not DateTime.Ticks). - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724411.aspx - TimeSpan uptime = TimeSpan.FromMilliseconds(GetTickCount64()); - return DateTime.Now - uptime; - } - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool MoveFileEx( - string existingFileName, - string newFileName, - uint flags); - - [DllImport("kernel32.dll", SetLastError = true)] - private static extern bool FlushFileBuffers(SafeFileHandle hFile); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool GetVolumeInformation( - string rootPathName, - StringBuilder volumeNameBuffer, - int volumeNameSize, - out uint volumeSerialNumber, - out uint maximumComponentLength, - out uint fileSystemFlags, - StringBuilder fileSystemNameBuffer, - int nFileSystemNameSize); - - [DllImport("advapi32.dll", EntryPoint = "ControlTraceW", CharSet = CharSet.Unicode)] - private static extern uint ControlTrace( - [In] ulong sessionHandle, - [In] string sessionName, - [In, Out] ref EventTraceProperties properties, - [In] uint controlCode); - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern bool GetVersionEx([In, Out] ref OSVersionInfo versionInfo); - - // For use with FSCTL_GET_REPARSE_POINT - [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool DeviceIoControl( - SafeFileHandle hDevice, - uint IoControlCode, - [In] IntPtr InBuffer, - uint nInBufferSize, - [Out] out REPARSE_DATA_BUFFER OutBuffer, - uint nOutBufferSize, - out uint pBytesReturned, - [In] IntPtr Overlapped); - - [DllImport("kernel32.dll")] - private static extern ulong GetTickCount64(); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct REPARSE_DATA_BUFFER - { - public uint ReparseTag; - public ushort ReparseDataLength; - public ushort Reserved; - public ushort SubstituteNameOffset; - public ushort SubstituteNameLength; - public ushort PrintNameOffset; - public ushort PrintNameLength; - [MarshalAs(UnmanagedType.ByValArray, SizeConst = ReparseDataPathBufferLength)] - public byte[] PathBuffer; - } - - [StructLayout(LayoutKind.Sequential)] - private struct WNodeHeader - { - public uint BufferSize; - public uint ProviderId; - public ulong HistoricalContext; - public ulong TimeStamp; - public Guid Guid; - public uint ClientContext; - public uint Flags; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct EventTraceProperties - { - public WNodeHeader Wnode; - public uint BufferSize; - public uint MinimumBuffers; - public uint MaximumBuffers; - public uint MaximumFileSize; - public uint LogFileMode; - public uint FlushTimer; - public uint EnableFlags; - public int AgeLimit; - public uint NumberOfBuffers; - public uint FreeBuffers; - public uint EventsLost; - public uint BuffersWritten; - public uint LogBuffersLost; - public uint RealTimeBuffersLost; - public IntPtr LoggerThreadId; - public uint LogFileNameOffset; - public uint LoggerNameOffset; - - // "You can use the maximum session name (1024 characters) and maximum log file name (1024 characters) lengths to calculate the buffer size and offsets if not known" - // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363696(v=vs.85).aspx - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] - public string LoggerName; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] - public string LogFileName; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct OSVersionInfo - { - public uint OSVersionInfoSize; - public uint MajorVersion; - public uint MinorVersion; - public uint BuildNumber; - public uint PlatformId; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] - public string CSDVersion; - } - } -} +using Microsoft.Win32.SafeHandles; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Scalar.Common +{ + public static partial class NativeMethods + { + private const uint EVENT_TRACE_CONTROL_FLUSH = 3; + + private const uint IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + private const uint IO_REPARSE_TAG_SYMLINK = 0xA000000C; + private const uint FSCTL_GET_REPARSE_POINT = 0x000900a8; + + private const int ReparseDataPathBufferLength = 1000; + + [Flags] + public enum MoveFileFlags : uint + { + MoveFileReplaceExisting = 0x00000001, // MOVEFILE_REPLACE_EXISTING + MoveFileCopyAllowed = 0x00000002, // MOVEFILE_COPY_ALLOWED + MoveFileDelayUntilReboot = 0x00000004, // MOVEFILE_DELAY_UNTIL_REBOOT + MoveFileWriteThrough = 0x00000008, // MOVEFILE_WRITE_THROUGH + MoveFileCreateHardlink = 0x00000010, // MOVEFILE_CREATE_HARDLINK + MoveFileFailIfNotTrackable = 0x00000020, // MOVEFILE_FAIL_IF_NOT_TRACKABLE + } + + [Flags] + public enum FileSystemFlags : uint + { + FILE_RETURNS_CLEANUP_RESULT_INFO = 0x00000200 + } + + public static void FlushFileBuffers(string path) + { + using (SafeFileHandle fileHandle = CreateFile( + path, + FileAccess.GENERIC_WRITE, + FileShare.ReadWrite, + IntPtr.Zero, + FileMode.Open, + FileAttributes.FILE_ATTRIBUTE_NORMAL, + IntPtr.Zero)) + { + if (fileHandle.IsInvalid) + { + ThrowLastWin32Exception($"Invalid handle for '{path}'"); + } + + if (!FlushFileBuffers(fileHandle)) + { + ThrowLastWin32Exception($"Failed to flush buffers for '{path}'"); + } + } + } + + public static bool IsFeatureSupportedByVolume(string volumeRoot, FileSystemFlags flags) + { + uint volumeSerialNumber; + uint maximumComponentLength; + uint fileSystemFlags; + + if (!GetVolumeInformation( + volumeRoot, + null, + 0, + out volumeSerialNumber, + out maximumComponentLength, + out fileSystemFlags, + null, + 0)) + { + ThrowLastWin32Exception($"Failed to get volume information for '{volumeRoot}'"); + } + + return (fileSystemFlags & (uint)flags) == (uint)flags; + } + + public static uint FlushTraceLogger(string sessionName, string sessionGuid, out string logfileName) + { + EventTraceProperties properties = new EventTraceProperties(); + properties.Wnode.BufferSize = (uint)Marshal.SizeOf(properties); + properties.Wnode.Guid = new Guid(sessionGuid); + properties.LoggerNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LoggerName"); + properties.LogFileNameOffset = (uint)Marshal.OffsetOf(typeof(EventTraceProperties), "LogFileName"); + uint result = ControlTrace(0, sessionName, ref properties, EVENT_TRACE_CONTROL_FLUSH); + logfileName = properties.LogFileName; + return result; + } + + public static void MoveFile(string existingFileName, string newFileName, MoveFileFlags flags) + { + if (!MoveFileEx(existingFileName, newFileName, (uint)flags)) + { + ThrowLastWin32Exception($"Failed to move '{existingFileName}' to '{newFileName}'"); + } + } + + /// + /// Get the build number of the OS + /// + /// Build number + /// + /// For this method to work correctly, the calling application must have a manifest file + /// that indicates the application supports Windows 10. + /// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms724451(v=vs.85).aspx for details + /// + public static uint GetWindowsBuildNumber() + { + OSVersionInfo versionInfo = new OSVersionInfo(); + versionInfo.OSVersionInfoSize = (uint)Marshal.SizeOf(versionInfo); + if (!GetVersionEx(ref versionInfo)) + { + ThrowLastWin32Exception($"Failed to get OS version info"); + } + + return versionInfo.BuildNumber; + } + + public static bool IsSymLink(string path) + { + using (SafeFileHandle output = CreateFile( + path, + FileAccess.FILE_READ_ATTRIBUTES, + FileShare.Read, + IntPtr.Zero, + FileMode.Open, + FileAttributes.FILE_FLAG_BACKUP_SEMANTICS | FileAttributes.FILE_FLAG_OPEN_REPARSE_POINT, + IntPtr.Zero)) + { + if (output.IsInvalid) + { + ThrowLastWin32Exception($"Invalid handle for '{path}' as symlink"); + } + + REPARSE_DATA_BUFFER reparseData = new REPARSE_DATA_BUFFER(); + reparseData.ReparseDataLength = (4 * sizeof(ushort)) + ReparseDataPathBufferLength; + uint bytesReturned; + if (!DeviceIoControl(output, FSCTL_GET_REPARSE_POINT, IntPtr.Zero, 0, out reparseData, (uint)Marshal.SizeOf(reparseData), out bytesReturned, IntPtr.Zero)) + { + ThrowLastWin32Exception($"Failed to place reparse point for '{path}'"); + } + + return reparseData.ReparseTag == IO_REPARSE_TAG_SYMLINK || reparseData.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT; + } + } + + public static DateTime GetLastRebootTime() + { + // GetTickCount64 is a native call and returns the number + // of milliseconds since the system was started (and not DateTime.Ticks). + // https://msdn.microsoft.com/en-us/library/windows/desktop/ms724411.aspx + TimeSpan uptime = TimeSpan.FromMilliseconds(GetTickCount64()); + return DateTime.Now - uptime; + } + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool MoveFileEx( + string existingFileName, + string newFileName, + uint flags); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FlushFileBuffers(SafeFileHandle hFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool GetVolumeInformation( + string rootPathName, + StringBuilder volumeNameBuffer, + int volumeNameSize, + out uint volumeSerialNumber, + out uint maximumComponentLength, + out uint fileSystemFlags, + StringBuilder fileSystemNameBuffer, + int nFileSystemNameSize); + + [DllImport("advapi32.dll", EntryPoint = "ControlTraceW", CharSet = CharSet.Unicode)] + private static extern uint ControlTrace( + [In] ulong sessionHandle, + [In] string sessionName, + [In, Out] ref EventTraceProperties properties, + [In] uint controlCode); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern bool GetVersionEx([In, Out] ref OSVersionInfo versionInfo); + + // For use with FSCTL_GET_REPARSE_POINT + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool DeviceIoControl( + SafeFileHandle hDevice, + uint IoControlCode, + [In] IntPtr InBuffer, + uint nInBufferSize, + [Out] out REPARSE_DATA_BUFFER OutBuffer, + uint nOutBufferSize, + out uint pBytesReturned, + [In] IntPtr Overlapped); + + [DllImport("kernel32.dll")] + private static extern ulong GetTickCount64(); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct REPARSE_DATA_BUFFER + { + public uint ReparseTag; + public ushort ReparseDataLength; + public ushort Reserved; + public ushort SubstituteNameOffset; + public ushort SubstituteNameLength; + public ushort PrintNameOffset; + public ushort PrintNameLength; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = ReparseDataPathBufferLength)] + public byte[] PathBuffer; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WNodeHeader + { + public uint BufferSize; + public uint ProviderId; + public ulong HistoricalContext; + public ulong TimeStamp; + public Guid Guid; + public uint ClientContext; + public uint Flags; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct EventTraceProperties + { + public WNodeHeader Wnode; + public uint BufferSize; + public uint MinimumBuffers; + public uint MaximumBuffers; + public uint MaximumFileSize; + public uint LogFileMode; + public uint FlushTimer; + public uint EnableFlags; + public int AgeLimit; + public uint NumberOfBuffers; + public uint FreeBuffers; + public uint EventsLost; + public uint BuffersWritten; + public uint LogBuffersLost; + public uint RealTimeBuffersLost; + public IntPtr LoggerThreadId; + public uint LogFileNameOffset; + public uint LoggerNameOffset; + + // "You can use the maximum session name (1024 characters) and maximum log file name (1024 characters) lengths to calculate the buffer size and offsets if not known" + // https://msdn.microsoft.com/en-us/library/windows/desktop/aa363696(v=vs.85).aspx + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] + public string LoggerName; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 1024)] + public string LogFileName; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct OSVersionInfo + { + public uint OSVersionInfoSize; + public uint MajorVersion; + public uint MinorVersion; + public uint BuildNumber; + public uint PlatformId; + + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string CSDVersion; + } + } +} diff --git a/Scalar.Common/NetworkStreams/BatchedLooseObjectDeserializer.cs b/Scalar.Common/NetworkStreams/BatchedLooseObjectDeserializer.cs index 88e32aeadf..006b346977 100644 --- a/Scalar.Common/NetworkStreams/BatchedLooseObjectDeserializer.cs +++ b/Scalar.Common/NetworkStreams/BatchedLooseObjectDeserializer.cs @@ -1,132 +1,132 @@ -using System; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.Common.NetworkStreams -{ - /// - /// Deserializer for concatenated loose objects. - /// - public class BatchedLooseObjectDeserializer - { - private const int NumObjectIdBytes = 20; - private const int NumObjectHeaderBytes = NumObjectIdBytes + sizeof(long); - private static readonly byte[] ExpectedHeader - = new byte[] - { - (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', // Magic - 1 // Version - }; - - private readonly Stream source; - private readonly OnLooseObject onLooseObject; - - public BatchedLooseObjectDeserializer(Stream source, OnLooseObject onLooseObject) - { - this.source = source; - this.onLooseObject = onLooseObject; - } - - /// - /// Invoked when the full content of a single loose object is available. - /// - public delegate void OnLooseObject(Stream objectStream, string sha1); - - /// - /// Read all the objects from the source stream and call for each. - /// - /// The total number of objects read - public int ProcessObjects() - { - this.ValidateHeader(); - - // Start reading objects - int numObjectsRead = 0; - byte[] curObjectHeader = new byte[NumObjectHeaderBytes]; - - while (true) - { - bool keepReading = this.ShouldContinueReading(curObjectHeader); - if (!keepReading) - { - break; - } - - // Get the length - long curLength = BitConverter.ToInt64(curObjectHeader, NumObjectIdBytes); - - // Handle the loose object - using (Stream rawObjectData = new RestrictedStream(this.source, curLength)) - { - string objectId = SHA1Util.HexStringFromBytes(curObjectHeader, NumObjectIdBytes); - - if (objectId.Equals(ScalarConstants.AllZeroSha)) - { - throw new RetryableException("Received all-zero SHA before end of stream"); - } - - this.onLooseObject(rawObjectData, objectId); - numObjectsRead++; - } - } - - return numObjectsRead; - } - - /// - /// Parse the current object header to check if we've reached the end. - /// - /// true if the end of the stream has been reached, false if not - private bool ShouldContinueReading(byte[] curObjectHeader) - { - int totalBytes = StreamUtil.TryReadGreedy( - this.source, - curObjectHeader, - 0, - curObjectHeader.Length); - - if (totalBytes == NumObjectHeaderBytes) - { - // Successful header read - return true; - } - else if (totalBytes == NumObjectIdBytes) - { - // We may have finished reading all the objects - for (int i = 0; i < NumObjectIdBytes; i++) - { - if (curObjectHeader[i] != 0) - { - throw new RetryableException( - string.Format( - "Reached end of stream before we got the expected zero-object ID Buffer: {0}", - SHA1Util.HexStringFromBytes(curObjectHeader))); - } - } - - return false; - } - else - { - throw new RetryableException( - string.Format( - "Reached end of stream before expected {0} or {1} bytes. Got {2}. Buffer: {3}", - NumObjectHeaderBytes, - NumObjectIdBytes, - totalBytes, - SHA1Util.HexStringFromBytes(curObjectHeader))); - } - } - - private void ValidateHeader() - { - byte[] headerBuf = new byte[ExpectedHeader.Length]; - StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); - if (!headerBuf.SequenceEqual(ExpectedHeader)) - { - throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); - } - } - } -} +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.Common.NetworkStreams +{ + /// + /// Deserializer for concatenated loose objects. + /// + public class BatchedLooseObjectDeserializer + { + private const int NumObjectIdBytes = 20; + private const int NumObjectHeaderBytes = NumObjectIdBytes + sizeof(long); + private static readonly byte[] ExpectedHeader + = new byte[] + { + (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', // Magic + 1 // Version + }; + + private readonly Stream source; + private readonly OnLooseObject onLooseObject; + + public BatchedLooseObjectDeserializer(Stream source, OnLooseObject onLooseObject) + { + this.source = source; + this.onLooseObject = onLooseObject; + } + + /// + /// Invoked when the full content of a single loose object is available. + /// + public delegate void OnLooseObject(Stream objectStream, string sha1); + + /// + /// Read all the objects from the source stream and call for each. + /// + /// The total number of objects read + public int ProcessObjects() + { + this.ValidateHeader(); + + // Start reading objects + int numObjectsRead = 0; + byte[] curObjectHeader = new byte[NumObjectHeaderBytes]; + + while (true) + { + bool keepReading = this.ShouldContinueReading(curObjectHeader); + if (!keepReading) + { + break; + } + + // Get the length + long curLength = BitConverter.ToInt64(curObjectHeader, NumObjectIdBytes); + + // Handle the loose object + using (Stream rawObjectData = new RestrictedStream(this.source, curLength)) + { + string objectId = SHA1Util.HexStringFromBytes(curObjectHeader, NumObjectIdBytes); + + if (objectId.Equals(ScalarConstants.AllZeroSha)) + { + throw new RetryableException("Received all-zero SHA before end of stream"); + } + + this.onLooseObject(rawObjectData, objectId); + numObjectsRead++; + } + } + + return numObjectsRead; + } + + /// + /// Parse the current object header to check if we've reached the end. + /// + /// true if the end of the stream has been reached, false if not + private bool ShouldContinueReading(byte[] curObjectHeader) + { + int totalBytes = StreamUtil.TryReadGreedy( + this.source, + curObjectHeader, + 0, + curObjectHeader.Length); + + if (totalBytes == NumObjectHeaderBytes) + { + // Successful header read + return true; + } + else if (totalBytes == NumObjectIdBytes) + { + // We may have finished reading all the objects + for (int i = 0; i < NumObjectIdBytes; i++) + { + if (curObjectHeader[i] != 0) + { + throw new RetryableException( + string.Format( + "Reached end of stream before we got the expected zero-object ID Buffer: {0}", + SHA1Util.HexStringFromBytes(curObjectHeader))); + } + } + + return false; + } + else + { + throw new RetryableException( + string.Format( + "Reached end of stream before expected {0} or {1} bytes. Got {2}. Buffer: {3}", + NumObjectHeaderBytes, + NumObjectIdBytes, + totalBytes, + SHA1Util.HexStringFromBytes(curObjectHeader))); + } + } + + private void ValidateHeader() + { + byte[] headerBuf = new byte[ExpectedHeader.Length]; + StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); + if (!headerBuf.SequenceEqual(ExpectedHeader)) + { + throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); + } + } + } +} diff --git a/Scalar.Common/NetworkStreams/PrefetchPacksDeserializer.cs b/Scalar.Common/NetworkStreams/PrefetchPacksDeserializer.cs index 61b380400e..f8fc45bf4c 100644 --- a/Scalar.Common/NetworkStreams/PrefetchPacksDeserializer.cs +++ b/Scalar.Common/NetworkStreams/PrefetchPacksDeserializer.cs @@ -1,124 +1,124 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.Common.NetworkStreams -{ - /// - /// Deserializer for packs and indexes for prefetch packs. - /// - public class PrefetchPacksDeserializer - { - private const int NumPackHeaderBytes = 3 * sizeof(long); - - private static readonly byte[] PrefetchPackExpectedHeader = - new byte[] - { - (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', // Magic - 1 // Version - }; - - private readonly Stream source; - - public PrefetchPacksDeserializer(Stream source) - { - this.source = source; - } - - /// - /// Read all the packs and indexes from the source stream and return a for each pack - /// and index. Caller must consume pack stream fully before the index stream. - /// - public IEnumerable EnumeratePacks() - { - this.ValidateHeader(); - - byte[] buffer = new byte[NumPackHeaderBytes]; - - int packCount = this.ReadPackCount(buffer); - - for (int i = 0; i < packCount; i++) - { - long timestamp; - long packLength; - long indexLength; - this.ReadPackHeader(buffer, out timestamp, out packLength, out indexLength); - - using (Stream packData = new RestrictedStream(this.source, packLength)) - using (Stream indexData = indexLength > 0 ? new RestrictedStream(this.source, indexLength) : null) - { - yield return new PackAndIndex(packData, indexData, timestamp); - } - } - } - - /// - /// Read the ushort pack count - /// - private ushort ReadPackCount(byte[] buffer) - { - StreamUtil.TryReadGreedy(this.source, buffer, 0, 2); - return BitConverter.ToUInt16(buffer, 0); - } - - /// - /// Parse the current pack header - /// - private void ReadPackHeader( - byte[] buffer, - out long timestamp, - out long packLength, - out long indexLength) - { - int totalBytes = StreamUtil.TryReadGreedy( - this.source, - buffer, - 0, - NumPackHeaderBytes); - - if (totalBytes == NumPackHeaderBytes) - { - timestamp = BitConverter.ToInt64(buffer, 0); - packLength = BitConverter.ToInt64(buffer, 8); - indexLength = BitConverter.ToInt64(buffer, 16); - } - else - { - throw new RetryableException( - string.Format( - "Reached end of stream before expected {0} bytes. Got {1}. Buffer: {2}", - NumPackHeaderBytes, - totalBytes, - SHA1Util.HexStringFromBytes(buffer))); - } - } - - private void ValidateHeader() - { - byte[] headerBuf = new byte[PrefetchPackExpectedHeader.Length]; - StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); - if (!headerBuf.SequenceEqual(PrefetchPackExpectedHeader)) - { - throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); - } - } - - public class PackAndIndex - { - public PackAndIndex(Stream packStream, Stream idxStream, long timestamp) - { - this.PackStream = packStream; - this.IndexStream = idxStream; - this.Timestamp = timestamp; - this.UniqueId = Guid.NewGuid().ToString("N"); - } - - public Stream PackStream { get; } - public Stream IndexStream { get; } - public long Timestamp { get; } - public string UniqueId { get; } - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.Common.NetworkStreams +{ + /// + /// Deserializer for packs and indexes for prefetch packs. + /// + public class PrefetchPacksDeserializer + { + private const int NumPackHeaderBytes = 3 * sizeof(long); + + private static readonly byte[] PrefetchPackExpectedHeader = + new byte[] + { + (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', // Magic + 1 // Version + }; + + private readonly Stream source; + + public PrefetchPacksDeserializer(Stream source) + { + this.source = source; + } + + /// + /// Read all the packs and indexes from the source stream and return a for each pack + /// and index. Caller must consume pack stream fully before the index stream. + /// + public IEnumerable EnumeratePacks() + { + this.ValidateHeader(); + + byte[] buffer = new byte[NumPackHeaderBytes]; + + int packCount = this.ReadPackCount(buffer); + + for (int i = 0; i < packCount; i++) + { + long timestamp; + long packLength; + long indexLength; + this.ReadPackHeader(buffer, out timestamp, out packLength, out indexLength); + + using (Stream packData = new RestrictedStream(this.source, packLength)) + using (Stream indexData = indexLength > 0 ? new RestrictedStream(this.source, indexLength) : null) + { + yield return new PackAndIndex(packData, indexData, timestamp); + } + } + } + + /// + /// Read the ushort pack count + /// + private ushort ReadPackCount(byte[] buffer) + { + StreamUtil.TryReadGreedy(this.source, buffer, 0, 2); + return BitConverter.ToUInt16(buffer, 0); + } + + /// + /// Parse the current pack header + /// + private void ReadPackHeader( + byte[] buffer, + out long timestamp, + out long packLength, + out long indexLength) + { + int totalBytes = StreamUtil.TryReadGreedy( + this.source, + buffer, + 0, + NumPackHeaderBytes); + + if (totalBytes == NumPackHeaderBytes) + { + timestamp = BitConverter.ToInt64(buffer, 0); + packLength = BitConverter.ToInt64(buffer, 8); + indexLength = BitConverter.ToInt64(buffer, 16); + } + else + { + throw new RetryableException( + string.Format( + "Reached end of stream before expected {0} bytes. Got {1}. Buffer: {2}", + NumPackHeaderBytes, + totalBytes, + SHA1Util.HexStringFromBytes(buffer))); + } + } + + private void ValidateHeader() + { + byte[] headerBuf = new byte[PrefetchPackExpectedHeader.Length]; + StreamUtil.TryReadGreedy(this.source, headerBuf, 0, headerBuf.Length); + if (!headerBuf.SequenceEqual(PrefetchPackExpectedHeader)) + { + throw new InvalidDataException("Unexpected header: " + Encoding.UTF8.GetString(headerBuf)); + } + } + + public class PackAndIndex + { + public PackAndIndex(Stream packStream, Stream idxStream, long timestamp) + { + this.PackStream = packStream; + this.IndexStream = idxStream; + this.Timestamp = timestamp; + this.UniqueId = Guid.NewGuid().ToString("N"); + } + + public Stream PackStream { get; } + public Stream IndexStream { get; } + public long Timestamp { get; } + public string UniqueId { get; } + } + } +} diff --git a/Scalar.Common/NetworkStreams/RestrictedStream.cs b/Scalar.Common/NetworkStreams/RestrictedStream.cs index 35dbbb86ae..bd9059297c 100644 --- a/Scalar.Common/NetworkStreams/RestrictedStream.cs +++ b/Scalar.Common/NetworkStreams/RestrictedStream.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; namespace Scalar.Common.NetworkStreams diff --git a/Scalar.Common/NuGetUpgrade/InstallActionInfo.cs b/Scalar.Common/NuGetUpgrade/InstallActionInfo.cs index 075b6156bc..5d7a01c800 100644 --- a/Scalar.Common/NuGetUpgrade/InstallActionInfo.cs +++ b/Scalar.Common/NuGetUpgrade/InstallActionInfo.cs @@ -2,8 +2,8 @@ namespace Scalar.Common.NuGetUpgrade { public class InstallActionInfo { - /// - /// Well known tokens that will be replaced when encountered in an Arguments field. + /// + /// Well known tokens that will be replaced when encountered in an Arguments field. /// public const string ManifestEntryInstallationIdToken = "installation_id"; public const string ManifestEntryLogDirectoryToken = "log_directory"; diff --git a/Scalar.Common/NuGetUpgrade/InstallManifest.cs b/Scalar.Common/NuGetUpgrade/InstallManifest.cs index 8e5e87084b..fbbee0b2db 100644 --- a/Scalar.Common/NuGetUpgrade/InstallManifest.cs +++ b/Scalar.Common/NuGetUpgrade/InstallManifest.cs @@ -1,50 +1,50 @@ -using Newtonsoft.Json; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.Common.NuGetUpgrade -{ - /// - /// Details on the upgrade included in this package, including information - /// on what packages are included and how to install them. - /// - public class InstallManifest - { - public const string WindowsPlatformKey = "Windows"; - - public InstallManifest() - { - this.PlatformInstallManifests = new Dictionary(); - } - - /// - /// Install manifests for different platforms. - /// - public Dictionary PlatformInstallManifests { get; private set; } - - public static InstallManifest FromJsonFile(string path) - { - using (StreamReader streamReader = File.OpenText(path)) - { - return InstallManifest.FromJson(streamReader); - } - } - - public static InstallManifest FromJsonString(string json) - { - return JsonConvert.DeserializeObject(json); - } - - public static InstallManifest FromJson(StreamReader stream) - { - JsonSerializer serializer = new JsonSerializer(); - return (InstallManifest)serializer.Deserialize(stream, typeof(InstallManifest)); - } - - public void AddPlatformInstallManifest(string platform, IEnumerable entries) - { - InstallManifestPlatform platformManifest = new InstallManifestPlatform(entries); - this.PlatformInstallManifests.Add(platform, platformManifest); - } - } -} +using Newtonsoft.Json; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.Common.NuGetUpgrade +{ + /// + /// Details on the upgrade included in this package, including information + /// on what packages are included and how to install them. + /// + public class InstallManifest + { + public const string WindowsPlatformKey = "Windows"; + + public InstallManifest() + { + this.PlatformInstallManifests = new Dictionary(); + } + + /// + /// Install manifests for different platforms. + /// + public Dictionary PlatformInstallManifests { get; private set; } + + public static InstallManifest FromJsonFile(string path) + { + using (StreamReader streamReader = File.OpenText(path)) + { + return InstallManifest.FromJson(streamReader); + } + } + + public static InstallManifest FromJsonString(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public static InstallManifest FromJson(StreamReader stream) + { + JsonSerializer serializer = new JsonSerializer(); + return (InstallManifest)serializer.Deserialize(stream, typeof(InstallManifest)); + } + + public void AddPlatformInstallManifest(string platform, IEnumerable entries) + { + InstallManifestPlatform platformManifest = new InstallManifestPlatform(entries); + this.PlatformInstallManifests.Add(platform, platformManifest); + } + } +} diff --git a/Scalar.Common/NuGetUpgrade/InstallManifestPlatform.cs b/Scalar.Common/NuGetUpgrade/InstallManifestPlatform.cs index f405133189..af6880169f 100644 --- a/Scalar.Common/NuGetUpgrade/InstallManifestPlatform.cs +++ b/Scalar.Common/NuGetUpgrade/InstallManifestPlatform.cs @@ -1,20 +1,20 @@ -using System.Collections.Generic; -using System.Linq; - -namespace Scalar.Common.NuGetUpgrade -{ - public class InstallManifestPlatform - { - public InstallManifestPlatform() - { - this.InstallActions = new List(); - } - - public InstallManifestPlatform(IEnumerable entries) - { - this.InstallActions = entries?.ToList() ?? new List(); - } - - public List InstallActions { get; } - } -} +using System.Collections.Generic; +using System.Linq; + +namespace Scalar.Common.NuGetUpgrade +{ + public class InstallManifestPlatform + { + public InstallManifestPlatform() + { + this.InstallActions = new List(); + } + + public InstallManifestPlatform(IEnumerable entries) + { + this.InstallActions = entries?.ToList() ?? new List(); + } + + public List InstallActions { get; } + } +} diff --git a/Scalar.Common/NuGetUpgrade/NuGetFeed.cs b/Scalar.Common/NuGetUpgrade/NuGetFeed.cs index 5a17eccb68..162ec1ca2f 100644 --- a/Scalar.Common/NuGetUpgrade/NuGetFeed.cs +++ b/Scalar.Common/NuGetUpgrade/NuGetFeed.cs @@ -1,273 +1,273 @@ -using NuGet.Commands; -using NuGet.Common; -using NuGet.Configuration; -using NuGet.Packaging.Core; -using NuGet.Protocol; -using NuGet.Protocol.Core.Types; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.Common.NuGetUpgrade -{ - /// - /// Handles interactions with a NuGet Feed. - /// - public class NuGetFeed : IDisposable - { - // This is the SHA256 Certificate Thumbrint we expect packages from Microsoft to be signed with - private const string TrustedMicrosoftCertFingerprint = "3F9001EA83C560D712C24CF213C3D312CB3BFF51EE89435D3430BD06B5D0EECE"; - - private readonly ITracer tracer; - private readonly string feedUrl; - private readonly string feedName; - private readonly string downloadFolder; - private readonly bool platformSupportsEncryption; - - private SourceRepository sourceRepository; - private string personalAccessToken; - private SourceCacheContext sourceCacheContext; - private ILogger nuGetLogger; - - public NuGetFeed( - string feedUrl, - string feedName, - string downloadFolder, - string personalAccessToken, - bool platformSupportsEncryption, - ITracer tracer) - { - this.feedUrl = feedUrl; - this.feedName = feedName; - this.downloadFolder = downloadFolder; - this.personalAccessToken = personalAccessToken; - this.tracer = tracer; - - // Configure the NuGet SourceCacheContext - - // - Direct download packages - do not download to global - // NuGet cache. This is set in NullSourceCacheContext.Instance - // - NoCache - Do not cache package version lists - this.sourceCacheContext = NullSourceCacheContext.Instance.Clone(); - this.sourceCacheContext.NoCache = true; - this.platformSupportsEncryption = platformSupportsEncryption; - - this.nuGetLogger = new Logger(this.tracer); - this.SetSourceRepository(); - } - - public void Dispose() - { - this.sourceRepository = null; - this.sourceCacheContext?.Dispose(); - this.sourceCacheContext = null; - } - - public virtual void SetCredentials(string credential) - { - this.personalAccessToken = credential; - - this.SetSourceRepository(); - } - - /// - /// Query a NuGet feed for list of packages that match the packageId. - /// - /// - /// List of packages that match query parameters - public virtual async Task> QueryFeedAsync(string packageId) - { - PackageMetadataResource packageMetadataResource = await this.sourceRepository.GetResourceAsync(); - IEnumerable queryResults = await packageMetadataResource.GetMetadataAsync( - packageId, - includePrerelease: false, - includeUnlisted: false, - sourceCacheContext: this.sourceCacheContext, - log: this.nuGetLogger, - token: CancellationToken.None); - - return queryResults.ToList(); - } - - /// - /// Download the specified packageId from the NuGet feed. - /// - /// PackageIdentity to download. - /// Path to the downloaded package. - public virtual async Task DownloadPackageAsync(PackageIdentity packageId) - { - string downloadPath = Path.Combine(this.downloadFolder, $"{this.feedName}.zip"); - PackageDownloadContext packageDownloadContext = new PackageDownloadContext( - this.sourceCacheContext, - this.downloadFolder, - true); - - DownloadResource downloadResource = await this.sourceRepository.GetResourceAsync(); - - using (DownloadResourceResult downloadResourceResult = await downloadResource.GetDownloadResourceResultAsync( - packageId, - packageDownloadContext, - globalPackagesFolder: string.Empty, - logger: this.nuGetLogger, - token: CancellationToken.None)) - { - if (downloadResourceResult.Status != DownloadResourceResultStatus.Available) - { - throw new Exception("Download of NuGet package failed. DownloadResult Status: {downloadResourceResult.Status}"); - } - - using (FileStream fileStream = File.Create(downloadPath)) - { - downloadResourceResult.PackageStream.CopyTo(fileStream); - } - } - - return downloadPath; - } - - public virtual bool VerifyPackage(string packagePath) - { - VerifyArgs verifyArgs = new VerifyArgs() - { - Verifications = new VerifyArgs.Verification[] { VerifyArgs.Verification.All }, - PackagePath = packagePath, - CertificateFingerprint = new List() { TrustedMicrosoftCertFingerprint }, - Logger = this.nuGetLogger - }; - - VerifyCommandRunner verifyCommandRunner = new VerifyCommandRunner(); - int result = verifyCommandRunner.ExecuteCommandAsync(verifyArgs).Result; - return result == 0; - } - - protected static EventMetadata CreateEventMetadata(Exception e = null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(NuGetFeed)); - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - return metadata; - } - - private static PackageSourceCredential BuildCredentialsFromPAT(string personalAccessToken, bool storePasswordInClearText) - { - // The storePasswordInClearText property is used to control whether the password - // is written to NuGet config files in clear text or not. It also controls whether the - // password is stored encrypted in memory or not. The ability to encrypt / decrypt the password - // is not supported in non-windows platforms at this point. - // We do not actually write out config files or store the password (except in memory). As in our - // usage of NuGet functionality we do not write out config files, it is OK to not set this property - // (with the tradeoff being the password is not encrypted in memory, and we need to make sure that new code - // does not start to write out config files). - return PackageSourceCredential.FromUserInput( - "VfsForGitNugetUpgrader", - "PersonalAccessToken", - personalAccessToken, - storePasswordInClearText: storePasswordInClearText); - } - - private void SetSourceRepository() - { - this.sourceRepository = Repository.Factory.GetCoreV3(this.feedUrl); - if (!string.IsNullOrEmpty(this.personalAccessToken)) - { - this.sourceRepository.PackageSource.Credentials = BuildCredentialsFromPAT(this.personalAccessToken, !this.platformSupportsEncryption); - } - } - - /// - /// Implementation of logger used by NuGet library. It takes all output - /// and redirects it to the Scalar logger. - /// - private class Logger : ILogger - { - private ITracer tracer; - - public Logger(ITracer tracer) - { - this.tracer = tracer; - } - - public void Log(LogLevel level, string data) - { - string message = $"NuGet Logger: ({level}): {data}"; - switch (level) - { - case LogLevel.Debug: - case LogLevel.Verbose: - case LogLevel.Minimal: - case LogLevel.Information: - this.tracer.RelatedInfo(message); - break; - case LogLevel.Warning: - this.tracer.RelatedWarning(message); - break; - case LogLevel.Error: - this.tracer.RelatedWarning(message); - break; - default: - this.tracer.RelatedWarning(message); - break; - } - } - - public void Log(ILogMessage message) - { - this.Log(message.Level, message.Message); - } - - public Task LogAsync(LogLevel level, string data) - { - this.Log(level, data); - return Task.CompletedTask; - } - - public Task LogAsync(ILogMessage message) - { - this.Log(message); - return Task.CompletedTask; - } - - public void LogDebug(string data) - { - this.Log(LogLevel.Debug, data); - } - - public void LogError(string data) - { - this.Log(LogLevel.Error, data); - } - - public void LogInformation(string data) - { - this.Log(LogLevel.Information, data); - } - - public void LogInformationSummary(string data) - { - this.Log(LogLevel.Information, data); - } - - public void LogMinimal(string data) - { - this.Log(LogLevel.Minimal, data); - } - - public void LogVerbose(string data) - { - this.Log(LogLevel.Verbose, data); - } - - public void LogWarning(string data) - { - this.Log(LogLevel.Warning, data); - } - } - } -} +using NuGet.Commands; +using NuGet.Common; +using NuGet.Configuration; +using NuGet.Packaging.Core; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.Common.NuGetUpgrade +{ + /// + /// Handles interactions with a NuGet Feed. + /// + public class NuGetFeed : IDisposable + { + // This is the SHA256 Certificate Thumbrint we expect packages from Microsoft to be signed with + private const string TrustedMicrosoftCertFingerprint = "3F9001EA83C560D712C24CF213C3D312CB3BFF51EE89435D3430BD06B5D0EECE"; + + private readonly ITracer tracer; + private readonly string feedUrl; + private readonly string feedName; + private readonly string downloadFolder; + private readonly bool platformSupportsEncryption; + + private SourceRepository sourceRepository; + private string personalAccessToken; + private SourceCacheContext sourceCacheContext; + private ILogger nuGetLogger; + + public NuGetFeed( + string feedUrl, + string feedName, + string downloadFolder, + string personalAccessToken, + bool platformSupportsEncryption, + ITracer tracer) + { + this.feedUrl = feedUrl; + this.feedName = feedName; + this.downloadFolder = downloadFolder; + this.personalAccessToken = personalAccessToken; + this.tracer = tracer; + + // Configure the NuGet SourceCacheContext - + // - Direct download packages - do not download to global + // NuGet cache. This is set in NullSourceCacheContext.Instance + // - NoCache - Do not cache package version lists + this.sourceCacheContext = NullSourceCacheContext.Instance.Clone(); + this.sourceCacheContext.NoCache = true; + this.platformSupportsEncryption = platformSupportsEncryption; + + this.nuGetLogger = new Logger(this.tracer); + this.SetSourceRepository(); + } + + public void Dispose() + { + this.sourceRepository = null; + this.sourceCacheContext?.Dispose(); + this.sourceCacheContext = null; + } + + public virtual void SetCredentials(string credential) + { + this.personalAccessToken = credential; + + this.SetSourceRepository(); + } + + /// + /// Query a NuGet feed for list of packages that match the packageId. + /// + /// + /// List of packages that match query parameters + public virtual async Task> QueryFeedAsync(string packageId) + { + PackageMetadataResource packageMetadataResource = await this.sourceRepository.GetResourceAsync(); + IEnumerable queryResults = await packageMetadataResource.GetMetadataAsync( + packageId, + includePrerelease: false, + includeUnlisted: false, + sourceCacheContext: this.sourceCacheContext, + log: this.nuGetLogger, + token: CancellationToken.None); + + return queryResults.ToList(); + } + + /// + /// Download the specified packageId from the NuGet feed. + /// + /// PackageIdentity to download. + /// Path to the downloaded package. + public virtual async Task DownloadPackageAsync(PackageIdentity packageId) + { + string downloadPath = Path.Combine(this.downloadFolder, $"{this.feedName}.zip"); + PackageDownloadContext packageDownloadContext = new PackageDownloadContext( + this.sourceCacheContext, + this.downloadFolder, + true); + + DownloadResource downloadResource = await this.sourceRepository.GetResourceAsync(); + + using (DownloadResourceResult downloadResourceResult = await downloadResource.GetDownloadResourceResultAsync( + packageId, + packageDownloadContext, + globalPackagesFolder: string.Empty, + logger: this.nuGetLogger, + token: CancellationToken.None)) + { + if (downloadResourceResult.Status != DownloadResourceResultStatus.Available) + { + throw new Exception("Download of NuGet package failed. DownloadResult Status: {downloadResourceResult.Status}"); + } + + using (FileStream fileStream = File.Create(downloadPath)) + { + downloadResourceResult.PackageStream.CopyTo(fileStream); + } + } + + return downloadPath; + } + + public virtual bool VerifyPackage(string packagePath) + { + VerifyArgs verifyArgs = new VerifyArgs() + { + Verifications = new VerifyArgs.Verification[] { VerifyArgs.Verification.All }, + PackagePath = packagePath, + CertificateFingerprint = new List() { TrustedMicrosoftCertFingerprint }, + Logger = this.nuGetLogger + }; + + VerifyCommandRunner verifyCommandRunner = new VerifyCommandRunner(); + int result = verifyCommandRunner.ExecuteCommandAsync(verifyArgs).Result; + return result == 0; + } + + protected static EventMetadata CreateEventMetadata(Exception e = null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(NuGetFeed)); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + private static PackageSourceCredential BuildCredentialsFromPAT(string personalAccessToken, bool storePasswordInClearText) + { + // The storePasswordInClearText property is used to control whether the password + // is written to NuGet config files in clear text or not. It also controls whether the + // password is stored encrypted in memory or not. The ability to encrypt / decrypt the password + // is not supported in non-windows platforms at this point. + // We do not actually write out config files or store the password (except in memory). As in our + // usage of NuGet functionality we do not write out config files, it is OK to not set this property + // (with the tradeoff being the password is not encrypted in memory, and we need to make sure that new code + // does not start to write out config files). + return PackageSourceCredential.FromUserInput( + "VfsForGitNugetUpgrader", + "PersonalAccessToken", + personalAccessToken, + storePasswordInClearText: storePasswordInClearText); + } + + private void SetSourceRepository() + { + this.sourceRepository = Repository.Factory.GetCoreV3(this.feedUrl); + if (!string.IsNullOrEmpty(this.personalAccessToken)) + { + this.sourceRepository.PackageSource.Credentials = BuildCredentialsFromPAT(this.personalAccessToken, !this.platformSupportsEncryption); + } + } + + /// + /// Implementation of logger used by NuGet library. It takes all output + /// and redirects it to the Scalar logger. + /// + private class Logger : ILogger + { + private ITracer tracer; + + public Logger(ITracer tracer) + { + this.tracer = tracer; + } + + public void Log(LogLevel level, string data) + { + string message = $"NuGet Logger: ({level}): {data}"; + switch (level) + { + case LogLevel.Debug: + case LogLevel.Verbose: + case LogLevel.Minimal: + case LogLevel.Information: + this.tracer.RelatedInfo(message); + break; + case LogLevel.Warning: + this.tracer.RelatedWarning(message); + break; + case LogLevel.Error: + this.tracer.RelatedWarning(message); + break; + default: + this.tracer.RelatedWarning(message); + break; + } + } + + public void Log(ILogMessage message) + { + this.Log(message.Level, message.Message); + } + + public Task LogAsync(LogLevel level, string data) + { + this.Log(level, data); + return Task.CompletedTask; + } + + public Task LogAsync(ILogMessage message) + { + this.Log(message); + return Task.CompletedTask; + } + + public void LogDebug(string data) + { + this.Log(LogLevel.Debug, data); + } + + public void LogError(string data) + { + this.Log(LogLevel.Error, data); + } + + public void LogInformation(string data) + { + this.Log(LogLevel.Information, data); + } + + public void LogInformationSummary(string data) + { + this.Log(LogLevel.Information, data); + } + + public void LogMinimal(string data) + { + this.Log(LogLevel.Minimal, data); + } + + public void LogVerbose(string data) + { + this.Log(LogLevel.Verbose, data); + } + + public void LogWarning(string data) + { + this.Log(LogLevel.Warning, data); + } + } + } +} diff --git a/Scalar.Common/NuGetUpgrade/NuGetUpgrader.cs b/Scalar.Common/NuGetUpgrade/NuGetUpgrader.cs index a2a469af2e..40ee7b2403 100644 --- a/Scalar.Common/NuGetUpgrade/NuGetUpgrader.cs +++ b/Scalar.Common/NuGetUpgrade/NuGetUpgrader.cs @@ -1,731 +1,731 @@ -using NuGet.Packaging.Core; -using NuGet.Protocol.Core.Types; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Threading; - -namespace Scalar.Common.NuGetUpgrade -{ - public class NuGetUpgrader : ProductUpgrader - { - protected readonly NuGetUpgraderConfig nuGetUpgraderConfig; - protected Version highestVersionAvailable; - - private const string ContentDirectoryName = "content"; - private const string InstallManifestFileName = "install-manifest.json"; - private const string ExtractedInstallerDirectoryName = "InstallerTemp"; - - private InstallManifest installManifest; - private NuGetFeed nuGetFeed; - private ICredentialStore credentialStore; - private bool isNuGetFeedInitialized; - - public NuGetUpgrader( - string currentVersion, - ITracer tracer, - PhysicalFileSystem fileSystem, - bool dryRun, - bool noVerify, - NuGetUpgraderConfig config, - string downloadFolder, - ICredentialStore credentialStore) - : this( - currentVersion, - tracer, - dryRun, - noVerify, - fileSystem, - config, - new NuGetFeed( - config.FeedUrl, - config.PackageFeedName, - downloadFolder, - null, - ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, - tracer), - credentialStore, - ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) - { - } - - internal NuGetUpgrader( - string currentVersion, - ITracer tracer, - bool dryRun, - bool noVerify, - PhysicalFileSystem fileSystem, - NuGetUpgraderConfig config, - NuGetFeed nuGetFeed, - ICredentialStore credentialStore, - ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy) - : base( - currentVersion, - tracer, - dryRun, - noVerify, - fileSystem, - productUpgraderPlatformStrategy) - { - this.nuGetUpgraderConfig = config; - - this.nuGetFeed = nuGetFeed; - this.credentialStore = credentialStore; - - // Extract the folder inside ProductUpgraderInfo.GetAssetDownloadsPath to ensure the - // correct ACLs are in place - this.ExtractedInstallerPath = Path.Combine( - ProductUpgraderInfo.GetAssetDownloadsPath(), - ExtractedInstallerDirectoryName); - } - - public string DownloadedPackagePath { get; private set; } - - public override bool SupportsAnonymousVersionQuery { get => false; } - - /// - /// Path to unzip the downloaded upgrade package - /// - private string ExtractedInstallerPath { get; } - - /// - /// Try to load a NuGetUpgrader from config settings. - /// Flag to indicate whether the system is configured to use a NuGetUpgrader. - /// A NuGetUpgrader can be set as the Upgrader to use, but it might not be properly configured. - /// - /// True if able to load a properly configured NuGetUpgrader - /// - public static bool TryCreate( - ITracer tracer, - PhysicalFileSystem fileSystem, - LocalScalarConfig scalarConfig, - ICredentialStore credentialStore, - bool dryRun, - bool noVerify, - out NuGetUpgrader nuGetUpgrader, - out bool isConfigured, - out string error) - { - NuGetUpgraderConfig upgraderConfig = new NuGetUpgraderConfig(tracer, scalarConfig); - nuGetUpgrader = null; - isConfigured = false; - - if (!upgraderConfig.TryLoad(out error)) - { - nuGetUpgrader = null; - return false; - } - - if (!(isConfigured = upgraderConfig.IsConfigured(out error))) - { - return false; - } - - // At this point, we have determined that the system is set up to use - // the NuGetUpgrader - - if (!upgraderConfig.IsReady(out error)) - { - return false; - } - - nuGetUpgrader = new NuGetUpgrader( - ProcessHelper.GetCurrentProcessVersion(), - tracer, - fileSystem, - dryRun, - noVerify, - upgraderConfig, - ProductUpgraderInfo.GetAssetDownloadsPath(), - credentialStore); - - return true; - } - - /// - /// Performs a replacement on well known strings in the arguments field of a manifest entry. - /// - /// The unprocessed string to use as arguments to an install command - /// A unique installer ID to replace the installer_id token with. - /// The argument string with tokens replaced. - public static string ReplaceArgTokens(string src, string installationId, string logsDirectory, string installerBaseDirectory) - { - string dst = src - .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryLogDirectoryToken), logsDirectory) - .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryInstallationIdToken), installationId) - .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryInstallerBaseDirectoryToken), installerBaseDirectory); - return dst; - } - - public override void Dispose() - { - this.nuGetFeed?.Dispose(); - this.nuGetFeed = null; - base.Dispose(); - } - - public override bool UpgradeAllowed(out string message) - { - if (string.IsNullOrEmpty(this.nuGetUpgraderConfig.FeedUrl)) - { - message = "Nuget Feed URL has not been configured"; - return false; - } - else if (string.IsNullOrEmpty(this.nuGetUpgraderConfig.PackageFeedName)) - { - message = "NuGet package feed has not been configured"; - return false; - } - - message = null; - return true; - } - - public override bool TryQueryNewestVersion(out Version newVersion, out string message) - { - try - { - if (!this.EnsureNuGetFeedInitialized(out message)) - { - newVersion = null; - return false; - } - - IList queryResults = this.QueryFeed(firstAttempt: true); - - // Find the package with the highest version - IPackageSearchMetadata newestPackage = null; - foreach (IPackageSearchMetadata result in queryResults) - { - if (newestPackage == null || result.Identity.Version > newestPackage.Identity.Version) - { - newestPackage = result; - } - } - - if (newestPackage != null && - newestPackage.Identity.Version.Version > this.installedVersion) - { - this.highestVersionAvailable = newestPackage.Identity.Version.Version; - } - - newVersion = this.highestVersionAvailable; - - if (newVersion != null) - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); - message = $"New version {newestPackage.Identity.Version} is available."; - return true; - } - else if (newestPackage != null) - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); - message = $"highest version available is {newestPackage.Identity.Version}, you are up-to-date"; - return true; - } - else - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed."); - message = $"No versions available via feed."; - } - } - catch (Exception ex) - { - this.TraceException( - ex, - nameof(this.TryQueryNewestVersion), - "Exception encountered querying for newest version of upgrade package."); - message = ex.Message; - newVersion = null; - } - - return false; - } - - public override bool TryDownloadNewestVersion(out string errorMessage) - { - if (this.highestVersionAvailable == null) - { - // If we hit this code path, it indicates there was a - // programmer error. The expectation is that this - // method will only be called after - // TryQueryNewestVersion has been called, and - // indicates that a newer version is available. - errorMessage = "No new version to download. Query for newest version to ensure a new version is available before downloading."; - return false; - } - - if (!this.EnsureNuGetFeedInitialized(out errorMessage)) - { - return false; - } - - if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage)) - { - this.tracer.RelatedError($"{nameof(NuGetUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}"); - return false; - } - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryDownloadNewestVersion), EventLevel.Informational)) - { - try - { - PackageIdentity packageId = this.GetPackageForVersion(this.highestVersionAvailable); - - if (packageId == null) - { - errorMessage = $"The specified version {this.highestVersionAvailable} was not found in the NuGet feed. Please check with your administrator to make sure the feed is set up correctly."; - return false; - } - - this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(packageId).GetAwaiter().GetResult(); - } - catch (Exception ex) - { - this.TraceException( - activity, - ex, - nameof(this.TryDownloadNewestVersion), - "Exception encountered downloading newest version of upgrade package."); - errorMessage = ex.Message; - return false; - } - } - - if (!this.noVerify) - { - if (!this.nuGetFeed.VerifyPackage(this.DownloadedPackagePath)) - { - errorMessage = "Package signature validation failed. Check the upgrade logs for more details."; - this.tracer.RelatedError(errorMessage); - this.fileSystem.DeleteFile(this.DownloadedPackagePath); - return false; - } - } - - errorMessage = null; - return true; - } - - public override bool TryCleanup(out string error) - { - return this.TryRecursivelyDeleteInstallerDirectory(out error); - } - - public override bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error) - { - string localError = null; - int installerExitCode; - bool installSuccessful = true; - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryRunInstaller), EventLevel.Informational)) - { - InstallActionInfo currentInstallAction = null; - try - { - string platformKey = ScalarPlatform.Instance.Name; - - if (!this.TryRecursivelyDeleteInstallerDirectory(out error)) - { - return false; - } - - if (!this.noVerify) - { - if (!this.nuGetFeed.VerifyPackage(this.DownloadedPackagePath)) - { - error = "Package signature validation failed. Check the upgrade logs for more details."; - activity.RelatedError(error); - this.fileSystem.DeleteFile(this.DownloadedPackagePath); - return false; - } - } - - this.UnzipPackage(); - this.installManifest = InstallManifest.FromJsonFile(Path.Combine(this.ExtractedInstallerPath, ContentDirectoryName, InstallManifestFileName)); - if (!this.installManifest.PlatformInstallManifests.TryGetValue(platformKey, out InstallManifestPlatform platformInstallManifest) || - platformInstallManifest == null) - { - activity.RelatedError($"Extracted InstallManifest from JSON, but there was no entry for {platformKey}."); - error = $"No entry in the manifest for the current platform ({platformKey}). Please verify the upgrade package."; - return false; - } - - activity.RelatedInfo($"Extracted InstallManifest from JSON. InstallActions: {platformInstallManifest.InstallActions.Count}"); - - foreach (InstallActionInfo entry in platformInstallManifest.InstallActions) - { - currentInstallAction = entry; - string installerBasePath = Path.Combine(this.ExtractedInstallerPath, ContentDirectoryName); - - string args = entry.Args ?? string.Empty; - - // Replace tokens on args - string processedArgs = NuGetUpgrader.ReplaceArgTokens(args, this.UpgradeInstanceId, ProductUpgraderInfo.GetLogDirectoryPath(), $"\"{installerBasePath}\""); - - activity.RelatedInfo( - "Running install action: Name: {0}, Version: {1}, InstallerPath: {2}, Command: {3}, RawArgs: {4}, ProcessedArgs: {5}", - entry.Name, - entry.Version, - entry.InstallerRelativePath ?? string.Empty, - entry.Command ?? string.Empty, - args, - processedArgs); - - string progressMessage = string.IsNullOrWhiteSpace(entry.Version) ? - $"Running {entry.Name}" : - $"Running {entry.Name} (version {entry.Version})"; - - installActionWrapper( - () => - { - if (!this.dryRun) - { - if (!string.IsNullOrEmpty(entry.Command)) - { - this.RunInstaller(entry.Command, processedArgs, out installerExitCode, out localError); - } - else - { - string installerPath = Path.Combine(installerBasePath, entry.InstallerRelativePath); - this.RunInstaller(installerPath, processedArgs, out installerExitCode, out localError); - } - } - else - { - // We add a sleep here to ensure - // the message for this install - // action is written to the - // console. Even though the - // message is written with a delay - // of 0, the messages are not - // always written out. If / when - // we can ensure that the message - // is written out to console, then - // we can remove this sleep. - Thread.Sleep(1500); - installerExitCode = 0; - } - - installSuccessful = installerExitCode == 0; - - return installSuccessful; - }, - progressMessage); - - if (!installSuccessful) - { - break; - } - } - } - catch (Exception ex) - { - localError = ex.Message; - installSuccessful = false; - } - - if (!installSuccessful) - { - string installActionName = string.IsNullOrEmpty(currentInstallAction?.Name) ? - "installer" : - currentInstallAction.Name; - - error = string.IsNullOrEmpty(localError) ? - $"The {installActionName} failed, but no error message was provided by the failing command." : - $"The {installActionName} failed with the following error: {localError}"; - - activity.RelatedError($"Could not complete all install actions. The following error was encountered: {error}"); - return false; - } - else - { - activity.RelatedInfo($"Install actions completed successfully."); - error = null; - return true; - } - } - } - - protected static EventMetadata CreateEventMetadata(Exception e = null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(NuGetFeed)); - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - return metadata; - } - - private static string ReplacementToken(string tokenString) - { - return "{" + tokenString + "}"; - } - - private PackageIdentity GetPackageForVersion(Version version) - { - IList queryResults = this.QueryFeed(firstAttempt: true); - - IPackageSearchMetadata packageForVersion = null; - foreach (IPackageSearchMetadata result in queryResults) - { - if (result.Identity.Version.Version == version) - { - packageForVersion = result; - break; - } - } - - return packageForVersion?.Identity; - } - - private bool TryGetPersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error) - { - return this.credentialStore.TryGetCredential(this.tracer, credentialUrl, out string username, out token, out error); - } - - private bool TryReacquirePersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error) - { - if (!this.credentialStore.TryDeleteCredential(this.tracer, credentialUrl, username: null, password: null, error: out error)) - { - token = null; - return false; - } - - return this.TryGetPersonalAccessToken(credentialUrl, tracer, out token, out error); - } - - private void UnzipPackage() - { - ZipFile.ExtractToDirectory(this.DownloadedPackagePath, this.ExtractedInstallerPath); - } - - private bool TryRecursivelyDeleteInstallerDirectory(out string error) - { - error = null; - Exception e; - if (!this.fileSystem.TryDeleteDirectory(this.ExtractedInstallerPath, out e)) - { - if (e != null) - { - this.TraceException( - e, - nameof(this.TryRecursivelyDeleteInstallerDirectory), - $"Exception encountered while deleting {this.ExtractedInstallerPath}."); - } - - error = e?.Message ?? "Failed to delete directory, but no error was specified."; - return false; - } - - return true; - } - - private IList QueryFeed(bool firstAttempt) - { - try - { - return this.nuGetFeed.QueryFeedAsync(this.nuGetUpgraderConfig.PackageFeedName).GetAwaiter().GetResult(); - } - catch (Exception ex) when (firstAttempt && - this.IsAuthRelatedException(ex)) - { - // If we fail to query the feed due to an authorization error, then it is possible we have stale - // credentials, or credentials without the correct scope. Re-aquire fresh credentials and try again. - EventMetadata data = CreateEventMetadata(ex); - this.tracer.RelatedWarning(data, "Failed to query feed due to unauthorized error. Re-acquiring new credentials and trying again."); - - if (!this.TryRefreshCredentials(out string error)) - { - // If we were unable to re-acquire credentials, throw a new exception indicating that we tried to handle this, but were unable to. - throw new Exception($"Failed to query the feed for upgrade packages due to: {ex.Message}, and was not able to re-acquire new credentials due to: {error}", ex); - } - - // Now that we have re-acquired credentials, try again - but with the retry flag set to false. - return this.QueryFeed(firstAttempt: false); - } - catch (Exception ex) - { - EventMetadata data = CreateEventMetadata(ex); - string message = $"Error encountered when querying NuGet feed. Is first attempt: {firstAttempt}."; - this.tracer.RelatedWarning(data, message); - throw new Exception($"Failed to query the NuGet package feed due to error: {ex.Message}", ex); - } - } - - private bool IsAuthRelatedException(Exception ex) - { - // In observation, we have seen either an HttpRequestException directly, or - // a FatalProtocolException wrapping an HttpRequestException when we are not able - // to auth against the NuGet feed. - System.Net.Http.HttpRequestException httpRequestException = null; - if (ex is System.Net.Http.HttpRequestException) - { - httpRequestException = ex as System.Net.Http.HttpRequestException; - } - else if (ex is FatalProtocolException && - ex.InnerException is System.Net.Http.HttpRequestException) - { - httpRequestException = ex.InnerException as System.Net.Http.HttpRequestException; - } - - if (httpRequestException != null && - (httpRequestException.Message.Contains("401") || httpRequestException.Message.Contains("403"))) - { - return true; - } - - return false; - } - - private bool TryRefreshCredentials(out string error) - { - try - { - string authUrl; - if (!AzDevOpsOrgFromNuGetFeed.TryCreateCredentialQueryUrl(this.nuGetUpgraderConfig.FeedUrl, out authUrl, out error)) - { - return false; - } - - if (!this.TryReacquirePersonalAccessToken(authUrl, this.tracer, out string token, out error)) - { - return false; - } - - this.nuGetFeed.SetCredentials(token); - return true; - } - catch (Exception ex) - { - error = ex.Message; - this.TraceException(ex, nameof(this.TryRefreshCredentials), "Failed to refresh credentials."); - return false; - } - } - - private bool EnsureNuGetFeedInitialized(out string error) - { - if (!this.isNuGetFeedInitialized) - { - if (this.credentialStore == null) - { - throw new InvalidOperationException("Attempted to call method that requires authentication but no CredentialStore is configured."); - } - - string authUrl; - if (!AzDevOpsOrgFromNuGetFeed.TryCreateCredentialQueryUrl(this.nuGetUpgraderConfig.FeedUrl, out authUrl, out error)) - { - return false; - } - - if (!this.TryGetPersonalAccessToken(authUrl, this.tracer, out string token, out error)) - { - return false; - } - - this.nuGetFeed.SetCredentials(token); - this.isNuGetFeedInitialized = true; - } - - error = null; - return true; - } - - public class NuGetUpgraderConfig - { - protected readonly ITracer tracer; - protected readonly LocalScalarConfig localConfig; - - public NuGetUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) - { - this.tracer = tracer; - this.localConfig = localScalarConfig; - } - - public NuGetUpgraderConfig( - ITracer tracer, - LocalScalarConfig localScalarConfig, - string feedUrl, - string packageFeedName) - : this(tracer, localScalarConfig) - { - this.FeedUrl = feedUrl; - this.PackageFeedName = packageFeedName; - } - - public string FeedUrl { get; private set; } - public string PackageFeedName { get; private set; } - public string CertificateFingerprint { get; private set; } - - /// - /// Check if the NuGetUpgrader is ready for use. A - /// NuGetUpgrader is considered ready if all required - /// config settings are present. - /// - public virtual bool IsReady(out string error) - { - if (string.IsNullOrEmpty(this.FeedUrl) || - string.IsNullOrEmpty(this.PackageFeedName)) - { - error = string.Join( - Environment.NewLine, - "One or more required settings for NuGetUpgrader are missing.", - $"Use `scalar config [{ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName}] ` to set the config."); - return false; - } - - error = null; - return true; - } - - /// - /// Check if the NuGetUpgrader is configured. - /// - public virtual bool IsConfigured(out string error) - { - if (string.IsNullOrEmpty(this.FeedUrl) && - string.IsNullOrEmpty(this.PackageFeedName)) - { - error = string.Join( - Environment.NewLine, - "NuGet upgrade server is not configured.", - $"Use `scalar config [ {ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName}] ` to set the config."); - return false; - } - - error = null; - return true; - } - - /// - /// Try to load the config for a NuGet upgrader. Returns false if there was an error reading the config. - /// - public virtual bool TryLoad(out string error) - { - string configValue; - if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl, out configValue, out error)) - { - this.tracer.RelatedError(error); - return false; - } - - this.FeedUrl = configValue; - - if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName, out configValue, out error)) - { - this.tracer.RelatedError(error); - return false; - } - - this.PackageFeedName = configValue; - return true; - } - } - } -} +using NuGet.Packaging.Core; +using NuGet.Protocol.Core.Types; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Threading; + +namespace Scalar.Common.NuGetUpgrade +{ + public class NuGetUpgrader : ProductUpgrader + { + protected readonly NuGetUpgraderConfig nuGetUpgraderConfig; + protected Version highestVersionAvailable; + + private const string ContentDirectoryName = "content"; + private const string InstallManifestFileName = "install-manifest.json"; + private const string ExtractedInstallerDirectoryName = "InstallerTemp"; + + private InstallManifest installManifest; + private NuGetFeed nuGetFeed; + private ICredentialStore credentialStore; + private bool isNuGetFeedInitialized; + + public NuGetUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + bool dryRun, + bool noVerify, + NuGetUpgraderConfig config, + string downloadFolder, + ICredentialStore credentialStore) + : this( + currentVersion, + tracer, + dryRun, + noVerify, + fileSystem, + config, + new NuGetFeed( + config.FeedUrl, + config.PackageFeedName, + downloadFolder, + null, + ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, + tracer), + credentialStore, + ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) + { + } + + internal NuGetUpgrader( + string currentVersion, + ITracer tracer, + bool dryRun, + bool noVerify, + PhysicalFileSystem fileSystem, + NuGetUpgraderConfig config, + NuGetFeed nuGetFeed, + ICredentialStore credentialStore, + ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy) + : base( + currentVersion, + tracer, + dryRun, + noVerify, + fileSystem, + productUpgraderPlatformStrategy) + { + this.nuGetUpgraderConfig = config; + + this.nuGetFeed = nuGetFeed; + this.credentialStore = credentialStore; + + // Extract the folder inside ProductUpgraderInfo.GetAssetDownloadsPath to ensure the + // correct ACLs are in place + this.ExtractedInstallerPath = Path.Combine( + ProductUpgraderInfo.GetAssetDownloadsPath(), + ExtractedInstallerDirectoryName); + } + + public string DownloadedPackagePath { get; private set; } + + public override bool SupportsAnonymousVersionQuery { get => false; } + + /// + /// Path to unzip the downloaded upgrade package + /// + private string ExtractedInstallerPath { get; } + + /// + /// Try to load a NuGetUpgrader from config settings. + /// Flag to indicate whether the system is configured to use a NuGetUpgrader. + /// A NuGetUpgrader can be set as the Upgrader to use, but it might not be properly configured. + /// + /// True if able to load a properly configured NuGetUpgrader + /// + public static bool TryCreate( + ITracer tracer, + PhysicalFileSystem fileSystem, + LocalScalarConfig scalarConfig, + ICredentialStore credentialStore, + bool dryRun, + bool noVerify, + out NuGetUpgrader nuGetUpgrader, + out bool isConfigured, + out string error) + { + NuGetUpgraderConfig upgraderConfig = new NuGetUpgraderConfig(tracer, scalarConfig); + nuGetUpgrader = null; + isConfigured = false; + + if (!upgraderConfig.TryLoad(out error)) + { + nuGetUpgrader = null; + return false; + } + + if (!(isConfigured = upgraderConfig.IsConfigured(out error))) + { + return false; + } + + // At this point, we have determined that the system is set up to use + // the NuGetUpgrader + + if (!upgraderConfig.IsReady(out error)) + { + return false; + } + + nuGetUpgrader = new NuGetUpgrader( + ProcessHelper.GetCurrentProcessVersion(), + tracer, + fileSystem, + dryRun, + noVerify, + upgraderConfig, + ProductUpgraderInfo.GetAssetDownloadsPath(), + credentialStore); + + return true; + } + + /// + /// Performs a replacement on well known strings in the arguments field of a manifest entry. + /// + /// The unprocessed string to use as arguments to an install command + /// A unique installer ID to replace the installer_id token with. + /// The argument string with tokens replaced. + public static string ReplaceArgTokens(string src, string installationId, string logsDirectory, string installerBaseDirectory) + { + string dst = src + .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryLogDirectoryToken), logsDirectory) + .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryInstallationIdToken), installationId) + .Replace(NuGetUpgrader.ReplacementToken(InstallActionInfo.ManifestEntryInstallerBaseDirectoryToken), installerBaseDirectory); + return dst; + } + + public override void Dispose() + { + this.nuGetFeed?.Dispose(); + this.nuGetFeed = null; + base.Dispose(); + } + + public override bool UpgradeAllowed(out string message) + { + if (string.IsNullOrEmpty(this.nuGetUpgraderConfig.FeedUrl)) + { + message = "Nuget Feed URL has not been configured"; + return false; + } + else if (string.IsNullOrEmpty(this.nuGetUpgraderConfig.PackageFeedName)) + { + message = "NuGet package feed has not been configured"; + return false; + } + + message = null; + return true; + } + + public override bool TryQueryNewestVersion(out Version newVersion, out string message) + { + try + { + if (!this.EnsureNuGetFeedInitialized(out message)) + { + newVersion = null; + return false; + } + + IList queryResults = this.QueryFeed(firstAttempt: true); + + // Find the package with the highest version + IPackageSearchMetadata newestPackage = null; + foreach (IPackageSearchMetadata result in queryResults) + { + if (newestPackage == null || result.Identity.Version > newestPackage.Identity.Version) + { + newestPackage = result; + } + } + + if (newestPackage != null && + newestPackage.Identity.Version.Version > this.installedVersion) + { + this.highestVersionAvailable = newestPackage.Identity.Version.Version; + } + + newVersion = this.highestVersionAvailable; + + if (newVersion != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); + message = $"New version {newestPackage.Identity.Version} is available."; + return true; + } + else if (newestPackage != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); + message = $"highest version available is {newestPackage.Identity.Version}, you are up-to-date"; + return true; + } + else + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed."); + message = $"No versions available via feed."; + } + } + catch (Exception ex) + { + this.TraceException( + ex, + nameof(this.TryQueryNewestVersion), + "Exception encountered querying for newest version of upgrade package."); + message = ex.Message; + newVersion = null; + } + + return false; + } + + public override bool TryDownloadNewestVersion(out string errorMessage) + { + if (this.highestVersionAvailable == null) + { + // If we hit this code path, it indicates there was a + // programmer error. The expectation is that this + // method will only be called after + // TryQueryNewestVersion has been called, and + // indicates that a newer version is available. + errorMessage = "No new version to download. Query for newest version to ensure a new version is available before downloading."; + return false; + } + + if (!this.EnsureNuGetFeedInitialized(out errorMessage)) + { + return false; + } + + if (!this.TryCreateAndConfigureDownloadDirectory(this.tracer, out errorMessage)) + { + this.tracer.RelatedError($"{nameof(NuGetUpgrader)}.{nameof(this.TryCreateAndConfigureDownloadDirectory)} failed. {errorMessage}"); + return false; + } + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryDownloadNewestVersion), EventLevel.Informational)) + { + try + { + PackageIdentity packageId = this.GetPackageForVersion(this.highestVersionAvailable); + + if (packageId == null) + { + errorMessage = $"The specified version {this.highestVersionAvailable} was not found in the NuGet feed. Please check with your administrator to make sure the feed is set up correctly."; + return false; + } + + this.DownloadedPackagePath = this.nuGetFeed.DownloadPackageAsync(packageId).GetAwaiter().GetResult(); + } + catch (Exception ex) + { + this.TraceException( + activity, + ex, + nameof(this.TryDownloadNewestVersion), + "Exception encountered downloading newest version of upgrade package."); + errorMessage = ex.Message; + return false; + } + } + + if (!this.noVerify) + { + if (!this.nuGetFeed.VerifyPackage(this.DownloadedPackagePath)) + { + errorMessage = "Package signature validation failed. Check the upgrade logs for more details."; + this.tracer.RelatedError(errorMessage); + this.fileSystem.DeleteFile(this.DownloadedPackagePath); + return false; + } + } + + errorMessage = null; + return true; + } + + public override bool TryCleanup(out string error) + { + return this.TryRecursivelyDeleteInstallerDirectory(out error); + } + + public override bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error) + { + string localError = null; + int installerExitCode; + bool installSuccessful = true; + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryRunInstaller), EventLevel.Informational)) + { + InstallActionInfo currentInstallAction = null; + try + { + string platformKey = ScalarPlatform.Instance.Name; + + if (!this.TryRecursivelyDeleteInstallerDirectory(out error)) + { + return false; + } + + if (!this.noVerify) + { + if (!this.nuGetFeed.VerifyPackage(this.DownloadedPackagePath)) + { + error = "Package signature validation failed. Check the upgrade logs for more details."; + activity.RelatedError(error); + this.fileSystem.DeleteFile(this.DownloadedPackagePath); + return false; + } + } + + this.UnzipPackage(); + this.installManifest = InstallManifest.FromJsonFile(Path.Combine(this.ExtractedInstallerPath, ContentDirectoryName, InstallManifestFileName)); + if (!this.installManifest.PlatformInstallManifests.TryGetValue(platformKey, out InstallManifestPlatform platformInstallManifest) || + platformInstallManifest == null) + { + activity.RelatedError($"Extracted InstallManifest from JSON, but there was no entry for {platformKey}."); + error = $"No entry in the manifest for the current platform ({platformKey}). Please verify the upgrade package."; + return false; + } + + activity.RelatedInfo($"Extracted InstallManifest from JSON. InstallActions: {platformInstallManifest.InstallActions.Count}"); + + foreach (InstallActionInfo entry in platformInstallManifest.InstallActions) + { + currentInstallAction = entry; + string installerBasePath = Path.Combine(this.ExtractedInstallerPath, ContentDirectoryName); + + string args = entry.Args ?? string.Empty; + + // Replace tokens on args + string processedArgs = NuGetUpgrader.ReplaceArgTokens(args, this.UpgradeInstanceId, ProductUpgraderInfo.GetLogDirectoryPath(), $"\"{installerBasePath}\""); + + activity.RelatedInfo( + "Running install action: Name: {0}, Version: {1}, InstallerPath: {2}, Command: {3}, RawArgs: {4}, ProcessedArgs: {5}", + entry.Name, + entry.Version, + entry.InstallerRelativePath ?? string.Empty, + entry.Command ?? string.Empty, + args, + processedArgs); + + string progressMessage = string.IsNullOrWhiteSpace(entry.Version) ? + $"Running {entry.Name}" : + $"Running {entry.Name} (version {entry.Version})"; + + installActionWrapper( + () => + { + if (!this.dryRun) + { + if (!string.IsNullOrEmpty(entry.Command)) + { + this.RunInstaller(entry.Command, processedArgs, out installerExitCode, out localError); + } + else + { + string installerPath = Path.Combine(installerBasePath, entry.InstallerRelativePath); + this.RunInstaller(installerPath, processedArgs, out installerExitCode, out localError); + } + } + else + { + // We add a sleep here to ensure + // the message for this install + // action is written to the + // console. Even though the + // message is written with a delay + // of 0, the messages are not + // always written out. If / when + // we can ensure that the message + // is written out to console, then + // we can remove this sleep. + Thread.Sleep(1500); + installerExitCode = 0; + } + + installSuccessful = installerExitCode == 0; + + return installSuccessful; + }, + progressMessage); + + if (!installSuccessful) + { + break; + } + } + } + catch (Exception ex) + { + localError = ex.Message; + installSuccessful = false; + } + + if (!installSuccessful) + { + string installActionName = string.IsNullOrEmpty(currentInstallAction?.Name) ? + "installer" : + currentInstallAction.Name; + + error = string.IsNullOrEmpty(localError) ? + $"The {installActionName} failed, but no error message was provided by the failing command." : + $"The {installActionName} failed with the following error: {localError}"; + + activity.RelatedError($"Could not complete all install actions. The following error was encountered: {error}"); + return false; + } + else + { + activity.RelatedInfo($"Install actions completed successfully."); + error = null; + return true; + } + } + } + + protected static EventMetadata CreateEventMetadata(Exception e = null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(NuGetFeed)); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + private static string ReplacementToken(string tokenString) + { + return "{" + tokenString + "}"; + } + + private PackageIdentity GetPackageForVersion(Version version) + { + IList queryResults = this.QueryFeed(firstAttempt: true); + + IPackageSearchMetadata packageForVersion = null; + foreach (IPackageSearchMetadata result in queryResults) + { + if (result.Identity.Version.Version == version) + { + packageForVersion = result; + break; + } + } + + return packageForVersion?.Identity; + } + + private bool TryGetPersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error) + { + return this.credentialStore.TryGetCredential(this.tracer, credentialUrl, out string username, out token, out error); + } + + private bool TryReacquirePersonalAccessToken(string credentialUrl, ITracer tracer, out string token, out string error) + { + if (!this.credentialStore.TryDeleteCredential(this.tracer, credentialUrl, username: null, password: null, error: out error)) + { + token = null; + return false; + } + + return this.TryGetPersonalAccessToken(credentialUrl, tracer, out token, out error); + } + + private void UnzipPackage() + { + ZipFile.ExtractToDirectory(this.DownloadedPackagePath, this.ExtractedInstallerPath); + } + + private bool TryRecursivelyDeleteInstallerDirectory(out string error) + { + error = null; + Exception e; + if (!this.fileSystem.TryDeleteDirectory(this.ExtractedInstallerPath, out e)) + { + if (e != null) + { + this.TraceException( + e, + nameof(this.TryRecursivelyDeleteInstallerDirectory), + $"Exception encountered while deleting {this.ExtractedInstallerPath}."); + } + + error = e?.Message ?? "Failed to delete directory, but no error was specified."; + return false; + } + + return true; + } + + private IList QueryFeed(bool firstAttempt) + { + try + { + return this.nuGetFeed.QueryFeedAsync(this.nuGetUpgraderConfig.PackageFeedName).GetAwaiter().GetResult(); + } + catch (Exception ex) when (firstAttempt && + this.IsAuthRelatedException(ex)) + { + // If we fail to query the feed due to an authorization error, then it is possible we have stale + // credentials, or credentials without the correct scope. Re-aquire fresh credentials and try again. + EventMetadata data = CreateEventMetadata(ex); + this.tracer.RelatedWarning(data, "Failed to query feed due to unauthorized error. Re-acquiring new credentials and trying again."); + + if (!this.TryRefreshCredentials(out string error)) + { + // If we were unable to re-acquire credentials, throw a new exception indicating that we tried to handle this, but were unable to. + throw new Exception($"Failed to query the feed for upgrade packages due to: {ex.Message}, and was not able to re-acquire new credentials due to: {error}", ex); + } + + // Now that we have re-acquired credentials, try again - but with the retry flag set to false. + return this.QueryFeed(firstAttempt: false); + } + catch (Exception ex) + { + EventMetadata data = CreateEventMetadata(ex); + string message = $"Error encountered when querying NuGet feed. Is first attempt: {firstAttempt}."; + this.tracer.RelatedWarning(data, message); + throw new Exception($"Failed to query the NuGet package feed due to error: {ex.Message}", ex); + } + } + + private bool IsAuthRelatedException(Exception ex) + { + // In observation, we have seen either an HttpRequestException directly, or + // a FatalProtocolException wrapping an HttpRequestException when we are not able + // to auth against the NuGet feed. + System.Net.Http.HttpRequestException httpRequestException = null; + if (ex is System.Net.Http.HttpRequestException) + { + httpRequestException = ex as System.Net.Http.HttpRequestException; + } + else if (ex is FatalProtocolException && + ex.InnerException is System.Net.Http.HttpRequestException) + { + httpRequestException = ex.InnerException as System.Net.Http.HttpRequestException; + } + + if (httpRequestException != null && + (httpRequestException.Message.Contains("401") || httpRequestException.Message.Contains("403"))) + { + return true; + } + + return false; + } + + private bool TryRefreshCredentials(out string error) + { + try + { + string authUrl; + if (!AzDevOpsOrgFromNuGetFeed.TryCreateCredentialQueryUrl(this.nuGetUpgraderConfig.FeedUrl, out authUrl, out error)) + { + return false; + } + + if (!this.TryReacquirePersonalAccessToken(authUrl, this.tracer, out string token, out error)) + { + return false; + } + + this.nuGetFeed.SetCredentials(token); + return true; + } + catch (Exception ex) + { + error = ex.Message; + this.TraceException(ex, nameof(this.TryRefreshCredentials), "Failed to refresh credentials."); + return false; + } + } + + private bool EnsureNuGetFeedInitialized(out string error) + { + if (!this.isNuGetFeedInitialized) + { + if (this.credentialStore == null) + { + throw new InvalidOperationException("Attempted to call method that requires authentication but no CredentialStore is configured."); + } + + string authUrl; + if (!AzDevOpsOrgFromNuGetFeed.TryCreateCredentialQueryUrl(this.nuGetUpgraderConfig.FeedUrl, out authUrl, out error)) + { + return false; + } + + if (!this.TryGetPersonalAccessToken(authUrl, this.tracer, out string token, out error)) + { + return false; + } + + this.nuGetFeed.SetCredentials(token); + this.isNuGetFeedInitialized = true; + } + + error = null; + return true; + } + + public class NuGetUpgraderConfig + { + protected readonly ITracer tracer; + protected readonly LocalScalarConfig localConfig; + + public NuGetUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) + { + this.tracer = tracer; + this.localConfig = localScalarConfig; + } + + public NuGetUpgraderConfig( + ITracer tracer, + LocalScalarConfig localScalarConfig, + string feedUrl, + string packageFeedName) + : this(tracer, localScalarConfig) + { + this.FeedUrl = feedUrl; + this.PackageFeedName = packageFeedName; + } + + public string FeedUrl { get; private set; } + public string PackageFeedName { get; private set; } + public string CertificateFingerprint { get; private set; } + + /// + /// Check if the NuGetUpgrader is ready for use. A + /// NuGetUpgrader is considered ready if all required + /// config settings are present. + /// + public virtual bool IsReady(out string error) + { + if (string.IsNullOrEmpty(this.FeedUrl) || + string.IsNullOrEmpty(this.PackageFeedName)) + { + error = string.Join( + Environment.NewLine, + "One or more required settings for NuGetUpgrader are missing.", + $"Use `scalar config [{ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName}] ` to set the config."); + return false; + } + + error = null; + return true; + } + + /// + /// Check if the NuGetUpgrader is configured. + /// + public virtual bool IsConfigured(out string error) + { + if (string.IsNullOrEmpty(this.FeedUrl) && + string.IsNullOrEmpty(this.PackageFeedName)) + { + error = string.Join( + Environment.NewLine, + "NuGet upgrade server is not configured.", + $"Use `scalar config [ {ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName}] ` to set the config."); + return false; + } + + error = null; + return true; + } + + /// + /// Try to load the config for a NuGet upgrader. Returns false if there was an error reading the config. + /// + public virtual bool TryLoad(out string error) + { + string configValue; + if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl, out configValue, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.FeedUrl = configValue; + + if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName, out configValue, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.PackageFeedName = configValue; + return true; + } + } + } +} diff --git a/Scalar.Common/NuGetUpgrade/OrgNuGetUpgrader.cs b/Scalar.Common/NuGetUpgrade/OrgNuGetUpgrader.cs index 1988fc5122..64841bab70 100644 --- a/Scalar.Common/NuGetUpgrade/OrgNuGetUpgrader.cs +++ b/Scalar.Common/NuGetUpgrade/OrgNuGetUpgrader.cs @@ -1,244 +1,244 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Net.Http; -using System.Runtime.Serialization; -using System.Threading.Tasks; - -namespace Scalar.Common.NuGetUpgrade -{ - public class OrgNuGetUpgrader : NuGetUpgrader - { - private HttpClient httpClient; - private string platform; - - public OrgNuGetUpgrader( - string currentVersion, - ITracer tracer, - PhysicalFileSystem fileSystem, - HttpClient httpClient, - bool dryRun, - bool noVerify, - OrgNuGetUpgraderConfig config, - string downloadFolder, - string platform, - ICredentialStore credentialStore) - : base( - currentVersion, - tracer, - fileSystem, - dryRun, - noVerify, - config, - downloadFolder, - credentialStore) - { - this.httpClient = httpClient; - this.platform = platform; - } - - public OrgNuGetUpgrader( - string currentVersion, - ITracer tracer, - PhysicalFileSystem fileSystem, - HttpClient httpClient, - bool dryRun, - bool noVerify, - OrgNuGetUpgraderConfig config, - string platform, - NuGetFeed nuGetFeed, - ICredentialStore credentialStore) - : base( - currentVersion, - tracer, - dryRun, - noVerify, - fileSystem, - config, - nuGetFeed, - credentialStore, - ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) - { - this.httpClient = httpClient; - this.platform = platform; - } - - public override bool SupportsAnonymousVersionQuery { get => true; } - - private OrgNuGetUpgraderConfig Config { get => this.nuGetUpgraderConfig as OrgNuGetUpgraderConfig; } - private string OrgInfoServerUrl { get => this.Config.OrgInfoServer; } - private string Ring { get => this.Config.UpgradeRing; } - - public static bool TryCreate( - ITracer tracer, - PhysicalFileSystem fileSystem, - LocalScalarConfig scalarConfig, - HttpClient httpClient, - ICredentialStore credentialStore, - bool dryRun, - bool noVerify, - out OrgNuGetUpgrader upgrader, - out string error) - { - OrgNuGetUpgraderConfig upgraderConfig = new OrgNuGetUpgraderConfig(tracer, scalarConfig); - upgrader = null; - - if (!upgraderConfig.TryLoad(out error)) - { - upgrader = null; - return false; - } - - if (!upgraderConfig.IsConfigured(out error)) - { - return false; - } - - if (!upgraderConfig.IsReady(out error)) - { - return false; - } - - string platform = ScalarPlatform.Instance.Name; - - upgrader = new OrgNuGetUpgrader( - ProcessHelper.GetCurrentProcessVersion(), - tracer, - fileSystem, - httpClient, - dryRun, - noVerify, - upgraderConfig, - ProductUpgraderInfo.GetAssetDownloadsPath(), - platform, - credentialStore); - - return true; - } - - public override bool TryQueryNewestVersion(out Version newVersion, out string message) - { - newVersion = null; - - if (!AzDevOpsOrgFromNuGetFeed.TryParseOrg(this.Config.FeedUrl, out string orgName)) - { - message = "OrgNuGetUpgrader is not able to parse org name from NuGet Package Feed URL"; - return false; - } - - OrgInfoApiClient infoServer = new OrgInfoApiClient(this.httpClient, this.OrgInfoServerUrl); - - try - { - this.highestVersionAvailable = infoServer.QueryNewestVersion(orgName, this.platform, this.Ring); - } - catch (Exception exception) when (exception is HttpRequestException || - exception is TaskCanceledException) - { - // GetStringAsync can also throw a TaskCanceledException to indicate a timeout - // https://github.com/dotnet/corefx/issues/20296 - message = string.Format("Network error: could not connect to server ({0}). {1}", this.OrgInfoServerUrl, exception.Message); - this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error connecting to server."); - - return false; - } - catch (SerializationException exception) - { - message = string.Format("Parse error: could not parse response from server({0}). {1}", this.OrgInfoServerUrl, exception.Message); - this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); - - return false; - } - catch (Exception exception) when (exception is ArgumentException || - exception is FormatException || - exception is OverflowException) - { - message = string.Format("Unexpected response from server: could nor parse version({0}). {1}", this.OrgInfoServerUrl, exception.Message); - this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); - - return false; - } - - if (this.highestVersionAvailable != null && - this.highestVersionAvailable > this.installedVersion) - { - newVersion = this.highestVersionAvailable; - } - - if (newVersion != null) - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); - message = $"New version {newVersion} is available."; - return true; - } - else if (this.highestVersionAvailable != null) - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); - message = $"Highest version available is {this.highestVersionAvailable}, you are up-to-date"; - return true; - } - else - { - this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed."); - message = "No versions available via endpoint."; - return true; - } - } - - public class OrgNuGetUpgraderConfig : NuGetUpgraderConfig - { - public OrgNuGetUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) - : base(tracer, localScalarConfig) - { - } - - public string OrgInfoServer { get; set; } - - public string UpgradeRing { get; set; } - - public override bool TryLoad(out string error) - { - if (!base.TryLoad(out error)) - { - return false; - } - - if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl, out string orgInfoServerUrl, out error)) - { - this.tracer.RelatedError(error); - return false; - } - - this.OrgInfoServer = orgInfoServerUrl; - - if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out string upgradeRing, out error)) - { - this.tracer.RelatedError(error); - return false; - } - - this.UpgradeRing = upgradeRing; - - return true; - } - - public override bool IsReady(out string error) - { - if (!base.IsReady(out error) || - string.IsNullOrEmpty(this.UpgradeRing) || - string.IsNullOrEmpty(this.OrgInfoServer)) - { - error = string.Join( - Environment.NewLine, - "One or more required settings for OrgNuGetUpgrader are missing.", - "Use `scalar config [{ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName} | {ScalarConstants.LocalScalarConfig.UpgradeRing} | {ScalarConstants.LocalScalarConfig.OrgInfoServerUrl}] ` to set the config."); - return false; - } - - return true; - } - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Net.Http; +using System.Runtime.Serialization; +using System.Threading.Tasks; + +namespace Scalar.Common.NuGetUpgrade +{ + public class OrgNuGetUpgrader : NuGetUpgrader + { + private HttpClient httpClient; + private string platform; + + public OrgNuGetUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + HttpClient httpClient, + bool dryRun, + bool noVerify, + OrgNuGetUpgraderConfig config, + string downloadFolder, + string platform, + ICredentialStore credentialStore) + : base( + currentVersion, + tracer, + fileSystem, + dryRun, + noVerify, + config, + downloadFolder, + credentialStore) + { + this.httpClient = httpClient; + this.platform = platform; + } + + public OrgNuGetUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + HttpClient httpClient, + bool dryRun, + bool noVerify, + OrgNuGetUpgraderConfig config, + string platform, + NuGetFeed nuGetFeed, + ICredentialStore credentialStore) + : base( + currentVersion, + tracer, + dryRun, + noVerify, + fileSystem, + config, + nuGetFeed, + credentialStore, + ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) + { + this.httpClient = httpClient; + this.platform = platform; + } + + public override bool SupportsAnonymousVersionQuery { get => true; } + + private OrgNuGetUpgraderConfig Config { get => this.nuGetUpgraderConfig as OrgNuGetUpgraderConfig; } + private string OrgInfoServerUrl { get => this.Config.OrgInfoServer; } + private string Ring { get => this.Config.UpgradeRing; } + + public static bool TryCreate( + ITracer tracer, + PhysicalFileSystem fileSystem, + LocalScalarConfig scalarConfig, + HttpClient httpClient, + ICredentialStore credentialStore, + bool dryRun, + bool noVerify, + out OrgNuGetUpgrader upgrader, + out string error) + { + OrgNuGetUpgraderConfig upgraderConfig = new OrgNuGetUpgraderConfig(tracer, scalarConfig); + upgrader = null; + + if (!upgraderConfig.TryLoad(out error)) + { + upgrader = null; + return false; + } + + if (!upgraderConfig.IsConfigured(out error)) + { + return false; + } + + if (!upgraderConfig.IsReady(out error)) + { + return false; + } + + string platform = ScalarPlatform.Instance.Name; + + upgrader = new OrgNuGetUpgrader( + ProcessHelper.GetCurrentProcessVersion(), + tracer, + fileSystem, + httpClient, + dryRun, + noVerify, + upgraderConfig, + ProductUpgraderInfo.GetAssetDownloadsPath(), + platform, + credentialStore); + + return true; + } + + public override bool TryQueryNewestVersion(out Version newVersion, out string message) + { + newVersion = null; + + if (!AzDevOpsOrgFromNuGetFeed.TryParseOrg(this.Config.FeedUrl, out string orgName)) + { + message = "OrgNuGetUpgrader is not able to parse org name from NuGet Package Feed URL"; + return false; + } + + OrgInfoApiClient infoServer = new OrgInfoApiClient(this.httpClient, this.OrgInfoServerUrl); + + try + { + this.highestVersionAvailable = infoServer.QueryNewestVersion(orgName, this.platform, this.Ring); + } + catch (Exception exception) when (exception is HttpRequestException || + exception is TaskCanceledException) + { + // GetStringAsync can also throw a TaskCanceledException to indicate a timeout + // https://github.com/dotnet/corefx/issues/20296 + message = string.Format("Network error: could not connect to server ({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error connecting to server."); + + return false; + } + catch (SerializationException exception) + { + message = string.Format("Parse error: could not parse response from server({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); + + return false; + } + catch (Exception exception) when (exception is ArgumentException || + exception is FormatException || + exception is OverflowException) + { + message = string.Format("Unexpected response from server: could nor parse version({0}). {1}", this.OrgInfoServerUrl, exception.Message); + this.TraceException(exception, nameof(this.TryQueryNewestVersion), "Error parsing response from server."); + + return false; + } + + if (this.highestVersionAvailable != null && + this.highestVersionAvailable > this.installedVersion) + { + newVersion = this.highestVersionAvailable; + } + + if (newVersion != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - new version available: installedVersion: {this.installedVersion}, highestVersionAvailable: {newVersion}"); + message = $"New version {newVersion} is available."; + return true; + } + else if (this.highestVersionAvailable != null) + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - up-to-date"); + message = $"Highest version available is {this.highestVersionAvailable}, you are up-to-date"; + return true; + } + else + { + this.tracer.RelatedInfo($"{nameof(this.TryQueryNewestVersion)} - no versions available from feed."); + message = "No versions available via endpoint."; + return true; + } + } + + public class OrgNuGetUpgraderConfig : NuGetUpgraderConfig + { + public OrgNuGetUpgraderConfig(ITracer tracer, LocalScalarConfig localScalarConfig) + : base(tracer, localScalarConfig) + { + } + + public string OrgInfoServer { get; set; } + + public string UpgradeRing { get; set; } + + public override bool TryLoad(out string error) + { + if (!base.TryLoad(out error)) + { + return false; + } + + if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl, out string orgInfoServerUrl, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.OrgInfoServer = orgInfoServerUrl; + + if (!this.localConfig.TryGetConfig(ScalarConstants.LocalScalarConfig.UpgradeRing, out string upgradeRing, out error)) + { + this.tracer.RelatedError(error); + return false; + } + + this.UpgradeRing = upgradeRing; + + return true; + } + + public override bool IsReady(out string error) + { + if (!base.IsReady(out error) || + string.IsNullOrEmpty(this.UpgradeRing) || + string.IsNullOrEmpty(this.OrgInfoServer)) + { + error = string.Join( + Environment.NewLine, + "One or more required settings for OrgNuGetUpgrader are missing.", + "Use `scalar config [{ScalarConstants.LocalScalarConfig.UpgradeFeedUrl} | {ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName} | {ScalarConstants.LocalScalarConfig.UpgradeRing} | {ScalarConstants.LocalScalarConfig.OrgInfoServerUrl}] ` to set the config."); + return false; + } + + return true; + } + } + } +} diff --git a/Scalar.Common/OrgInfoApiClient.cs b/Scalar.Common/OrgInfoApiClient.cs index 822dd17e6f..287cf9163f 100644 --- a/Scalar.Common/OrgInfoApiClient.cs +++ b/Scalar.Common/OrgInfoApiClient.cs @@ -1,78 +1,78 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Web; - -namespace Scalar.Common -{ - /// - /// Class that handles communication with a server that contains version information. - /// - public class OrgInfoApiClient - { - private const string VersionApi = "/api/GetLatestVersion"; - - private HttpClient client; - private string baseUrl; - - public OrgInfoApiClient(HttpClient client, string baseUrl) - { - this.client = client; - this.baseUrl = baseUrl; - } - - private string VersionUrl - { - get - { - return this.baseUrl + VersionApi; - } - } - - public Version QueryNewestVersion(string orgName, string platform, string ring) - { - Dictionary queryParams = new Dictionary() - { - { "Organization", orgName }, - { "Platform", platform }, - { "Ring", ring }, - }; - - string responseString = this.client.GetStringAsync(this.ConstructRequest(this.VersionUrl, queryParams)).GetAwaiter().GetResult(); - VersionResponse versionResponse = VersionResponse.FromJsonString(responseString); - - if (string.IsNullOrEmpty(versionResponse.Version)) - { - return null; - } - - return new Version(versionResponse.Version); - } - - private string ConstructRequest(string baseUrl, Dictionary queryParams) - { - StringBuilder sb = new StringBuilder(baseUrl); - - if (queryParams.Any()) - { - sb.Append("?"); - } - - bool isFirst = true; - foreach (KeyValuePair kvp in queryParams) - { - if (!isFirst) - { - sb.Append("&"); - } - - isFirst = false; - sb.Append($"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}"); - } - - return sb.ToString(); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Web; + +namespace Scalar.Common +{ + /// + /// Class that handles communication with a server that contains version information. + /// + public class OrgInfoApiClient + { + private const string VersionApi = "/api/GetLatestVersion"; + + private HttpClient client; + private string baseUrl; + + public OrgInfoApiClient(HttpClient client, string baseUrl) + { + this.client = client; + this.baseUrl = baseUrl; + } + + private string VersionUrl + { + get + { + return this.baseUrl + VersionApi; + } + } + + public Version QueryNewestVersion(string orgName, string platform, string ring) + { + Dictionary queryParams = new Dictionary() + { + { "Organization", orgName }, + { "Platform", platform }, + { "Ring", ring }, + }; + + string responseString = this.client.GetStringAsync(this.ConstructRequest(this.VersionUrl, queryParams)).GetAwaiter().GetResult(); + VersionResponse versionResponse = VersionResponse.FromJsonString(responseString); + + if (string.IsNullOrEmpty(versionResponse.Version)) + { + return null; + } + + return new Version(versionResponse.Version); + } + + private string ConstructRequest(string baseUrl, Dictionary queryParams) + { + StringBuilder sb = new StringBuilder(baseUrl); + + if (queryParams.Any()) + { + sb.Append("?"); + } + + bool isFirst = true; + foreach (KeyValuePair kvp in queryParams) + { + if (!isFirst) + { + sb.Append("&"); + } + + isFirst = false; + sb.Append($"{HttpUtility.UrlEncode(kvp.Key)}={HttpUtility.UrlEncode(kvp.Value)}"); + } + + return sb.ToString(); + } + } +} diff --git a/Scalar.Common/Paths.Shared.cs b/Scalar.Common/Paths.Shared.cs index 360f0aa1d7..86c32eca94 100644 --- a/Scalar.Common/Paths.Shared.cs +++ b/Scalar.Common/Paths.Shared.cs @@ -1,59 +1,59 @@ -using System; -using System.IO; -using System.Linq; - -namespace Scalar.Common -{ - public static class Paths - { - public static string GetGitEnlistmentRoot(string directory) - { - return GetRoot(directory, ScalarConstants.DotGit.Root); - } - - public static string GetRoot(string startingDirectory, string rootName) - { - startingDirectory = startingDirectory.TrimEnd(Path.DirectorySeparatorChar); - DirectoryInfo dirInfo; - - try - { - dirInfo = new DirectoryInfo(startingDirectory); - } - catch (Exception) - { - return null; - } - - while (dirInfo != null) - { - if (dirInfo.Exists) - { - DirectoryInfo[] dotScalarDirs = new DirectoryInfo[0]; - - try - { - dotScalarDirs = dirInfo.GetDirectories(rootName); - } - catch (IOException) - { - } - - if (dotScalarDirs.Count() == 1) - { - return dirInfo.FullName; - } - } - - dirInfo = dirInfo.Parent; - } - - return null; - } - - public static string ConvertPathToGitFormat(string path) - { - return path.Replace(Path.DirectorySeparatorChar, ScalarConstants.GitPathSeparator); - } - } -} +using System; +using System.IO; +using System.Linq; + +namespace Scalar.Common +{ + public static class Paths + { + public static string GetGitEnlistmentRoot(string directory) + { + return GetRoot(directory, ScalarConstants.DotGit.Root); + } + + public static string GetRoot(string startingDirectory, string rootName) + { + startingDirectory = startingDirectory.TrimEnd(Path.DirectorySeparatorChar); + DirectoryInfo dirInfo; + + try + { + dirInfo = new DirectoryInfo(startingDirectory); + } + catch (Exception) + { + return null; + } + + while (dirInfo != null) + { + if (dirInfo.Exists) + { + DirectoryInfo[] dotScalarDirs = new DirectoryInfo[0]; + + try + { + dotScalarDirs = dirInfo.GetDirectories(rootName); + } + catch (IOException) + { + } + + if (dotScalarDirs.Count() == 1) + { + return dirInfo.FullName; + } + } + + dirInfo = dirInfo.Parent; + } + + return null; + } + + public static string ConvertPathToGitFormat(string path) + { + return path.Replace(Path.DirectorySeparatorChar, ScalarConstants.GitPathSeparator); + } + } +} diff --git a/Scalar.Common/Prefetch/BlobPrefetcher.cs b/Scalar.Common/Prefetch/BlobPrefetcher.cs index c8c62ce1ee..0533740aee 100644 --- a/Scalar.Common/Prefetch/BlobPrefetcher.cs +++ b/Scalar.Common/Prefetch/BlobPrefetcher.cs @@ -1,91 +1,91 @@ -using Newtonsoft.Json; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Prefetch.Git; -using Scalar.Common.Prefetch.Pipeline; -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.Common.Prefetch -{ - public class BlobPrefetcher - { - protected const string RefsHeadsGitPath = "refs/heads/"; - - protected readonly Enlistment Enlistment; - protected readonly GitObjectsHttpRequestor ObjectRequestor; - protected readonly GitObjects GitObjects; - protected readonly ITracer Tracer; - - protected readonly int ChunkSize; - protected readonly int SearchThreadCount; - protected readonly int DownloadThreadCount; - protected readonly int IndexThreadCount; - - protected readonly bool SkipConfigUpdate; - - private const string AreaPath = nameof(BlobPrefetcher); - private static string pathSeparatorString = Path.DirectorySeparatorChar.ToString(); - - private FileBasedDictionary lastPrefetchArgs; - - public BlobPrefetcher( - ITracer tracer, - Enlistment enlistment, - GitObjectsHttpRequestor objectRequestor, - int chunkSize, - int searchThreadCount, - int downloadThreadCount, - int indexThreadCount) - : this(tracer, enlistment, objectRequestor, null, null, null, chunkSize, searchThreadCount, downloadThreadCount, indexThreadCount) - { - } - - public BlobPrefetcher( - ITracer tracer, - Enlistment enlistment, - GitObjectsHttpRequestor objectRequestor, - List fileList, - List folderList, - FileBasedDictionary lastPrefetchArgs, - int chunkSize, - int searchThreadCount, - int downloadThreadCount, - int indexThreadCount) - { - this.SearchThreadCount = searchThreadCount; - this.DownloadThreadCount = downloadThreadCount; - this.IndexThreadCount = indexThreadCount; - this.ChunkSize = chunkSize; - this.Tracer = tracer; - this.Enlistment = enlistment; - this.ObjectRequestor = objectRequestor; - - this.GitObjects = new PrefetchGitObjects(tracer, enlistment, this.ObjectRequestor); - this.FileList = fileList ?? new List(); - this.FolderList = folderList ?? new List(); - - this.lastPrefetchArgs = lastPrefetchArgs; - - // We never want to update config settings for a ScalarEnlistment - this.SkipConfigUpdate = enlistment is ScalarEnlistment; - } - - public bool HasFailures { get; protected set; } - - public List FileList { get; } - - public List FolderList { get; } - - public static bool TryLoadFolderList(Enlistment enlistment, string foldersInput, string folderListFile, List folderListOutput, bool readListFromStdIn, out string error) - { - return TryLoadFileOrFolderList( +using Newtonsoft.Json; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Prefetch.Git; +using Scalar.Common.Prefetch.Pipeline; +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.Common.Prefetch +{ + public class BlobPrefetcher + { + protected const string RefsHeadsGitPath = "refs/heads/"; + + protected readonly Enlistment Enlistment; + protected readonly GitObjectsHttpRequestor ObjectRequestor; + protected readonly GitObjects GitObjects; + protected readonly ITracer Tracer; + + protected readonly int ChunkSize; + protected readonly int SearchThreadCount; + protected readonly int DownloadThreadCount; + protected readonly int IndexThreadCount; + + protected readonly bool SkipConfigUpdate; + + private const string AreaPath = nameof(BlobPrefetcher); + private static string pathSeparatorString = Path.DirectorySeparatorChar.ToString(); + + private FileBasedDictionary lastPrefetchArgs; + + public BlobPrefetcher( + ITracer tracer, + Enlistment enlistment, + GitObjectsHttpRequestor objectRequestor, + int chunkSize, + int searchThreadCount, + int downloadThreadCount, + int indexThreadCount) + : this(tracer, enlistment, objectRequestor, null, null, null, chunkSize, searchThreadCount, downloadThreadCount, indexThreadCount) + { + } + + public BlobPrefetcher( + ITracer tracer, + Enlistment enlistment, + GitObjectsHttpRequestor objectRequestor, + List fileList, + List folderList, + FileBasedDictionary lastPrefetchArgs, + int chunkSize, + int searchThreadCount, + int downloadThreadCount, + int indexThreadCount) + { + this.SearchThreadCount = searchThreadCount; + this.DownloadThreadCount = downloadThreadCount; + this.IndexThreadCount = indexThreadCount; + this.ChunkSize = chunkSize; + this.Tracer = tracer; + this.Enlistment = enlistment; + this.ObjectRequestor = objectRequestor; + + this.GitObjects = new PrefetchGitObjects(tracer, enlistment, this.ObjectRequestor); + this.FileList = fileList ?? new List(); + this.FolderList = folderList ?? new List(); + + this.lastPrefetchArgs = lastPrefetchArgs; + + // We never want to update config settings for a ScalarEnlistment + this.SkipConfigUpdate = enlistment is ScalarEnlistment; + } + + public bool HasFailures { get; protected set; } + + public List FileList { get; } + + public List FolderList { get; } + + public static bool TryLoadFolderList(Enlistment enlistment, string foldersInput, string folderListFile, List folderListOutput, bool readListFromStdIn, out string error) + { + return TryLoadFileOrFolderList( enlistment, foldersInput, folderListFile, @@ -96,518 +96,518 @@ public static bool TryLoadFolderList(Enlistment enlistment, string foldersInput, s.Contains("*") ? "Wildcards are not supported for folders. Invalid entry: " + s : null, - error: out error); - } - + error: out error); + } + public static bool TryLoadFileList(Enlistment enlistment, string filesInput, string filesListFile, List fileListOutput, bool readListFromStdIn, out string error) - { - return TryLoadFileOrFolderList( - enlistment, - filesInput, - filesListFile, - readListFromStdIn: readListFromStdIn, - isFolder: false, - output: fileListOutput, + { + return TryLoadFileOrFolderList( + enlistment, + filesInput, + filesListFile, + readListFromStdIn: readListFromStdIn, + isFolder: false, + output: fileListOutput, elementValidationFunction: s => - { - if (s.IndexOf('*', 1) != -1) - { - return "Only prefix wildcards are supported. Invalid entry: " + s; - } - - if (s.EndsWith(ScalarConstants.GitPathSeparatorString) || - s.EndsWith(pathSeparatorString)) + { + if (s.IndexOf('*', 1) != -1) { - return "Folders are not allowed in the file list. Invalid entry: " + s; - } - - return null; - }, - error: out error); - } - - public static bool IsNoopPrefetch( - ITracer tracer, - FileBasedDictionary lastPrefetchArgs, - string commitId, - List files, - List folders, - bool hydrateFilesAfterDownload) - { - if (lastPrefetchArgs != null && - lastPrefetchArgs.TryGetValue(PrefetchArgs.CommitId, out string lastCommitId) && - lastPrefetchArgs.TryGetValue(PrefetchArgs.Files, out string lastFilesString) && - lastPrefetchArgs.TryGetValue(PrefetchArgs.Folders, out string lastFoldersString) && - lastPrefetchArgs.TryGetValue(PrefetchArgs.Hydrate, out string lastHydrateString)) - { - string newFilesString = JsonConvert.SerializeObject(files); - string newFoldersString = JsonConvert.SerializeObject(folders); - bool isNoop = - commitId == lastCommitId && - hydrateFilesAfterDownload.ToString() == lastHydrateString && - newFilesString == lastFilesString && - newFoldersString == lastFoldersString; - - tracer.RelatedEvent( - EventLevel.Informational, - "BlobPrefetcher.IsNoopPrefetch", - new EventMetadata - { - { "Last" + PrefetchArgs.CommitId, lastCommitId }, - { "Last" + PrefetchArgs.Files, lastFilesString }, - { "Last" + PrefetchArgs.Folders, lastFoldersString }, - { "Last" + PrefetchArgs.Hydrate, lastHydrateString }, - { "New" + PrefetchArgs.CommitId, commitId }, - { "New" + PrefetchArgs.Files, newFilesString }, - { "New" + PrefetchArgs.Folders, newFoldersString }, - { "New" + PrefetchArgs.Hydrate, hydrateFilesAfterDownload.ToString() }, - { "Result", isNoop }, - }); - - return isNoop; - } - - return false; - } - - public static void AppendToNewlineSeparatedFile(string filename, string newContent) - { - AppendToNewlineSeparatedFile(new PhysicalFileSystem(), filename, newContent); - } - - public static void AppendToNewlineSeparatedFile(PhysicalFileSystem fileSystem, string filename, string newContent) - { - using (Stream fileStream = fileSystem.OpenFileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, false)) - { - using (StreamReader reader = new StreamReader(fileStream)) - using (StreamWriter writer = new StreamWriter(fileStream)) - { - long position = reader.BaseStream.Seek(0, SeekOrigin.End); - if (position > 0) - { - reader.BaseStream.Seek(position - 1, SeekOrigin.Begin); - } - - string lastCharacter = reader.ReadToEnd(); - if (lastCharacter != "\n" && lastCharacter != string.Empty) - { - writer.Write("\n"); - } - - writer.Write(newContent.Trim()); - writer.Write("\n"); - } - - fileStream.Close(); - } - } - - /// A specific branch to filter for, or null for all branches returned from info/refs - public virtual void Prefetch(string branchOrCommit, bool isBranch) - { - int matchedBlobCount; - int downloadedBlobCount; - int hydratedFileCount; - - this.PrefetchWithStats(branchOrCommit, isBranch, false, out matchedBlobCount, out downloadedBlobCount, out hydratedFileCount); - } - - public void PrefetchWithStats( - string branchOrCommit, - bool isBranch, - bool hydrateFilesAfterDownload, - out int matchedBlobCount, - out int downloadedBlobCount, - out int hydratedFileCount) - { - matchedBlobCount = 0; - downloadedBlobCount = 0; - hydratedFileCount = 0; - - if (string.IsNullOrWhiteSpace(branchOrCommit)) - { - throw new FetchException("Must specify branch or commit to fetch"); - } - - GitRefs refs = null; - string commitToFetch; - if (isBranch) - { - refs = this.ObjectRequestor.QueryInfoRefs(branchOrCommit); - if (refs == null) - { - throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl); - } - else if (refs.Count == 0) - { - throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl); - } - - commitToFetch = refs.GetTipCommitId(branchOrCommit); - } - else - { - commitToFetch = branchOrCommit; - } - - this.DownloadMissingCommit(commitToFetch, this.GitObjects); - - // For FastFetch only, examine the shallow file to determine the previous commit that had been fetched - string shallowFile = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Shallow); - string previousCommit = null; - - // Use the shallow file to find a recent commit to diff against to try and reduce the number of SHAs to check. - if (File.Exists(shallowFile)) - { - previousCommit = File.ReadAllLines(shallowFile).Where(line => !string.IsNullOrWhiteSpace(line)).LastOrDefault(); - if (string.IsNullOrWhiteSpace(previousCommit)) - { - this.Tracer.RelatedError("Shallow file exists, but contains no valid SHAs."); - this.HasFailures = true; - return; - } - } - - BlockingCollection availableBlobs = new BlockingCollection(); - - //// - // First create the pipeline - // - // diff ---> blobFinder ---> downloader ---> packIndexer - // | | | | - // ------------------------------------------------------> fileHydrator - //// - - // diff - // Inputs: - // * files/folders - // * commit id - // Outputs: - // * RequiredBlobs (property): Blob ids required to satisfy desired paths - // * FileAddOperations (property): Repo-relative paths corresponding to those blob ids - DiffHelper diff = new DiffHelper(this.Tracer, this.Enlistment, this.FileList, this.FolderList, includeSymLinks: false); - - // blobFinder - // Inputs: - // * requiredBlobs (in param): Blob ids from output of `diff` - // Outputs: - // * availableBlobs (out param): Locally available blob ids (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) - // * MissingBlobs (property): Blob ids that are missing and need to be downloaded - // * AvailableBlobs (property): Same as availableBlobs - FindBlobsStage blobFinder = new FindBlobsStage(this.SearchThreadCount, diff.RequiredBlobs, availableBlobs, this.Tracer, this.Enlistment); - - // downloader - // Inputs: - // * missingBlobs (in param): Blob ids from output of `blobFinder` - // Outputs: - // * availableBlobs (out param): Loose objects that have completed downloading (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) - // * AvailableObjects (property): Same as availableBlobs - // * AvailablePacks (property): Packfiles that have completed downloading - BatchObjectDownloadStage downloader = new BatchObjectDownloadStage(this.DownloadThreadCount, this.ChunkSize, blobFinder.MissingBlobs, availableBlobs, this.Tracer, this.Enlistment, this.ObjectRequestor, this.GitObjects); - - // packIndexer - // Inputs: - // * availablePacks (in param): Packfiles that have completed downloading from output of `downloader` - // Outputs: - // * availableBlobs (out param): Blobs that have completed downloading and indexing (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) - IndexPackStage packIndexer = new IndexPackStage(this.IndexThreadCount, downloader.AvailablePacks, availableBlobs, this.Tracer, this.GitObjects); - - // fileHydrator - // Inputs: - // * workingDirectoryRoot (in param): the root of the working directory where hydration takes place - // * blobIdsToPaths (in param): paths of all blob ids that need to be hydrated from output of `diff` - // * availableBlobs (in param): blobs id that are available locally, from whatever source - // Outputs: - // * Hydrated files on disk. - HydrateFilesStage fileHydrator = new HydrateFilesStage(Environment.ProcessorCount * 2, this.Enlistment.WorkingDirectoryRoot, diff.FileAddOperations, availableBlobs, this.Tracer); - - // All the stages of the pipeline are created and wired up, now kick them off in the proper sequence - - ThreadStart performDiff = () => - { - diff.PerformDiff(previousCommit, commitToFetch); - this.HasFailures |= diff.HasFailures; - }; - - if (hydrateFilesAfterDownload) - { - // Call synchronously to ensure that diff.FileAddOperations - // is completely populated when fileHydrator starts - performDiff(); - } - else - { - new Thread(performDiff).Start(); - } - - blobFinder.Start(); - downloader.Start(); - - if (hydrateFilesAfterDownload) - { - fileHydrator.Start(); - } - - // If indexing happens during searching, searching progressively gets slower, so wait on searching before indexing. - blobFinder.WaitForCompletion(); - this.HasFailures |= blobFinder.HasFailures; - - packIndexer.Start(); - - downloader.WaitForCompletion(); - this.HasFailures |= downloader.HasFailures; - - packIndexer.WaitForCompletion(); - this.HasFailures |= packIndexer.HasFailures; - - availableBlobs.CompleteAdding(); - - if (hydrateFilesAfterDownload) - { - fileHydrator.WaitForCompletion(); - this.HasFailures |= fileHydrator.HasFailures; - } - - matchedBlobCount = blobFinder.AvailableBlobCount + blobFinder.MissingBlobCount; - downloadedBlobCount = blobFinder.MissingBlobCount; - hydratedFileCount = fileHydrator.ReadFileCount; - - if (!this.SkipConfigUpdate && !this.HasFailures) - { - this.UpdateRefs(branchOrCommit, isBranch, refs); - - if (isBranch) - { - this.HasFailures |= !this.UpdateRefSpec(this.Tracer, this.Enlistment, branchOrCommit, refs); - } - } - - if (!this.HasFailures) - { - this.SavePrefetchArgs(commitToFetch, hydrateFilesAfterDownload); - } - } - - protected bool UpdateRefSpec(ITracer tracer, Enlistment enlistment, string branchOrCommit, GitRefs refs) - { - using (ITracer activity = tracer.StartActivity("UpdateRefSpec", EventLevel.Informational, Keywords.Telemetry, metadata: null)) - { - const string OriginRefMapSettingName = "remote.origin.fetch"; - - // We must update the refspec to get proper "git pull" functionality. - string localBranch = branchOrCommit.StartsWith(RefsHeadsGitPath) ? branchOrCommit : (RefsHeadsGitPath + branchOrCommit); - string remoteBranch = refs.GetBranchRefPairs().Single().Key; - string refSpec = "+" + localBranch + ":" + remoteBranch; - - GitProcess git = new GitProcess(enlistment); - - // Replace all ref-specs this - // * ensures the default refspec (remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*) is removed which avoids some "git fetch/pull" failures - // * gives added "git fetch" performance since git will only fetch the branch provided in the refspec. - GitProcess.Result setResult = git.SetInLocalConfig(OriginRefMapSettingName, refSpec, replaceAll: true); - if (setResult.ExitCodeIsFailure) - { - activity.RelatedError("Could not update ref spec to {0}: {1}", refSpec, setResult.Errors); - return false; - } - } - - return true; - } - - /// - /// * Updates any remote branch (N/A for fetch of detached commit) - /// * Updates shallow file - /// - protected virtual void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs refs) - { - string commitSha = null; - if (isBranch) - { - KeyValuePair remoteRef = refs.GetBranchRefPairs().Single(); - string remoteBranch = remoteRef.Key; - commitSha = remoteRef.Value; - - this.HasFailures |= !this.UpdateRef(this.Tracer, remoteBranch, commitSha); - } - else - { - commitSha = branchOrCommit; - } - - // Update shallow file to ensure this is a valid shallow repo - AppendToNewlineSeparatedFile(Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Shallow), commitSha); - } - - protected bool UpdateRef(ITracer tracer, string refName, string targetCommitish) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("RefName", refName); - metadata.Add("TargetCommitish", targetCommitish); - using (ITracer activity = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) - { - GitProcess gitProcess = new GitProcess(this.Enlistment); - GitProcess.Result result = null; - if (this.IsSymbolicRef(targetCommitish)) - { - // Using update-ref with a branch name will leave a SHA in the ref file which detaches HEAD, so use symbolic-ref instead. - result = gitProcess.UpdateBranchSymbolicRef(refName, targetCommitish); - } - else - { - result = gitProcess.UpdateBranchSha(refName, targetCommitish); - } - - if (result.ExitCodeIsFailure) - { - activity.RelatedError(result.Errors); - return false; - } - - return true; - } - } - - protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects) - { - EventMetadata startMetadata = new EventMetadata(); - startMetadata.Add("CommitSha", commitSha); - - using (ITracer activity = this.Tracer.StartActivity("DownloadTrees", EventLevel.Informational, Keywords.Telemetry, startMetadata)) - { - using (LibGit2Repo repo = new LibGit2Repo(this.Tracer, this.Enlistment.WorkingDirectoryBackingRoot)) - { - if (!repo.ObjectExists(commitSha)) - { - if (!gitObjects.TryDownloadCommit(commitSha)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("ObjectsEndpointUrl", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); - activity.RelatedError(metadata, "Could not download commits"); - throw new FetchException("Could not download commits from {0}", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); - } - } - } - } - } - + return "Only prefix wildcards are supported. Invalid entry: " + s; + } + + if (s.EndsWith(ScalarConstants.GitPathSeparatorString) || + s.EndsWith(pathSeparatorString)) + { + return "Folders are not allowed in the file list. Invalid entry: " + s; + } + + return null; + }, + error: out error); + } + + public static bool IsNoopPrefetch( + ITracer tracer, + FileBasedDictionary lastPrefetchArgs, + string commitId, + List files, + List folders, + bool hydrateFilesAfterDownload) + { + if (lastPrefetchArgs != null && + lastPrefetchArgs.TryGetValue(PrefetchArgs.CommitId, out string lastCommitId) && + lastPrefetchArgs.TryGetValue(PrefetchArgs.Files, out string lastFilesString) && + lastPrefetchArgs.TryGetValue(PrefetchArgs.Folders, out string lastFoldersString) && + lastPrefetchArgs.TryGetValue(PrefetchArgs.Hydrate, out string lastHydrateString)) + { + string newFilesString = JsonConvert.SerializeObject(files); + string newFoldersString = JsonConvert.SerializeObject(folders); + bool isNoop = + commitId == lastCommitId && + hydrateFilesAfterDownload.ToString() == lastHydrateString && + newFilesString == lastFilesString && + newFoldersString == lastFoldersString; + + tracer.RelatedEvent( + EventLevel.Informational, + "BlobPrefetcher.IsNoopPrefetch", + new EventMetadata + { + { "Last" + PrefetchArgs.CommitId, lastCommitId }, + { "Last" + PrefetchArgs.Files, lastFilesString }, + { "Last" + PrefetchArgs.Folders, lastFoldersString }, + { "Last" + PrefetchArgs.Hydrate, lastHydrateString }, + { "New" + PrefetchArgs.CommitId, commitId }, + { "New" + PrefetchArgs.Files, newFilesString }, + { "New" + PrefetchArgs.Folders, newFoldersString }, + { "New" + PrefetchArgs.Hydrate, hydrateFilesAfterDownload.ToString() }, + { "Result", isNoop }, + }); + + return isNoop; + } + + return false; + } + + public static void AppendToNewlineSeparatedFile(string filename, string newContent) + { + AppendToNewlineSeparatedFile(new PhysicalFileSystem(), filename, newContent); + } + + public static void AppendToNewlineSeparatedFile(PhysicalFileSystem fileSystem, string filename, string newContent) + { + using (Stream fileStream = fileSystem.OpenFileStream(filename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite, false)) + { + using (StreamReader reader = new StreamReader(fileStream)) + using (StreamWriter writer = new StreamWriter(fileStream)) + { + long position = reader.BaseStream.Seek(0, SeekOrigin.End); + if (position > 0) + { + reader.BaseStream.Seek(position - 1, SeekOrigin.Begin); + } + + string lastCharacter = reader.ReadToEnd(); + if (lastCharacter != "\n" && lastCharacter != string.Empty) + { + writer.Write("\n"); + } + + writer.Write(newContent.Trim()); + writer.Write("\n"); + } + + fileStream.Close(); + } + } + + /// A specific branch to filter for, or null for all branches returned from info/refs + public virtual void Prefetch(string branchOrCommit, bool isBranch) + { + int matchedBlobCount; + int downloadedBlobCount; + int hydratedFileCount; + + this.PrefetchWithStats(branchOrCommit, isBranch, false, out matchedBlobCount, out downloadedBlobCount, out hydratedFileCount); + } + + public void PrefetchWithStats( + string branchOrCommit, + bool isBranch, + bool hydrateFilesAfterDownload, + out int matchedBlobCount, + out int downloadedBlobCount, + out int hydratedFileCount) + { + matchedBlobCount = 0; + downloadedBlobCount = 0; + hydratedFileCount = 0; + + if (string.IsNullOrWhiteSpace(branchOrCommit)) + { + throw new FetchException("Must specify branch or commit to fetch"); + } + + GitRefs refs = null; + string commitToFetch; + if (isBranch) + { + refs = this.ObjectRequestor.QueryInfoRefs(branchOrCommit); + if (refs == null) + { + throw new FetchException("Could not query info/refs from: {0}", this.Enlistment.RepoUrl); + } + else if (refs.Count == 0) + { + throw new FetchException("Could not find branch {0} in info/refs from: {1}", branchOrCommit, this.Enlistment.RepoUrl); + } + + commitToFetch = refs.GetTipCommitId(branchOrCommit); + } + else + { + commitToFetch = branchOrCommit; + } + + this.DownloadMissingCommit(commitToFetch, this.GitObjects); + + // For FastFetch only, examine the shallow file to determine the previous commit that had been fetched + string shallowFile = Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Shallow); + string previousCommit = null; + + // Use the shallow file to find a recent commit to diff against to try and reduce the number of SHAs to check. + if (File.Exists(shallowFile)) + { + previousCommit = File.ReadAllLines(shallowFile).Where(line => !string.IsNullOrWhiteSpace(line)).LastOrDefault(); + if (string.IsNullOrWhiteSpace(previousCommit)) + { + this.Tracer.RelatedError("Shallow file exists, but contains no valid SHAs."); + this.HasFailures = true; + return; + } + } + + BlockingCollection availableBlobs = new BlockingCollection(); + + //// + // First create the pipeline + // + // diff ---> blobFinder ---> downloader ---> packIndexer + // | | | | + // ------------------------------------------------------> fileHydrator + //// + + // diff + // Inputs: + // * files/folders + // * commit id + // Outputs: + // * RequiredBlobs (property): Blob ids required to satisfy desired paths + // * FileAddOperations (property): Repo-relative paths corresponding to those blob ids + DiffHelper diff = new DiffHelper(this.Tracer, this.Enlistment, this.FileList, this.FolderList, includeSymLinks: false); + + // blobFinder + // Inputs: + // * requiredBlobs (in param): Blob ids from output of `diff` + // Outputs: + // * availableBlobs (out param): Locally available blob ids (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) + // * MissingBlobs (property): Blob ids that are missing and need to be downloaded + // * AvailableBlobs (property): Same as availableBlobs + FindBlobsStage blobFinder = new FindBlobsStage(this.SearchThreadCount, diff.RequiredBlobs, availableBlobs, this.Tracer, this.Enlistment); + + // downloader + // Inputs: + // * missingBlobs (in param): Blob ids from output of `blobFinder` + // Outputs: + // * availableBlobs (out param): Loose objects that have completed downloading (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) + // * AvailableObjects (property): Same as availableBlobs + // * AvailablePacks (property): Packfiles that have completed downloading + BatchObjectDownloadStage downloader = new BatchObjectDownloadStage(this.DownloadThreadCount, this.ChunkSize, blobFinder.MissingBlobs, availableBlobs, this.Tracer, this.Enlistment, this.ObjectRequestor, this.GitObjects); + + // packIndexer + // Inputs: + // * availablePacks (in param): Packfiles that have completed downloading from output of `downloader` + // Outputs: + // * availableBlobs (out param): Blobs that have completed downloading and indexing (shared between `blobFinder`, `downloader`, and `packIndexer`, all add blob ids to the list as they are locally available) + IndexPackStage packIndexer = new IndexPackStage(this.IndexThreadCount, downloader.AvailablePacks, availableBlobs, this.Tracer, this.GitObjects); + + // fileHydrator + // Inputs: + // * workingDirectoryRoot (in param): the root of the working directory where hydration takes place + // * blobIdsToPaths (in param): paths of all blob ids that need to be hydrated from output of `diff` + // * availableBlobs (in param): blobs id that are available locally, from whatever source + // Outputs: + // * Hydrated files on disk. + HydrateFilesStage fileHydrator = new HydrateFilesStage(Environment.ProcessorCount * 2, this.Enlistment.WorkingDirectoryRoot, diff.FileAddOperations, availableBlobs, this.Tracer); + + // All the stages of the pipeline are created and wired up, now kick them off in the proper sequence + + ThreadStart performDiff = () => + { + diff.PerformDiff(previousCommit, commitToFetch); + this.HasFailures |= diff.HasFailures; + }; + + if (hydrateFilesAfterDownload) + { + // Call synchronously to ensure that diff.FileAddOperations + // is completely populated when fileHydrator starts + performDiff(); + } + else + { + new Thread(performDiff).Start(); + } + + blobFinder.Start(); + downloader.Start(); + + if (hydrateFilesAfterDownload) + { + fileHydrator.Start(); + } + + // If indexing happens during searching, searching progressively gets slower, so wait on searching before indexing. + blobFinder.WaitForCompletion(); + this.HasFailures |= blobFinder.HasFailures; + + packIndexer.Start(); + + downloader.WaitForCompletion(); + this.HasFailures |= downloader.HasFailures; + + packIndexer.WaitForCompletion(); + this.HasFailures |= packIndexer.HasFailures; + + availableBlobs.CompleteAdding(); + + if (hydrateFilesAfterDownload) + { + fileHydrator.WaitForCompletion(); + this.HasFailures |= fileHydrator.HasFailures; + } + + matchedBlobCount = blobFinder.AvailableBlobCount + blobFinder.MissingBlobCount; + downloadedBlobCount = blobFinder.MissingBlobCount; + hydratedFileCount = fileHydrator.ReadFileCount; + + if (!this.SkipConfigUpdate && !this.HasFailures) + { + this.UpdateRefs(branchOrCommit, isBranch, refs); + + if (isBranch) + { + this.HasFailures |= !this.UpdateRefSpec(this.Tracer, this.Enlistment, branchOrCommit, refs); + } + } + + if (!this.HasFailures) + { + this.SavePrefetchArgs(commitToFetch, hydrateFilesAfterDownload); + } + } + + protected bool UpdateRefSpec(ITracer tracer, Enlistment enlistment, string branchOrCommit, GitRefs refs) + { + using (ITracer activity = tracer.StartActivity("UpdateRefSpec", EventLevel.Informational, Keywords.Telemetry, metadata: null)) + { + const string OriginRefMapSettingName = "remote.origin.fetch"; + + // We must update the refspec to get proper "git pull" functionality. + string localBranch = branchOrCommit.StartsWith(RefsHeadsGitPath) ? branchOrCommit : (RefsHeadsGitPath + branchOrCommit); + string remoteBranch = refs.GetBranchRefPairs().Single().Key; + string refSpec = "+" + localBranch + ":" + remoteBranch; + + GitProcess git = new GitProcess(enlistment); + + // Replace all ref-specs this + // * ensures the default refspec (remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*) is removed which avoids some "git fetch/pull" failures + // * gives added "git fetch" performance since git will only fetch the branch provided in the refspec. + GitProcess.Result setResult = git.SetInLocalConfig(OriginRefMapSettingName, refSpec, replaceAll: true); + if (setResult.ExitCodeIsFailure) + { + activity.RelatedError("Could not update ref spec to {0}: {1}", refSpec, setResult.Errors); + return false; + } + } + + return true; + } + + /// + /// * Updates any remote branch (N/A for fetch of detached commit) + /// * Updates shallow file + /// + protected virtual void UpdateRefs(string branchOrCommit, bool isBranch, GitRefs refs) + { + string commitSha = null; + if (isBranch) + { + KeyValuePair remoteRef = refs.GetBranchRefPairs().Single(); + string remoteBranch = remoteRef.Key; + commitSha = remoteRef.Value; + + this.HasFailures |= !this.UpdateRef(this.Tracer, remoteBranch, commitSha); + } + else + { + commitSha = branchOrCommit; + } + + // Update shallow file to ensure this is a valid shallow repo + AppendToNewlineSeparatedFile(Path.Combine(this.Enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Shallow), commitSha); + } + + protected bool UpdateRef(ITracer tracer, string refName, string targetCommitish) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("RefName", refName); + metadata.Add("TargetCommitish", targetCommitish); + using (ITracer activity = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) + { + GitProcess gitProcess = new GitProcess(this.Enlistment); + GitProcess.Result result = null; + if (this.IsSymbolicRef(targetCommitish)) + { + // Using update-ref with a branch name will leave a SHA in the ref file which detaches HEAD, so use symbolic-ref instead. + result = gitProcess.UpdateBranchSymbolicRef(refName, targetCommitish); + } + else + { + result = gitProcess.UpdateBranchSha(refName, targetCommitish); + } + + if (result.ExitCodeIsFailure) + { + activity.RelatedError(result.Errors); + return false; + } + + return true; + } + } + + protected void DownloadMissingCommit(string commitSha, GitObjects gitObjects) + { + EventMetadata startMetadata = new EventMetadata(); + startMetadata.Add("CommitSha", commitSha); + + using (ITracer activity = this.Tracer.StartActivity("DownloadTrees", EventLevel.Informational, Keywords.Telemetry, startMetadata)) + { + using (LibGit2Repo repo = new LibGit2Repo(this.Tracer, this.Enlistment.WorkingDirectoryBackingRoot)) + { + if (!repo.ObjectExists(commitSha)) + { + if (!gitObjects.TryDownloadCommit(commitSha)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ObjectsEndpointUrl", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); + activity.RelatedError(metadata, "Could not download commits"); + throw new FetchException("Could not download commits from {0}", this.ObjectRequestor.CacheServer.ObjectsEndpointUrl); + } + } + } + } + } + private static IEnumerable GetFilesFromVerbParameter(string valueString) { return valueString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - } - + } + private static IEnumerable GetFilesFromFile(string fileName, out string error) - { - error = null; + { + error = null; if (string.IsNullOrWhiteSpace(fileName)) { return Enumerable.Empty(); - } - + } + if (!File.Exists(fileName)) { - error = string.Format("Could not find '{0}' list file.", fileName); + error = string.Format("Could not find '{0}' list file.", fileName); return Enumerable.Empty(); - } + } - return File.ReadAllLines(fileName) + return File.ReadAllLines(fileName) .Select(line => line.Trim()); - } - + } + private static IEnumerable GetFilesFromStdin(bool shouldRead) { if (!shouldRead) { yield break; - } - - string line; + } + + string line; while ((line = Console.In.ReadLine()) != null) { yield return line.Trim(); } - } - - private static bool TryLoadFileOrFolderList(Enlistment enlistment, string valueString, string listFileName, bool readListFromStdIn, bool isFolder, List output, Func elementValidationFunction, out string error) - { - output.AddRange( - GetFilesFromVerbParameter(valueString) - .Union(GetFilesFromFile(listFileName, out string fileReadError)) - .Union(GetFilesFromStdin(readListFromStdIn)) - .Where(path => !path.StartsWith(ScalarConstants.GitCommentSign.ToString())) - .Where(path => !string.IsNullOrWhiteSpace(path)) - .Select(path => BlobPrefetcher.ToFilterPath(path, isFolder: isFolder))); + } + + private static bool TryLoadFileOrFolderList(Enlistment enlistment, string valueString, string listFileName, bool readListFromStdIn, bool isFolder, List output, Func elementValidationFunction, out string error) + { + output.AddRange( + GetFilesFromVerbParameter(valueString) + .Union(GetFilesFromFile(listFileName, out string fileReadError)) + .Union(GetFilesFromStdin(readListFromStdIn)) + .Where(path => !path.StartsWith(ScalarConstants.GitCommentSign.ToString())) + .Where(path => !string.IsNullOrWhiteSpace(path)) + .Select(path => BlobPrefetcher.ToFilterPath(path, isFolder: isFolder))); if (!string.IsNullOrWhiteSpace(fileReadError)) { - error = fileReadError; + error = fileReadError; return false; - } - - string[] errorArray = output - .Select(elementValidationFunction) - .Where(s => !string.IsNullOrWhiteSpace(s)) - .ToArray(); - + } + + string[] errorArray = output + .Select(elementValidationFunction) + .Where(s => !string.IsNullOrWhiteSpace(s)) + .ToArray(); + if (errorArray != null && errorArray.Length > 0) - { + { error = string.Join("\n", errorArray); - return false; - } - - error = null; - return true; - } - - private static string ToFilterPath(string path, bool isFolder) - { - string filterPath = - path.StartsWith("*") - ? path - : path.Replace(ScalarConstants.GitPathSeparator, Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); - - if (isFolder && filterPath.Length > 0 && !filterPath.EndsWith(pathSeparatorString)) - { - filterPath += pathSeparatorString; - } - - return filterPath; - } - - private bool IsSymbolicRef(string targetCommitish) - { - return targetCommitish.StartsWith("refs/", StringComparison.OrdinalIgnoreCase); - } - - private void SavePrefetchArgs(string targetCommit, bool hydrate) - { - if (this.lastPrefetchArgs != null) - { - this.lastPrefetchArgs.SetValuesAndFlush( - new[] - { - new KeyValuePair(PrefetchArgs.CommitId, targetCommit), - new KeyValuePair(PrefetchArgs.Files, JsonConvert.SerializeObject(this.FileList)), - new KeyValuePair(PrefetchArgs.Folders, JsonConvert.SerializeObject(this.FolderList)), - new KeyValuePair(PrefetchArgs.Hydrate, hydrate.ToString()), - }); - } - } - - public class FetchException : Exception - { - public FetchException(string format, params object[] args) - : base(string.Format(format, args)) - { - } - } - - private static class PrefetchArgs - { - public const string CommitId = "CommitId"; - public const string Files = "Files"; - public const string Folders = "Folders"; - public const string Hydrate = "Hydrate"; - } - } + return false; + } + + error = null; + return true; + } + + private static string ToFilterPath(string path, bool isFolder) + { + string filterPath = + path.StartsWith("*") + ? path + : path.Replace(ScalarConstants.GitPathSeparator, Path.DirectorySeparatorChar).TrimStart(Path.DirectorySeparatorChar); + + if (isFolder && filterPath.Length > 0 && !filterPath.EndsWith(pathSeparatorString)) + { + filterPath += pathSeparatorString; + } + + return filterPath; + } + + private bool IsSymbolicRef(string targetCommitish) + { + return targetCommitish.StartsWith("refs/", StringComparison.OrdinalIgnoreCase); + } + + private void SavePrefetchArgs(string targetCommit, bool hydrate) + { + if (this.lastPrefetchArgs != null) + { + this.lastPrefetchArgs.SetValuesAndFlush( + new[] + { + new KeyValuePair(PrefetchArgs.CommitId, targetCommit), + new KeyValuePair(PrefetchArgs.Files, JsonConvert.SerializeObject(this.FileList)), + new KeyValuePair(PrefetchArgs.Folders, JsonConvert.SerializeObject(this.FolderList)), + new KeyValuePair(PrefetchArgs.Hydrate, hydrate.ToString()), + }); + } + } + + public class FetchException : Exception + { + public FetchException(string format, params object[] args) + : base(string.Format(format, args)) + { + } + } + + private static class PrefetchArgs + { + public const string CommitId = "CommitId"; + public const string Files = "Files"; + public const string Folders = "Folders"; + public const string Hydrate = "Hydrate"; + } + } } diff --git a/Scalar.Common/Prefetch/Git/DiffHelper.cs b/Scalar.Common/Prefetch/Git/DiffHelper.cs index 508cdae14c..62445ed32c 100644 --- a/Scalar.Common/Prefetch/Git/DiffHelper.cs +++ b/Scalar.Common/Prefetch/Git/DiffHelper.cs @@ -1,430 +1,430 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.Common.Prefetch.Git -{ - public class DiffHelper - { - private const string AreaPath = nameof(DiffHelper); - - private ITracer tracer; - private HashSet exactFileList; - private List patternList; - private List folderList; - private HashSet filesAdded = new HashSet(StringComparer.OrdinalIgnoreCase); - - private HashSet stagedDirectoryOperations = new HashSet(new DiffTreeByNameComparer()); - private HashSet stagedFileDeletes = new HashSet(StringComparer.OrdinalIgnoreCase); - - private Enlistment enlistment; - private GitProcess git; - - public DiffHelper(ITracer tracer, Enlistment enlistment, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) - : this(tracer, enlistment, new GitProcess(enlistment), fileList, folderList, includeSymLinks) - { - } - - public DiffHelper(ITracer tracer, Enlistment enlistment, GitProcess git, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) - { - this.tracer = tracer; - this.exactFileList = new HashSet(fileList.Where(x => !x.StartsWith("*")), StringComparer.OrdinalIgnoreCase); - this.patternList = fileList.Where(x => x.StartsWith("*")).ToList(); - this.folderList = new List(folderList); - this.enlistment = enlistment; - this.git = git; - this.ShouldIncludeSymLinks = includeSymLinks; - - this.DirectoryOperations = new ConcurrentQueue(); - this.FileDeleteOperations = new ConcurrentQueue(); - this.FileAddOperations = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); - this.RequiredBlobs = new BlockingCollection(); - } - - public bool ShouldIncludeSymLinks { get; set; } - - public bool HasFailures { get; private set; } - - public ConcurrentQueue DirectoryOperations { get; } - - public ConcurrentQueue FileDeleteOperations { get; } - - /// - /// Mapping from available sha to filenames where blob should be written - /// - public ConcurrentDictionary> FileAddOperations { get; } - - /// - /// Blobs required to perform a checkout of the destination - /// - public BlockingCollection RequiredBlobs { get; } - - public int TotalDirectoryOperations - { - get { return this.stagedDirectoryOperations.Count; } - } - - public int TotalFileDeletes - { - get { return this.stagedFileDeletes.Count; } - } - - /// - /// Returns true if the whole tree was updated - /// - public bool UpdatedWholeTree { get; internal set; } = false; - - public void PerformDiff(string targetCommitSha) - { - string targetTreeSha; - string headTreeSha; - using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) - { - targetTreeSha = repo.GetTreeSha(targetCommitSha); - headTreeSha = repo.GetTreeSha("HEAD"); - } - - this.PerformDiff(headTreeSha, targetTreeSha); - } - - public void PerformDiff(string sourceTreeSha, string targetTreeSha) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("TargetTreeSha", targetTreeSha); - metadata.Add("HeadTreeSha", sourceTreeSha); - using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational, Keywords.Telemetry, metadata)) - { - metadata = new EventMetadata(); - if (sourceTreeSha == null) - { - this.UpdatedWholeTree = true; - - // Nothing is checked out (fresh git init), so we must search the entire tree. - GitProcess.Result result = this.git.LsTree( - targetTreeSha, - line => this.EnqueueOperationsFromLsTreeLine(activity, line), - recursive: true, - showAllTrees: true); - - if (result.ExitCodeIsFailure) - { - this.HasFailures = true; - metadata.Add("Errors", result.Errors); - metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); - } - - metadata.Add("Operation", "LsTree"); - } - else - { - // Diff head and target, determine what needs to be done. - GitProcess.Result result = this.git.DiffTree( - sourceTreeSha, - targetTreeSha, - line => this.EnqueueOperationsFromDiffTreeLine(this.tracer, line)); - - if (result.ExitCodeIsFailure) - { - this.HasFailures = true; - metadata.Add("Errors", result.Errors); - metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); - } - - metadata.Add("Operation", "DiffTree"); - } - - this.FlushStagedQueues(); - - metadata.Add("Success", !this.HasFailures); - metadata.Add("DirectoryOperationsCount", this.TotalDirectoryOperations); - metadata.Add("FileDeleteOperationsCount", this.TotalFileDeletes); - metadata.Add("RequiredBlobsCount", this.RequiredBlobs.Count); - metadata.Add("FileAddOperationsCount", this.FileAddOperations.Sum(kvp => kvp.Value.Count)); - activity.Stop(metadata); - } - } - - public void ParseDiffFile(string filename) - { - using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational)) - { - using (StreamReader file = new StreamReader(File.OpenRead(filename))) - { - while (!file.EndOfStream) - { - this.EnqueueOperationsFromDiffTreeLine(activity, file.ReadLine()); - } - } - - this.FlushStagedQueues(); - } - } - - private void FlushStagedQueues() - { - List deletedPaths = new List(); - foreach (DiffTreeResult result in this.stagedDirectoryOperations) - { - // Don't enqueue deletes that will be handled by recursively deleting their parent. - // Git traverses diffs in pre-order, so we are guaranteed to ignore child deletes here. - if (result.Operation == DiffTreeResult.Operations.Delete) - { - if (deletedPaths.Any(path => result.TargetPath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) - { - continue; - } - - deletedPaths.Add(result.TargetPath); - } - - this.DirectoryOperations.Enqueue(result); - } - - foreach (string filePath in this.stagedFileDeletes) - { - if (deletedPaths.Any(path => filePath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) - { - continue; - } - - deletedPaths.Add(filePath); - - this.FileDeleteOperations.Enqueue(filePath); - } - - this.RequiredBlobs.CompleteAdding(); - } - - private void EnqueueOperationsFromLsTreeLine(ITracer activity, string line) - { - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(line); - if (result == null) - { - this.tracer.RelatedError("Unrecognized ls-tree line: {0}", line); - } - - if (!this.ShouldIncludeResult(result)) - { - return; - } - - if (result.TargetIsDirectory) - { - if (!this.stagedDirectoryOperations.Add(result)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(result.TargetPath), result.TargetPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "File exists in tree with two different cases. Taking the last one."); - this.tracer.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); - - // Since we match only on filename, re-adding is the easiest way to update the set. - this.stagedDirectoryOperations.Remove(result); - this.stagedDirectoryOperations.Add(result); - } - } - else - { - this.EnqueueFileAddOperation(activity, result); - } - } - - private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string line) - { - if (!line.StartsWith(":")) - { - // Diff-tree starts with metadata we can ignore. - // Real diff lines always start with a colon - return; - } - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line); - if (!this.ShouldIncludeResult(result)) - { - return; - } - - if (result.Operation == DiffTreeResult.Operations.Unknown || - result.Operation == DiffTreeResult.Operations.Unmerged || - result.Operation == DiffTreeResult.Operations.CopyEdit || - result.Operation == DiffTreeResult.Operations.RenameEdit) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(result.TargetPath), result.TargetPath); - metadata.Add(nameof(line), line); - activity.RelatedError(metadata, "Unexpected diff operation: " + result.Operation); - this.HasFailures = true; - return; - } - - // Separate and enqueue all directory operations first. - if (result.SourceIsDirectory || result.TargetIsDirectory) - { - switch (result.Operation) - { - case DiffTreeResult.Operations.Delete: - if (!this.stagedDirectoryOperations.Add(result)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(result.TargetPath), result.TargetPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); - activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); - } - - break; - case DiffTreeResult.Operations.Add: - case DiffTreeResult.Operations.Modify: - if (!this.stagedDirectoryOperations.Add(result)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(result.TargetPath), result.TargetPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); - activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); - - // Replace the delete with the add to make sure we don't delete a folder from under ourselves - this.stagedDirectoryOperations.Remove(result); - this.stagedDirectoryOperations.Add(result); - } - - break; - default: - activity.RelatedError("Unexpected diff operation from line: {0}", line); - break; - } - } - else - { - switch (result.Operation) - { - case DiffTreeResult.Operations.Delete: - this.EnqueueFileDeleteOperation(activity, result.TargetPath); - - break; - case DiffTreeResult.Operations.Modify: - case DiffTreeResult.Operations.Add: - this.EnqueueFileAddOperation(activity, result); - break; - default: - activity.RelatedError("Unexpected diff operation from line: {0}", line); - break; - } - } - } - - private bool ShouldIncludeResult(DiffTreeResult blobAdd) - { - if (blobAdd.TargetIsSymLink && !this.ShouldIncludeSymLinks) - { - return false; - } - - if (blobAdd.TargetPath == null) - { - return true; - } - - if (this.exactFileList.Count == 0 && - this.patternList.Count == 0 && - this.folderList.Count == 0) - { - return true; - } - - if (this.exactFileList.Contains(blobAdd.TargetPath) || - this.patternList.Any(path => blobAdd.TargetPath.EndsWith(path.Substring(1), StringComparison.OrdinalIgnoreCase))) - { - return true; - } - - if (this.folderList.Any(path => blobAdd.TargetPath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - - return false; - } - - private void EnqueueFileDeleteOperation(ITracer activity, string targetPath) - { - if (this.filesAdded.Contains(targetPath)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(targetPath), targetPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); - activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); - - return; - } - - this.stagedFileDeletes.Add(targetPath); - } - - /// - /// This is not used in a multithreaded method, it doesn't need to be thread-safe - /// - private void EnqueueFileAddOperation(ITracer activity, DiffTreeResult operation) - { - // Each filepath should be case-insensitive unique. If there are duplicates, only the last parsed one should remain. - if (!this.filesAdded.Add(operation.TargetPath)) - { - foreach (KeyValuePair> kvp in this.FileAddOperations) - { - PathWithMode tempPathWithMode = new PathWithMode(operation.TargetPath, 0x0000); - if (kvp.Value.Remove(tempPathWithMode)) - { - break; - } - } - } - - if (this.stagedFileDeletes.Remove(operation.TargetPath)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(operation.TargetPath), operation.TargetPath); - metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); - activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); - } - - this.FileAddOperations.AddOrUpdate( - operation.TargetSha, - new HashSet { new PathWithMode(operation.TargetPath, operation.TargetMode) }, - (key, oldValue) => - { - oldValue.Add(new PathWithMode(operation.TargetPath, operation.TargetMode)); - return oldValue; - }); - - this.RequiredBlobs.Add(operation.TargetSha); - } - - private class DiffTreeByNameComparer : IEqualityComparer - { - public bool Equals(DiffTreeResult x, DiffTreeResult y) - { - if (x.TargetPath != null) - { - if (y.TargetPath != null) - { - return x.TargetPath.Equals(y.TargetPath, StringComparison.OrdinalIgnoreCase); - } - - return false; - } - else - { - // both null means they're equal - return y.TargetPath == null; - } - } - - public int GetHashCode(DiffTreeResult obj) - { - return obj.TargetPath != null ? - StringComparer.OrdinalIgnoreCase.GetHashCode(obj.TargetPath) : 0; - } - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.Common.Prefetch.Git +{ + public class DiffHelper + { + private const string AreaPath = nameof(DiffHelper); + + private ITracer tracer; + private HashSet exactFileList; + private List patternList; + private List folderList; + private HashSet filesAdded = new HashSet(StringComparer.OrdinalIgnoreCase); + + private HashSet stagedDirectoryOperations = new HashSet(new DiffTreeByNameComparer()); + private HashSet stagedFileDeletes = new HashSet(StringComparer.OrdinalIgnoreCase); + + private Enlistment enlistment; + private GitProcess git; + + public DiffHelper(ITracer tracer, Enlistment enlistment, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) + : this(tracer, enlistment, new GitProcess(enlistment), fileList, folderList, includeSymLinks) + { + } + + public DiffHelper(ITracer tracer, Enlistment enlistment, GitProcess git, IEnumerable fileList, IEnumerable folderList, bool includeSymLinks) + { + this.tracer = tracer; + this.exactFileList = new HashSet(fileList.Where(x => !x.StartsWith("*")), StringComparer.OrdinalIgnoreCase); + this.patternList = fileList.Where(x => x.StartsWith("*")).ToList(); + this.folderList = new List(folderList); + this.enlistment = enlistment; + this.git = git; + this.ShouldIncludeSymLinks = includeSymLinks; + + this.DirectoryOperations = new ConcurrentQueue(); + this.FileDeleteOperations = new ConcurrentQueue(); + this.FileAddOperations = new ConcurrentDictionary>(StringComparer.OrdinalIgnoreCase); + this.RequiredBlobs = new BlockingCollection(); + } + + public bool ShouldIncludeSymLinks { get; set; } + + public bool HasFailures { get; private set; } + + public ConcurrentQueue DirectoryOperations { get; } + + public ConcurrentQueue FileDeleteOperations { get; } + + /// + /// Mapping from available sha to filenames where blob should be written + /// + public ConcurrentDictionary> FileAddOperations { get; } + + /// + /// Blobs required to perform a checkout of the destination + /// + public BlockingCollection RequiredBlobs { get; } + + public int TotalDirectoryOperations + { + get { return this.stagedDirectoryOperations.Count; } + } + + public int TotalFileDeletes + { + get { return this.stagedFileDeletes.Count; } + } + + /// + /// Returns true if the whole tree was updated + /// + public bool UpdatedWholeTree { get; internal set; } = false; + + public void PerformDiff(string targetCommitSha) + { + string targetTreeSha; + string headTreeSha; + using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) + { + targetTreeSha = repo.GetTreeSha(targetCommitSha); + headTreeSha = repo.GetTreeSha("HEAD"); + } + + this.PerformDiff(headTreeSha, targetTreeSha); + } + + public void PerformDiff(string sourceTreeSha, string targetTreeSha) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("TargetTreeSha", targetTreeSha); + metadata.Add("HeadTreeSha", sourceTreeSha); + using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational, Keywords.Telemetry, metadata)) + { + metadata = new EventMetadata(); + if (sourceTreeSha == null) + { + this.UpdatedWholeTree = true; + + // Nothing is checked out (fresh git init), so we must search the entire tree. + GitProcess.Result result = this.git.LsTree( + targetTreeSha, + line => this.EnqueueOperationsFromLsTreeLine(activity, line), + recursive: true, + showAllTrees: true); + + if (result.ExitCodeIsFailure) + { + this.HasFailures = true; + metadata.Add("Errors", result.Errors); + metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); + } + + metadata.Add("Operation", "LsTree"); + } + else + { + // Diff head and target, determine what needs to be done. + GitProcess.Result result = this.git.DiffTree( + sourceTreeSha, + targetTreeSha, + line => this.EnqueueOperationsFromDiffTreeLine(this.tracer, line)); + + if (result.ExitCodeIsFailure) + { + this.HasFailures = true; + metadata.Add("Errors", result.Errors); + metadata.Add("Output", result.Output.Length > 1024 ? result.Output.Substring(1024) : result.Output); + } + + metadata.Add("Operation", "DiffTree"); + } + + this.FlushStagedQueues(); + + metadata.Add("Success", !this.HasFailures); + metadata.Add("DirectoryOperationsCount", this.TotalDirectoryOperations); + metadata.Add("FileDeleteOperationsCount", this.TotalFileDeletes); + metadata.Add("RequiredBlobsCount", this.RequiredBlobs.Count); + metadata.Add("FileAddOperationsCount", this.FileAddOperations.Sum(kvp => kvp.Value.Count)); + activity.Stop(metadata); + } + } + + public void ParseDiffFile(string filename) + { + using (ITracer activity = this.tracer.StartActivity("PerformDiff", EventLevel.Informational)) + { + using (StreamReader file = new StreamReader(File.OpenRead(filename))) + { + while (!file.EndOfStream) + { + this.EnqueueOperationsFromDiffTreeLine(activity, file.ReadLine()); + } + } + + this.FlushStagedQueues(); + } + } + + private void FlushStagedQueues() + { + List deletedPaths = new List(); + foreach (DiffTreeResult result in this.stagedDirectoryOperations) + { + // Don't enqueue deletes that will be handled by recursively deleting their parent. + // Git traverses diffs in pre-order, so we are guaranteed to ignore child deletes here. + if (result.Operation == DiffTreeResult.Operations.Delete) + { + if (deletedPaths.Any(path => result.TargetPath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + deletedPaths.Add(result.TargetPath); + } + + this.DirectoryOperations.Enqueue(result); + } + + foreach (string filePath in this.stagedFileDeletes) + { + if (deletedPaths.Any(path => filePath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) + { + continue; + } + + deletedPaths.Add(filePath); + + this.FileDeleteOperations.Enqueue(filePath); + } + + this.RequiredBlobs.CompleteAdding(); + } + + private void EnqueueOperationsFromLsTreeLine(ITracer activity, string line) + { + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(line); + if (result == null) + { + this.tracer.RelatedError("Unrecognized ls-tree line: {0}", line); + } + + if (!this.ShouldIncludeResult(result)) + { + return; + } + + if (result.TargetIsDirectory) + { + if (!this.stagedDirectoryOperations.Add(result)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(result.TargetPath), result.TargetPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "File exists in tree with two different cases. Taking the last one."); + this.tracer.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); + + // Since we match only on filename, re-adding is the easiest way to update the set. + this.stagedDirectoryOperations.Remove(result); + this.stagedDirectoryOperations.Add(result); + } + } + else + { + this.EnqueueFileAddOperation(activity, result); + } + } + + private void EnqueueOperationsFromDiffTreeLine(ITracer activity, string line) + { + if (!line.StartsWith(":")) + { + // Diff-tree starts with metadata we can ignore. + // Real diff lines always start with a colon + return; + } + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(line); + if (!this.ShouldIncludeResult(result)) + { + return; + } + + if (result.Operation == DiffTreeResult.Operations.Unknown || + result.Operation == DiffTreeResult.Operations.Unmerged || + result.Operation == DiffTreeResult.Operations.CopyEdit || + result.Operation == DiffTreeResult.Operations.RenameEdit) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(result.TargetPath), result.TargetPath); + metadata.Add(nameof(line), line); + activity.RelatedError(metadata, "Unexpected diff operation: " + result.Operation); + this.HasFailures = true; + return; + } + + // Separate and enqueue all directory operations first. + if (result.SourceIsDirectory || result.TargetIsDirectory) + { + switch (result.Operation) + { + case DiffTreeResult.Operations.Delete: + if (!this.stagedDirectoryOperations.Add(result)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(result.TargetPath), result.TargetPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); + activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); + } + + break; + case DiffTreeResult.Operations.Add: + case DiffTreeResult.Operations.Modify: + if (!this.stagedDirectoryOperations.Add(result)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(result.TargetPath), result.TargetPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); + activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); + + // Replace the delete with the add to make sure we don't delete a folder from under ourselves + this.stagedDirectoryOperations.Remove(result); + this.stagedDirectoryOperations.Add(result); + } + + break; + default: + activity.RelatedError("Unexpected diff operation from line: {0}", line); + break; + } + } + else + { + switch (result.Operation) + { + case DiffTreeResult.Operations.Delete: + this.EnqueueFileDeleteOperation(activity, result.TargetPath); + + break; + case DiffTreeResult.Operations.Modify: + case DiffTreeResult.Operations.Add: + this.EnqueueFileAddOperation(activity, result); + break; + default: + activity.RelatedError("Unexpected diff operation from line: {0}", line); + break; + } + } + } + + private bool ShouldIncludeResult(DiffTreeResult blobAdd) + { + if (blobAdd.TargetIsSymLink && !this.ShouldIncludeSymLinks) + { + return false; + } + + if (blobAdd.TargetPath == null) + { + return true; + } + + if (this.exactFileList.Count == 0 && + this.patternList.Count == 0 && + this.folderList.Count == 0) + { + return true; + } + + if (this.exactFileList.Contains(blobAdd.TargetPath) || + this.patternList.Any(path => blobAdd.TargetPath.EndsWith(path.Substring(1), StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + if (this.folderList.Any(path => blobAdd.TargetPath.StartsWith(path, StringComparison.OrdinalIgnoreCase))) + { + return true; + } + + return false; + } + + private void EnqueueFileDeleteOperation(ITracer activity, string targetPath) + { + if (this.filesAdded.Contains(targetPath)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(targetPath), targetPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); + activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); + + return; + } + + this.stagedFileDeletes.Add(targetPath); + } + + /// + /// This is not used in a multithreaded method, it doesn't need to be thread-safe + /// + private void EnqueueFileAddOperation(ITracer activity, DiffTreeResult operation) + { + // Each filepath should be case-insensitive unique. If there are duplicates, only the last parsed one should remain. + if (!this.filesAdded.Add(operation.TargetPath)) + { + foreach (KeyValuePair> kvp in this.FileAddOperations) + { + PathWithMode tempPathWithMode = new PathWithMode(operation.TargetPath, 0x0000); + if (kvp.Value.Remove(tempPathWithMode)) + { + break; + } + } + } + + if (this.stagedFileDeletes.Remove(operation.TargetPath)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(operation.TargetPath), operation.TargetPath); + metadata.Add(TracingConstants.MessageKey.WarningMessage, "A case change was attempted. It will not be reflected in the working directory."); + activity.RelatedEvent(EventLevel.Warning, "CaseConflict", metadata); + } + + this.FileAddOperations.AddOrUpdate( + operation.TargetSha, + new HashSet { new PathWithMode(operation.TargetPath, operation.TargetMode) }, + (key, oldValue) => + { + oldValue.Add(new PathWithMode(operation.TargetPath, operation.TargetMode)); + return oldValue; + }); + + this.RequiredBlobs.Add(operation.TargetSha); + } + + private class DiffTreeByNameComparer : IEqualityComparer + { + public bool Equals(DiffTreeResult x, DiffTreeResult y) + { + if (x.TargetPath != null) + { + if (y.TargetPath != null) + { + return x.TargetPath.Equals(y.TargetPath, StringComparison.OrdinalIgnoreCase); + } + + return false; + } + else + { + // both null means they're equal + return y.TargetPath == null; + } + } + + public int GetHashCode(DiffTreeResult obj) + { + return obj.TargetPath != null ? + StringComparer.OrdinalIgnoreCase.GetHashCode(obj.TargetPath) : 0; + } + } + } +} diff --git a/Scalar.Common/Prefetch/Git/PathWithMode.cs b/Scalar.Common/Prefetch/Git/PathWithMode.cs index 710c657fd8..cd53a853aa 100644 --- a/Scalar.Common/Prefetch/Git/PathWithMode.cs +++ b/Scalar.Common/Prefetch/Git/PathWithMode.cs @@ -1,33 +1,33 @@ -using System; - -namespace Scalar.Common.Prefetch.Git -{ - public class PathWithMode - { - public PathWithMode(string path, ushort mode) - { - this.Path = path; - this.Mode = mode; - } - - public ushort Mode { get; } - public string Path { get; } - - public override bool Equals(object obj) - { - PathWithMode x = obj as PathWithMode; - - if (x == null) - { - return false; - } - - return x.Path.Equals(this.Path, StringComparison.OrdinalIgnoreCase); - } - - public override int GetHashCode() - { - return StringComparer.OrdinalIgnoreCase.GetHashCode(this.Path); - } - } -} +using System; + +namespace Scalar.Common.Prefetch.Git +{ + public class PathWithMode + { + public PathWithMode(string path, ushort mode) + { + this.Path = path; + this.Mode = mode; + } + + public ushort Mode { get; } + public string Path { get; } + + public override bool Equals(object obj) + { + PathWithMode x = obj as PathWithMode; + + if (x == null) + { + return false; + } + + return x.Path.Equals(this.Path, StringComparison.OrdinalIgnoreCase); + } + + public override int GetHashCode() + { + return StringComparer.OrdinalIgnoreCase.GetHashCode(this.Path); + } + } +} diff --git a/Scalar.Common/Prefetch/Git/PrefetchGitObjects.cs b/Scalar.Common/Prefetch/Git/PrefetchGitObjects.cs index f33017d869..c7378c398f 100644 --- a/Scalar.Common/Prefetch/Git/PrefetchGitObjects.cs +++ b/Scalar.Common/Prefetch/Git/PrefetchGitObjects.cs @@ -1,14 +1,14 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; - -namespace Scalar.Common.Prefetch.Git -{ - public class PrefetchGitObjects : GitObjects - { - public PrefetchGitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) : base(tracer, enlistment, objectRequestor, fileSystem) - { - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; + +namespace Scalar.Common.Prefetch.Git +{ + public class PrefetchGitObjects : GitObjects + { + public PrefetchGitObjects(ITracer tracer, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor, PhysicalFileSystem fileSystem = null) : base(tracer, enlistment, objectRequestor, fileSystem) + { + } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/BatchObjectDownloadStage.cs b/Scalar.Common/Prefetch/Pipeline/BatchObjectDownloadStage.cs index f591a6e2f1..ea91ef43f3 100644 --- a/Scalar.Common/Prefetch/Pipeline/BatchObjectDownloadStage.cs +++ b/Scalar.Common/Prefetch/Pipeline/BatchObjectDownloadStage.cs @@ -1,244 +1,244 @@ -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.NetworkStreams; -using Scalar.Common.Prefetch.Pipeline.Data; -using Scalar.Common.Tracing; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline -{ - /// - /// Takes in blocks of object shas, downloads object shas as a pack or loose object, outputs pack locations (if applicable). - /// - public class BatchObjectDownloadStage : PrefetchPipelineStage - { - private const string AreaPath = nameof(BatchObjectDownloadStage); - private const string DownloadAreaPath = "Download"; - - private static readonly TimeSpan HeartBeatPeriod = TimeSpan.FromSeconds(20); - - private readonly DownloadRequestAggregator downloadRequests; - - private int activeDownloadCount; - - private ITracer tracer; - private Enlistment enlistment; - private GitObjectsHttpRequestor objectRequestor; - private GitObjects gitObjects; - private Timer heartbeat; - - private long bytesDownloaded = 0; - - public BatchObjectDownloadStage( - int maxParallel, - int chunkSize, - BlockingCollection missingBlobs, - BlockingCollection availableBlobs, - ITracer tracer, - Enlistment enlistment, - GitObjectsHttpRequestor objectRequestor, - GitObjects gitObjects) - : base(maxParallel) - { - this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); - - this.downloadRequests = new DownloadRequestAggregator(missingBlobs, chunkSize); - - this.enlistment = enlistment; - this.objectRequestor = objectRequestor; - - this.gitObjects = gitObjects; - - this.AvailablePacks = new BlockingCollection(); - this.AvailableObjects = availableBlobs; - } - - public BlockingCollection AvailablePacks { get; } - - public BlockingCollection AvailableObjects { get; } - - protected override void DoBeforeWork() - { - this.heartbeat = new Timer(this.EmitHeartbeat, null, TimeSpan.Zero, HeartBeatPeriod); - base.DoBeforeWork(); - } - - protected override void DoWork() - { - BlobDownloadRequest request; - while (this.downloadRequests.TryTake(out request)) - { - Interlocked.Increment(ref this.activeDownloadCount); - - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", request.RequestId); - metadata.Add("ActiveDownloads", this.activeDownloadCount); - metadata.Add("NumberOfObjects", request.ObjectIds.Count); - - using (ITracer activity = this.tracer.StartActivity(DownloadAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) - { - try - { - HashSet successfulDownloads = new HashSet(StringComparer.OrdinalIgnoreCase); - RetryWrapper.InvocationResult result = this.objectRequestor.TryDownloadObjects( - () => request.ObjectIds.Except(successfulDownloads), - onSuccess: (tryCount, response) => this.WriteObjectOrPack(request, tryCount, response, successfulDownloads), - onFailure: RetryWrapper.StandardErrorHandler(activity, request.RequestId, DownloadAreaPath), - preferBatchedLooseObjects: true); - - if (!result.Succeeded) - { - this.HasFailures = true; - } - - metadata.Add("Success", result.Succeeded); - metadata.Add("AttemptNumber", result.Attempts); - metadata["ActiveDownloads"] = this.activeDownloadCount - 1; - activity.Stop(metadata); - } - finally - { - Interlocked.Decrement(ref this.activeDownloadCount); - } - } - } - } - - protected override void DoAfterWork() - { - this.heartbeat.Dispose(); - this.heartbeat = null; - - this.AvailablePacks.CompleteAdding(); - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestCount", BlobDownloadRequest.TotalRequests); - metadata.Add("BytesDownloaded", this.bytesDownloaded); - this.tracer.Stop(metadata); - } - - private RetryWrapper.CallbackResult WriteObjectOrPack( - BlobDownloadRequest request, - int tryCount, - GitEndPointResponseData response, - HashSet successfulDownloads = null) - { - // To reduce allocations, reuse the same buffer when writing objects in this batch - byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; - - string fileName = null; - switch (response.ContentType) - { - case GitObjectContentType.LooseObject: - string sha = request.ObjectIds.First(); - fileName = this.gitObjects.WriteLooseObject( - response.Stream, - sha, - overwriteExistingObject: false, - bufToCopyWith: bufToCopyWith); - this.AvailableObjects.Add(sha); - break; - case GitObjectContentType.PackFile: - fileName = this.gitObjects.WriteTempPackFile(response.Stream); - this.AvailablePacks.Add(new IndexPackRequest(fileName, request)); - break; - case GitObjectContentType.BatchedLooseObjects: - BatchedLooseObjectDeserializer.OnLooseObject onLooseObject = (objectStream, sha1) => - { - this.gitObjects.WriteLooseObject( - objectStream, - sha1, - overwriteExistingObject: false, - bufToCopyWith: bufToCopyWith); - this.AvailableObjects.Add(sha1); - - if (successfulDownloads != null) - { - successfulDownloads.Add(sha1); - } - - // This isn't strictly correct because we don't add object header bytes, - // just the actual compressed content length, but we expect the amount of - // header data to be negligible compared to the objects themselves. - Interlocked.Add(ref this.bytesDownloaded, objectStream.Length); - }; - - new BatchedLooseObjectDeserializer(response.Stream, onLooseObject).ProcessObjects(); - break; - } - - if (fileName != null) - { - // NOTE: If we are writing a file as part of this method, the only case - // where it's not expected to exist is when running unit tests - FileInfo info = new FileInfo(fileName); - if (info.Exists) - { - Interlocked.Add(ref this.bytesDownloaded, info.Length); - } - else - { - return new RetryWrapper.CallbackResult( - new GitObjectsHttpRequestor.GitObjectTaskResult(false)); - } - } - - return new RetryWrapper.CallbackResult( - new GitObjectsHttpRequestor.GitObjectTaskResult(true)); - } - - private void EmitHeartbeat(object state) - { - EventMetadata metadata = new EventMetadata(); - metadata["ActiveDownloads"] = this.activeDownloadCount; - this.tracer.RelatedEvent(EventLevel.Verbose, "DownloadHeartbeat", metadata); - } - - private class DownloadRequestAggregator - { - private BlockingCollection missingBlobs; - private int chunkSize; - - public DownloadRequestAggregator(BlockingCollection missingBlobs, int chunkSize) - { - this.missingBlobs = missingBlobs; - this.chunkSize = chunkSize; - } - - public bool TryTake(out BlobDownloadRequest request) - { - List blobsInChunk = new List(); - - for (int i = 0; i < this.chunkSize; ++i) - { - // Only wait a short while for new work to show up, otherwise go ahead and download what we have accumulated so far - const int TimeoutMs = 100; - - string blobId; - if (this.missingBlobs.TryTake(out blobId, TimeoutMs)) - { - blobsInChunk.Add(blobId); - } - else if (blobsInChunk.Count > 0 || - this.missingBlobs.IsAddingCompleted) - { - break; - } - } - - if (blobsInChunk.Count > 0) - { - request = new BlobDownloadRequest(blobsInChunk); - return true; - } - - request = null; - return false; - } - } - } +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.NetworkStreams; +using Scalar.Common.Prefetch.Pipeline.Data; +using Scalar.Common.Tracing; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline +{ + /// + /// Takes in blocks of object shas, downloads object shas as a pack or loose object, outputs pack locations (if applicable). + /// + public class BatchObjectDownloadStage : PrefetchPipelineStage + { + private const string AreaPath = nameof(BatchObjectDownloadStage); + private const string DownloadAreaPath = "Download"; + + private static readonly TimeSpan HeartBeatPeriod = TimeSpan.FromSeconds(20); + + private readonly DownloadRequestAggregator downloadRequests; + + private int activeDownloadCount; + + private ITracer tracer; + private Enlistment enlistment; + private GitObjectsHttpRequestor objectRequestor; + private GitObjects gitObjects; + private Timer heartbeat; + + private long bytesDownloaded = 0; + + public BatchObjectDownloadStage( + int maxParallel, + int chunkSize, + BlockingCollection missingBlobs, + BlockingCollection availableBlobs, + ITracer tracer, + Enlistment enlistment, + GitObjectsHttpRequestor objectRequestor, + GitObjects gitObjects) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); + + this.downloadRequests = new DownloadRequestAggregator(missingBlobs, chunkSize); + + this.enlistment = enlistment; + this.objectRequestor = objectRequestor; + + this.gitObjects = gitObjects; + + this.AvailablePacks = new BlockingCollection(); + this.AvailableObjects = availableBlobs; + } + + public BlockingCollection AvailablePacks { get; } + + public BlockingCollection AvailableObjects { get; } + + protected override void DoBeforeWork() + { + this.heartbeat = new Timer(this.EmitHeartbeat, null, TimeSpan.Zero, HeartBeatPeriod); + base.DoBeforeWork(); + } + + protected override void DoWork() + { + BlobDownloadRequest request; + while (this.downloadRequests.TryTake(out request)) + { + Interlocked.Increment(ref this.activeDownloadCount); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", request.RequestId); + metadata.Add("ActiveDownloads", this.activeDownloadCount); + metadata.Add("NumberOfObjects", request.ObjectIds.Count); + + using (ITracer activity = this.tracer.StartActivity(DownloadAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) + { + try + { + HashSet successfulDownloads = new HashSet(StringComparer.OrdinalIgnoreCase); + RetryWrapper.InvocationResult result = this.objectRequestor.TryDownloadObjects( + () => request.ObjectIds.Except(successfulDownloads), + onSuccess: (tryCount, response) => this.WriteObjectOrPack(request, tryCount, response, successfulDownloads), + onFailure: RetryWrapper.StandardErrorHandler(activity, request.RequestId, DownloadAreaPath), + preferBatchedLooseObjects: true); + + if (!result.Succeeded) + { + this.HasFailures = true; + } + + metadata.Add("Success", result.Succeeded); + metadata.Add("AttemptNumber", result.Attempts); + metadata["ActiveDownloads"] = this.activeDownloadCount - 1; + activity.Stop(metadata); + } + finally + { + Interlocked.Decrement(ref this.activeDownloadCount); + } + } + } + } + + protected override void DoAfterWork() + { + this.heartbeat.Dispose(); + this.heartbeat = null; + + this.AvailablePacks.CompleteAdding(); + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestCount", BlobDownloadRequest.TotalRequests); + metadata.Add("BytesDownloaded", this.bytesDownloaded); + this.tracer.Stop(metadata); + } + + private RetryWrapper.CallbackResult WriteObjectOrPack( + BlobDownloadRequest request, + int tryCount, + GitEndPointResponseData response, + HashSet successfulDownloads = null) + { + // To reduce allocations, reuse the same buffer when writing objects in this batch + byte[] bufToCopyWith = new byte[StreamUtil.DefaultCopyBufferSize]; + + string fileName = null; + switch (response.ContentType) + { + case GitObjectContentType.LooseObject: + string sha = request.ObjectIds.First(); + fileName = this.gitObjects.WriteLooseObject( + response.Stream, + sha, + overwriteExistingObject: false, + bufToCopyWith: bufToCopyWith); + this.AvailableObjects.Add(sha); + break; + case GitObjectContentType.PackFile: + fileName = this.gitObjects.WriteTempPackFile(response.Stream); + this.AvailablePacks.Add(new IndexPackRequest(fileName, request)); + break; + case GitObjectContentType.BatchedLooseObjects: + BatchedLooseObjectDeserializer.OnLooseObject onLooseObject = (objectStream, sha1) => + { + this.gitObjects.WriteLooseObject( + objectStream, + sha1, + overwriteExistingObject: false, + bufToCopyWith: bufToCopyWith); + this.AvailableObjects.Add(sha1); + + if (successfulDownloads != null) + { + successfulDownloads.Add(sha1); + } + + // This isn't strictly correct because we don't add object header bytes, + // just the actual compressed content length, but we expect the amount of + // header data to be negligible compared to the objects themselves. + Interlocked.Add(ref this.bytesDownloaded, objectStream.Length); + }; + + new BatchedLooseObjectDeserializer(response.Stream, onLooseObject).ProcessObjects(); + break; + } + + if (fileName != null) + { + // NOTE: If we are writing a file as part of this method, the only case + // where it's not expected to exist is when running unit tests + FileInfo info = new FileInfo(fileName); + if (info.Exists) + { + Interlocked.Add(ref this.bytesDownloaded, info.Length); + } + else + { + return new RetryWrapper.CallbackResult( + new GitObjectsHttpRequestor.GitObjectTaskResult(false)); + } + } + + return new RetryWrapper.CallbackResult( + new GitObjectsHttpRequestor.GitObjectTaskResult(true)); + } + + private void EmitHeartbeat(object state) + { + EventMetadata metadata = new EventMetadata(); + metadata["ActiveDownloads"] = this.activeDownloadCount; + this.tracer.RelatedEvent(EventLevel.Verbose, "DownloadHeartbeat", metadata); + } + + private class DownloadRequestAggregator + { + private BlockingCollection missingBlobs; + private int chunkSize; + + public DownloadRequestAggregator(BlockingCollection missingBlobs, int chunkSize) + { + this.missingBlobs = missingBlobs; + this.chunkSize = chunkSize; + } + + public bool TryTake(out BlobDownloadRequest request) + { + List blobsInChunk = new List(); + + for (int i = 0; i < this.chunkSize; ++i) + { + // Only wait a short while for new work to show up, otherwise go ahead and download what we have accumulated so far + const int TimeoutMs = 100; + + string blobId; + if (this.missingBlobs.TryTake(out blobId, TimeoutMs)) + { + blobsInChunk.Add(blobId); + } + else if (blobsInChunk.Count > 0 || + this.missingBlobs.IsAddingCompleted) + { + break; + } + } + + if (blobsInChunk.Count > 0) + { + request = new BlobDownloadRequest(blobsInChunk); + return true; + } + + request = null; + return false; + } + } + } } diff --git a/Scalar.Common/Prefetch/Pipeline/Data/BlobDownloadRequest.cs b/Scalar.Common/Prefetch/Pipeline/Data/BlobDownloadRequest.cs index 41bba1ab03..447c974081 100644 --- a/Scalar.Common/Prefetch/Pipeline/Data/BlobDownloadRequest.cs +++ b/Scalar.Common/Prefetch/Pipeline/Data/BlobDownloadRequest.cs @@ -1,28 +1,28 @@ -using System.Collections.Generic; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline.Data -{ - public class BlobDownloadRequest - { - private static int requestCounter = 0; - - public BlobDownloadRequest(IReadOnlyList objectIds) - { - this.ObjectIds = objectIds; - this.RequestId = Interlocked.Increment(ref requestCounter); - } - - public static int TotalRequests - { - get - { - return requestCounter; - } - } - - public IReadOnlyList ObjectIds { get; } - - public int RequestId { get; } - } -} +using System.Collections.Generic; +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline.Data +{ + public class BlobDownloadRequest + { + private static int requestCounter = 0; + + public BlobDownloadRequest(IReadOnlyList objectIds) + { + this.ObjectIds = objectIds; + this.RequestId = Interlocked.Increment(ref requestCounter); + } + + public static int TotalRequests + { + get + { + return requestCounter; + } + } + + public IReadOnlyList ObjectIds { get; } + + public int RequestId { get; } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/Data/IndexPackRequest.cs b/Scalar.Common/Prefetch/Pipeline/Data/IndexPackRequest.cs index 31ac9326bb..bbb4d87492 100644 --- a/Scalar.Common/Prefetch/Pipeline/Data/IndexPackRequest.cs +++ b/Scalar.Common/Prefetch/Pipeline/Data/IndexPackRequest.cs @@ -1,15 +1,15 @@ -namespace Scalar.Common.Prefetch.Pipeline.Data -{ - public class IndexPackRequest - { - public IndexPackRequest(string tempPackFile, BlobDownloadRequest downloadRequest) - { - this.TempPackFile = tempPackFile; - this.DownloadRequest = downloadRequest; - } - - public BlobDownloadRequest DownloadRequest { get; } - - public string TempPackFile { get; } - } +namespace Scalar.Common.Prefetch.Pipeline.Data +{ + public class IndexPackRequest + { + public IndexPackRequest(string tempPackFile, BlobDownloadRequest downloadRequest) + { + this.TempPackFile = tempPackFile; + this.DownloadRequest = downloadRequest; + } + + public BlobDownloadRequest DownloadRequest { get; } + + public string TempPackFile { get; } + } } diff --git a/Scalar.Common/Prefetch/Pipeline/Data/TreeSearchRequest.cs b/Scalar.Common/Prefetch/Pipeline/Data/TreeSearchRequest.cs index ffa3c3778a..0d681be04d 100644 --- a/Scalar.Common/Prefetch/Pipeline/Data/TreeSearchRequest.cs +++ b/Scalar.Common/Prefetch/Pipeline/Data/TreeSearchRequest.cs @@ -1,18 +1,18 @@ -namespace Scalar.Common.Prefetch.Pipeline.Data -{ - public class SearchTreeRequest - { - public SearchTreeRequest(string treeSha, string rootPath, bool shouldRecurse) - { - this.TreeSha = treeSha; - this.RootPath = rootPath; - this.ShouldRecurse = shouldRecurse; - } - - public bool ShouldRecurse { get; } - - public string TreeSha { get; } - - public string RootPath { get; } - } -} +namespace Scalar.Common.Prefetch.Pipeline.Data +{ + public class SearchTreeRequest + { + public SearchTreeRequest(string treeSha, string rootPath, bool shouldRecurse) + { + this.TreeSha = treeSha; + this.RootPath = rootPath; + this.ShouldRecurse = shouldRecurse; + } + + public bool ShouldRecurse { get; } + + public string TreeSha { get; } + + public string RootPath { get; } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/FindBlobsStage.cs b/Scalar.Common/Prefetch/Pipeline/FindBlobsStage.cs index 8e4cededdf..b08afe09df 100644 --- a/Scalar.Common/Prefetch/Pipeline/FindBlobsStage.cs +++ b/Scalar.Common/Prefetch/Pipeline/FindBlobsStage.cs @@ -1,89 +1,89 @@ -using Scalar.Common.Git; -using Scalar.Common.Prefetch.Git; -using Scalar.Common.Tracing; -using System.Collections.Concurrent; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline -{ - /// - /// Takes in search requests, searches each tree as requested, outputs blocks of missing blob shas. - /// - public class FindBlobsStage : PrefetchPipelineStage - { - private const string AreaPath = nameof(FindBlobsStage); - - private ITracer tracer; - private Enlistment enlistment; - private int missingBlobCount; - private int availableBlobCount; - - private BlockingCollection requiredBlobs; - - private ConcurrentHashSet alreadyFoundBlobIds; - - public FindBlobsStage( - int maxParallel, - BlockingCollection requiredBlobs, - BlockingCollection availableBlobs, - ITracer tracer, - Enlistment enlistment) - : base(maxParallel) - { - this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); - this.requiredBlobs = requiredBlobs; - this.enlistment = enlistment; - this.alreadyFoundBlobIds = new ConcurrentHashSet(); - - this.MissingBlobs = new BlockingCollection(); - this.AvailableBlobs = availableBlobs; - } - - public BlockingCollection MissingBlobs { get; } - public BlockingCollection AvailableBlobs { get; } - - public int MissingBlobCount - { - get { return this.missingBlobCount; } - } - - public int AvailableBlobCount - { - get { return this.availableBlobCount; } - } - - protected override void DoWork() - { - string blobId; - using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) - { - while (this.requiredBlobs.TryTake(out blobId, Timeout.Infinite)) - { - if (this.alreadyFoundBlobIds.Add(blobId)) - { - if (!repo.ObjectExists(blobId)) - { - Interlocked.Increment(ref this.missingBlobCount); - this.MissingBlobs.Add(blobId); - } - else - { - Interlocked.Increment(ref this.availableBlobCount); - this.AvailableBlobs.Add(blobId); - } - } - } - } - } - - protected override void DoAfterWork() - { - this.MissingBlobs.CompleteAdding(); - - EventMetadata metadata = new EventMetadata(); - metadata.Add("TotalMissingObjects", this.missingBlobCount); - metadata.Add("AvailableObjects", this.availableBlobCount); - this.tracer.Stop(metadata); - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Prefetch.Git; +using Scalar.Common.Tracing; +using System.Collections.Concurrent; +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline +{ + /// + /// Takes in search requests, searches each tree as requested, outputs blocks of missing blob shas. + /// + public class FindBlobsStage : PrefetchPipelineStage + { + private const string AreaPath = nameof(FindBlobsStage); + + private ITracer tracer; + private Enlistment enlistment; + private int missingBlobCount; + private int availableBlobCount; + + private BlockingCollection requiredBlobs; + + private ConcurrentHashSet alreadyFoundBlobIds; + + public FindBlobsStage( + int maxParallel, + BlockingCollection requiredBlobs, + BlockingCollection availableBlobs, + ITracer tracer, + Enlistment enlistment) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); + this.requiredBlobs = requiredBlobs; + this.enlistment = enlistment; + this.alreadyFoundBlobIds = new ConcurrentHashSet(); + + this.MissingBlobs = new BlockingCollection(); + this.AvailableBlobs = availableBlobs; + } + + public BlockingCollection MissingBlobs { get; } + public BlockingCollection AvailableBlobs { get; } + + public int MissingBlobCount + { + get { return this.missingBlobCount; } + } + + public int AvailableBlobCount + { + get { return this.availableBlobCount; } + } + + protected override void DoWork() + { + string blobId; + using (LibGit2Repo repo = new LibGit2Repo(this.tracer, this.enlistment.WorkingDirectoryBackingRoot)) + { + while (this.requiredBlobs.TryTake(out blobId, Timeout.Infinite)) + { + if (this.alreadyFoundBlobIds.Add(blobId)) + { + if (!repo.ObjectExists(blobId)) + { + Interlocked.Increment(ref this.missingBlobCount); + this.MissingBlobs.Add(blobId); + } + else + { + Interlocked.Increment(ref this.availableBlobCount); + this.AvailableBlobs.Add(blobId); + } + } + } + } + } + + protected override void DoAfterWork() + { + this.MissingBlobs.CompleteAdding(); + + EventMetadata metadata = new EventMetadata(); + metadata.Add("TotalMissingObjects", this.missingBlobCount); + metadata.Add("AvailableObjects", this.availableBlobCount); + this.tracer.Stop(metadata); + } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/HydrateFilesStage.cs b/Scalar.Common/Prefetch/Pipeline/HydrateFilesStage.cs index 8a14405558..1c68e19632 100644 --- a/Scalar.Common/Prefetch/Pipeline/HydrateFilesStage.cs +++ b/Scalar.Common/Prefetch/Pipeline/HydrateFilesStage.cs @@ -1,72 +1,72 @@ -using Scalar.Common.Prefetch.Git; -using Scalar.Common.Tracing; -using System.Collections.Concurrent; -using System.Collections.Generic; +using Scalar.Common.Prefetch.Git; +using Scalar.Common.Tracing; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.IO; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline -{ - public class HydrateFilesStage : PrefetchPipelineStage - { - private readonly string workingDirectoryRoot; - private readonly ConcurrentDictionary> blobIdToPaths; - private readonly BlockingCollection availableBlobs; - - private ITracer tracer; - private int readFileCount; - - public HydrateFilesStage(int maxThreads, string workingDirectoryRoot, ConcurrentDictionary> blobIdToPaths, BlockingCollection availableBlobs, ITracer tracer) - : base(maxThreads) - { - this.workingDirectoryRoot = workingDirectoryRoot; - this.blobIdToPaths = blobIdToPaths; - this.availableBlobs = availableBlobs; - - this.tracer = tracer; - } - - public int ReadFileCount - { - get { return this.readFileCount; } - } - - protected override void DoWork() - { - using (ITracer activity = this.tracer.StartActivity("ReadFiles", EventLevel.Informational)) - { - int readFilesCurrentThread = 0; - int failedFilesCurrentThread = 0; - - byte[] buffer = new byte[1]; - string blobId; - while (this.availableBlobs.TryTake(out blobId, Timeout.Infinite)) - { - foreach (PathWithMode modeAndPath in this.blobIdToPaths[blobId]) - { - bool succeeded = ScalarPlatform.Instance.FileSystem.HydrateFile(Path.Combine(this.workingDirectoryRoot, modeAndPath.Path), buffer); - if (succeeded) - { - Interlocked.Increment(ref this.readFileCount); - readFilesCurrentThread++; - } - else - { - activity.RelatedError("Failed to read " + modeAndPath.Path); - - failedFilesCurrentThread++; - this.HasFailures = true; - } +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline +{ + public class HydrateFilesStage : PrefetchPipelineStage + { + private readonly string workingDirectoryRoot; + private readonly ConcurrentDictionary> blobIdToPaths; + private readonly BlockingCollection availableBlobs; + + private ITracer tracer; + private int readFileCount; + + public HydrateFilesStage(int maxThreads, string workingDirectoryRoot, ConcurrentDictionary> blobIdToPaths, BlockingCollection availableBlobs, ITracer tracer) + : base(maxThreads) + { + this.workingDirectoryRoot = workingDirectoryRoot; + this.blobIdToPaths = blobIdToPaths; + this.availableBlobs = availableBlobs; + + this.tracer = tracer; + } + + public int ReadFileCount + { + get { return this.readFileCount; } + } + + protected override void DoWork() + { + using (ITracer activity = this.tracer.StartActivity("ReadFiles", EventLevel.Informational)) + { + int readFilesCurrentThread = 0; + int failedFilesCurrentThread = 0; + + byte[] buffer = new byte[1]; + string blobId; + while (this.availableBlobs.TryTake(out blobId, Timeout.Infinite)) + { + foreach (PathWithMode modeAndPath in this.blobIdToPaths[blobId]) + { + bool succeeded = ScalarPlatform.Instance.FileSystem.HydrateFile(Path.Combine(this.workingDirectoryRoot, modeAndPath.Path), buffer); + if (succeeded) + { + Interlocked.Increment(ref this.readFileCount); + readFilesCurrentThread++; + } + else + { + activity.RelatedError("Failed to read " + modeAndPath.Path); + + failedFilesCurrentThread++; + this.HasFailures = true; + } } - } - - activity.Stop( - new EventMetadata - { - { "FilesRead", readFilesCurrentThread }, - { "Failures", failedFilesCurrentThread }, - }); - } - } - } -} + } + + activity.Stop( + new EventMetadata + { + { "FilesRead", readFilesCurrentThread }, + { "Failures", failedFilesCurrentThread }, + }); + } + } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/IndexPackStage.cs b/Scalar.Common/Prefetch/Pipeline/IndexPackStage.cs index 90f17cf604..7ed42765a1 100644 --- a/Scalar.Common/Prefetch/Pipeline/IndexPackStage.cs +++ b/Scalar.Common/Prefetch/Pipeline/IndexPackStage.cs @@ -1,77 +1,77 @@ -using Scalar.Common.Git; -using Scalar.Common.Prefetch.Pipeline.Data; -using Scalar.Common.Tracing; -using System.Collections.Concurrent; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline -{ - public class IndexPackStage : PrefetchPipelineStage - { - private const string AreaPath = nameof(IndexPackStage); - private const string IndexPackAreaPath = "IndexPack"; - - private readonly BlockingCollection availablePacks; - - private ITracer tracer; - private GitObjects gitObjects; - - private long shasIndexed = 0; - - public IndexPackStage( - int maxParallel, - BlockingCollection availablePacks, - BlockingCollection availableBlobs, - ITracer tracer, - GitObjects gitObjects) - : base(maxParallel) - { - this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); - this.availablePacks = availablePacks; - this.gitObjects = gitObjects; - this.AvailableBlobs = availableBlobs; - } - - public BlockingCollection AvailableBlobs { get; } - - protected override void DoWork() - { - IndexPackRequest request; - while (this.availablePacks.TryTake(out request, Timeout.Infinite)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", request.DownloadRequest.RequestId); - using (ITracer activity = this.tracer.StartActivity(IndexPackAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) - { - GitProcess.Result result = this.gitObjects.IndexTempPackFile(request.TempPackFile); - if (result.ExitCodeIsFailure) - { - EventMetadata errorMetadata = new EventMetadata(); - errorMetadata.Add("RequestId", request.DownloadRequest.RequestId); - activity.RelatedError(errorMetadata, result.Errors); - this.HasFailures = true; - } - - if (!this.HasFailures) - { - foreach (string blobId in request.DownloadRequest.ObjectIds) - { - this.AvailableBlobs.Add(blobId); - Interlocked.Increment(ref this.shasIndexed); - } - } - - metadata.Add("Success", !this.HasFailures); - activity.Stop(metadata); - } - } - } - - protected override void DoAfterWork() - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("ShasIndexed", this.shasIndexed); - this.tracer.Stop(metadata); - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Prefetch.Pipeline.Data; +using Scalar.Common.Tracing; +using System.Collections.Concurrent; +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline +{ + public class IndexPackStage : PrefetchPipelineStage + { + private const string AreaPath = nameof(IndexPackStage); + private const string IndexPackAreaPath = "IndexPack"; + + private readonly BlockingCollection availablePacks; + + private ITracer tracer; + private GitObjects gitObjects; + + private long shasIndexed = 0; + + public IndexPackStage( + int maxParallel, + BlockingCollection availablePacks, + BlockingCollection availableBlobs, + ITracer tracer, + GitObjects gitObjects) + : base(maxParallel) + { + this.tracer = tracer.StartActivity(AreaPath, EventLevel.Informational, Keywords.Telemetry, metadata: null); + this.availablePacks = availablePacks; + this.gitObjects = gitObjects; + this.AvailableBlobs = availableBlobs; + } + + public BlockingCollection AvailableBlobs { get; } + + protected override void DoWork() + { + IndexPackRequest request; + while (this.availablePacks.TryTake(out request, Timeout.Infinite)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", request.DownloadRequest.RequestId); + using (ITracer activity = this.tracer.StartActivity(IndexPackAreaPath, EventLevel.Informational, Keywords.Telemetry, metadata)) + { + GitProcess.Result result = this.gitObjects.IndexTempPackFile(request.TempPackFile); + if (result.ExitCodeIsFailure) + { + EventMetadata errorMetadata = new EventMetadata(); + errorMetadata.Add("RequestId", request.DownloadRequest.RequestId); + activity.RelatedError(errorMetadata, result.Errors); + this.HasFailures = true; + } + + if (!this.HasFailures) + { + foreach (string blobId in request.DownloadRequest.ObjectIds) + { + this.AvailableBlobs.Add(blobId); + Interlocked.Increment(ref this.shasIndexed); + } + } + + metadata.Add("Success", !this.HasFailures); + activity.Stop(metadata); + } + } + } + + protected override void DoAfterWork() + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("ShasIndexed", this.shasIndexed); + this.tracer.Stop(metadata); + } + } +} diff --git a/Scalar.Common/Prefetch/Pipeline/PrefetchPipelineStage.cs b/Scalar.Common/Prefetch/Pipeline/PrefetchPipelineStage.cs index 4e2dd90664..d2197a8598 100644 --- a/Scalar.Common/Prefetch/Pipeline/PrefetchPipelineStage.cs +++ b/Scalar.Common/Prefetch/Pipeline/PrefetchPipelineStage.cs @@ -1,61 +1,61 @@ -using System; -using System.Threading; - -namespace Scalar.Common.Prefetch.Pipeline -{ - public abstract class PrefetchPipelineStage - { - private int maxParallel; - private Thread[] workers; - - public PrefetchPipelineStage(int maxParallel) - { - this.maxParallel = maxParallel; - } - - public bool HasFailures { get; protected set; } - - public void Start() - { - if (this.workers != null) - { - throw new InvalidOperationException("Cannot call start twice"); - } - - this.DoBeforeWork(); - - this.workers = new Thread[this.maxParallel]; - for (int i = 0; i < this.workers.Length; ++i) - { - this.workers[i] = new Thread(this.DoWork); - this.workers[i].Start(); - } - } - - public void WaitForCompletion() - { - if (this.workers == null) - { - throw new InvalidOperationException("Cannot wait for completion before start is called"); - } - - foreach (Thread t in this.workers) - { - t.Join(); - } - - this.DoAfterWork(); - this.workers = null; - } - - protected virtual void DoBeforeWork() - { - } - - protected abstract void DoWork(); - - protected virtual void DoAfterWork() - { - } - } -} +using System; +using System.Threading; + +namespace Scalar.Common.Prefetch.Pipeline +{ + public abstract class PrefetchPipelineStage + { + private int maxParallel; + private Thread[] workers; + + public PrefetchPipelineStage(int maxParallel) + { + this.maxParallel = maxParallel; + } + + public bool HasFailures { get; protected set; } + + public void Start() + { + if (this.workers != null) + { + throw new InvalidOperationException("Cannot call start twice"); + } + + this.DoBeforeWork(); + + this.workers = new Thread[this.maxParallel]; + for (int i = 0; i < this.workers.Length; ++i) + { + this.workers[i] = new Thread(this.DoWork); + this.workers[i].Start(); + } + } + + public void WaitForCompletion() + { + if (this.workers == null) + { + throw new InvalidOperationException("Cannot wait for completion before start is called"); + } + + foreach (Thread t in this.workers) + { + t.Join(); + } + + this.DoAfterWork(); + this.workers = null; + } + + protected virtual void DoBeforeWork() + { + } + + protected abstract void DoWork(); + + protected virtual void DoAfterWork() + { + } + } +} diff --git a/Scalar.Common/ProcessHelper.cs b/Scalar.Common/ProcessHelper.cs index ab7a1dd239..0c8cb294fb 100644 --- a/Scalar.Common/ProcessHelper.cs +++ b/Scalar.Common/ProcessHelper.cs @@ -1,147 +1,147 @@ -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace Scalar.Common -{ - public static class ProcessHelper - { - private static string currentProcessVersion = null; - - public static ProcessResult Run(string programName, string args, bool redirectOutput = true) - { - ProcessStartInfo processInfo = new ProcessStartInfo(programName); - processInfo.UseShellExecute = false; - processInfo.RedirectStandardInput = true; - processInfo.RedirectStandardOutput = redirectOutput; - processInfo.RedirectStandardError = redirectOutput; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = redirectOutput; - processInfo.Arguments = args; - - return Run(processInfo); - } - - public static string GetCurrentProcessLocation() - { - Assembly assembly = Assembly.GetExecutingAssembly(); - return Path.GetDirectoryName(assembly.Location); - } - - public static string GetEntryClassName() - { - Assembly assembly = Assembly.GetEntryAssembly(); - if (assembly == null) - { - // The PR build tests doesn't produce an entry assembly because it is run from unmanaged code, - // so we'll fall back on using this assembly. This should never ever happen for a normal exe invocation. - assembly = Assembly.GetExecutingAssembly(); - } - - return assembly.GetName().Name; - } - - public static string GetCurrentProcessVersion() - { - if (currentProcessVersion == null) - { - Assembly assembly = Assembly.GetExecutingAssembly(); - FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); - currentProcessVersion = fileVersionInfo.ProductVersion; - } - - return currentProcessVersion; - } - - public static bool IsDevelopmentVersion() - { - Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion()); - - return currentVersion.Major == 0; - } - - public static string GetProgramLocation(string programLocaterCommand, string processName) - { - ProcessResult result = ProcessHelper.Run(programLocaterCommand, processName); - if (result.ExitCode != 0) - { - return null; - } - - string firstPath = - string.IsNullOrWhiteSpace(result.Output) - ? null - : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - if (firstPath == null) - { - return null; - } - - try - { - return Path.GetDirectoryName(firstPath); - } - catch (IOException) - { - return null; - } - } - - public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null) - { - using (Process executingProcess = new Process()) - { - string output = string.Empty; - string errors = string.Empty; - - // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx - // To avoid deadlocks, use asynchronous read operations on at least one of the streams. - // Do not perform a synchronous read to the end of both redirected streams. - executingProcess.StartInfo = processInfo; - executingProcess.ErrorDataReceived += (sender, args) => - { - if (args.Data != null) - { - errors = errors + args.Data + errorMsgDelimeter; - } - }; - - if (executionLock != null) - { - lock (executionLock) - { - output = StartProcess(executingProcess); - } - } - else - { - output = StartProcess(executingProcess); - } - - return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); - } - } - - private static string StartProcess(Process executingProcess) - { - executingProcess.Start(); - - if (executingProcess.StartInfo.RedirectStandardError) - { - executingProcess.BeginErrorReadLine(); - } - - string output = string.Empty; - if (executingProcess.StartInfo.RedirectStandardOutput) - { - output = executingProcess.StandardOutput.ReadToEnd(); - } - - executingProcess.WaitForExit(); - - return output; - } - } -} +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Scalar.Common +{ + public static class ProcessHelper + { + private static string currentProcessVersion = null; + + public static ProcessResult Run(string programName, string args, bool redirectOutput = true) + { + ProcessStartInfo processInfo = new ProcessStartInfo(programName); + processInfo.UseShellExecute = false; + processInfo.RedirectStandardInput = true; + processInfo.RedirectStandardOutput = redirectOutput; + processInfo.RedirectStandardError = redirectOutput; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.CreateNoWindow = redirectOutput; + processInfo.Arguments = args; + + return Run(processInfo); + } + + public static string GetCurrentProcessLocation() + { + Assembly assembly = Assembly.GetExecutingAssembly(); + return Path.GetDirectoryName(assembly.Location); + } + + public static string GetEntryClassName() + { + Assembly assembly = Assembly.GetEntryAssembly(); + if (assembly == null) + { + // The PR build tests doesn't produce an entry assembly because it is run from unmanaged code, + // so we'll fall back on using this assembly. This should never ever happen for a normal exe invocation. + assembly = Assembly.GetExecutingAssembly(); + } + + return assembly.GetName().Name; + } + + public static string GetCurrentProcessVersion() + { + if (currentProcessVersion == null) + { + Assembly assembly = Assembly.GetExecutingAssembly(); + FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location); + currentProcessVersion = fileVersionInfo.ProductVersion; + } + + return currentProcessVersion; + } + + public static bool IsDevelopmentVersion() + { + Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion()); + + return currentVersion.Major == 0; + } + + public static string GetProgramLocation(string programLocaterCommand, string processName) + { + ProcessResult result = ProcessHelper.Run(programLocaterCommand, processName); + if (result.ExitCode != 0) + { + return null; + } + + string firstPath = + string.IsNullOrWhiteSpace(result.Output) + ? null + : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + if (firstPath == null) + { + return null; + } + + try + { + return Path.GetDirectoryName(firstPath); + } + catch (IOException) + { + return null; + } + } + + public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null) + { + using (Process executingProcess = new Process()) + { + string output = string.Empty; + string errors = string.Empty; + + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + executingProcess.StartInfo = processInfo; + executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors = errors + args.Data + errorMsgDelimeter; + } + }; + + if (executionLock != null) + { + lock (executionLock) + { + output = StartProcess(executingProcess); + } + } + else + { + output = StartProcess(executingProcess); + } + + return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); + } + } + + private static string StartProcess(Process executingProcess) + { + executingProcess.Start(); + + if (executingProcess.StartInfo.RedirectStandardError) + { + executingProcess.BeginErrorReadLine(); + } + + string output = string.Empty; + if (executingProcess.StartInfo.RedirectStandardOutput) + { + output = executingProcess.StandardOutput.ReadToEnd(); + } + + executingProcess.WaitForExit(); + + return output; + } + } +} diff --git a/Scalar.Common/ProcessResult.cs b/Scalar.Common/ProcessResult.cs index bb48883621..00becfeb4b 100644 --- a/Scalar.Common/ProcessResult.cs +++ b/Scalar.Common/ProcessResult.cs @@ -1,16 +1,16 @@ -namespace Scalar.Common -{ - public class ProcessResult - { - public ProcessResult(string output, string errors, int exitCode) - { - this.Output = output; - this.Errors = errors; - this.ExitCode = exitCode; - } - - public string Output { get; } - public string Errors { get; } - public int ExitCode { get; } - } -} +namespace Scalar.Common +{ + public class ProcessResult + { + public ProcessResult(string output, string errors, int exitCode) + { + this.Output = output; + this.Errors = errors; + this.ExitCode = exitCode; + } + + public string Output { get; } + public string Errors { get; } + public int ExitCode { get; } + } +} diff --git a/Scalar.Common/ProcessRunnerImpl.cs b/Scalar.Common/ProcessRunnerImpl.cs index e010a7723c..1a8b0f23c5 100644 --- a/Scalar.Common/ProcessRunnerImpl.cs +++ b/Scalar.Common/ProcessRunnerImpl.cs @@ -1,16 +1,16 @@ -namespace Scalar.Common -{ - /// - /// Default product implementation of IProcessRunner - /// interface. Delegates calls to static ProcessHelper class. This - /// class can be used to enable testing of components that call - /// into the ProcessHelper functionality. - /// - public class ProcessRunnerImpl : IProcessRunner - { - public ProcessResult Run(string programName, string args, bool redirectOutput) - { - return ProcessHelper.Run(programName, args, redirectOutput); - } - } -} +namespace Scalar.Common +{ + /// + /// Default product implementation of IProcessRunner + /// interface. Delegates calls to static ProcessHelper class. This + /// class can be used to enable testing of components that call + /// into the ProcessHelper functionality. + /// + public class ProcessRunnerImpl : IProcessRunner + { + public ProcessResult Run(string programName, string args, bool redirectOutput) + { + return ProcessHelper.Run(programName, args, redirectOutput); + } + } +} diff --git a/Scalar.Common/ProductUpgrader.cs b/Scalar.Common/ProductUpgrader.cs index 7104e68306..5e3f9cace5 100644 --- a/Scalar.Common/ProductUpgrader.cs +++ b/Scalar.Common/ProductUpgrader.cs @@ -1,245 +1,245 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.NuGetUpgrade; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net.Http; - -namespace Scalar.Common -{ - /// - /// Delegate to wrap install action steps in. - /// This can be used to report the beginning / end of each install step. - /// - /// The method to run inside wrapper - /// The message to display - /// success or failure return from the method run. - public delegate bool InstallActionWrapper(Func method, string message); - - public abstract class ProductUpgrader : IDisposable - { - protected readonly Version installedVersion; - protected readonly ITracer tracer; - protected readonly PhysicalFileSystem fileSystem; - - protected bool noVerify; - protected bool dryRun; - protected ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; - - protected ProductUpgrader( - string currentVersion, - ITracer tracer, - bool dryRun, - bool noVerify, - PhysicalFileSystem fileSystem) - : this( - currentVersion, - tracer, - dryRun, - noVerify, - fileSystem, - ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) - { - } - - protected ProductUpgrader( - string currentVersion, - ITracer tracer, - bool dryRun, - bool noVerify, - PhysicalFileSystem fileSystem, - ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy) - { - this.installedVersion = new Version(currentVersion); - this.dryRun = dryRun; - this.noVerify = noVerify; - this.tracer = tracer; - this.fileSystem = fileSystem; - this.productUpgraderPlatformStrategy = productUpgraderPlatformStrategy; - } - - /// - /// For mocking purposes only - /// - protected ProductUpgrader() - { - } - - public abstract bool SupportsAnonymousVersionQuery { get; } - - public string UpgradeInstanceId { get; set; } = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - - public static bool TryCreateUpgrader( - ITracer tracer, - PhysicalFileSystem fileSystem, - LocalScalarConfig scalarConfig, - ICredentialStore credentialStore, - bool dryRun, - bool noVerify, - out ProductUpgrader newUpgrader, - out string error) - { - Dictionary entries; - if (!scalarConfig.TryGetAllConfig(out entries, out error)) - { - newUpgrader = null; - return false; - } - - bool containsUpgradeFeedUrl = entries.ContainsKey(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl); - bool containsUpgradePackageName = entries.ContainsKey(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName); - bool containsOrgInfoServerUrl = entries.ContainsKey(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl); - - if (containsUpgradeFeedUrl || containsUpgradePackageName) - { - // We are configured for NuGet - determine if we are using OrgNuGetUpgrader or not - if (containsOrgInfoServerUrl) - { - if (OrgNuGetUpgrader.TryCreate( - tracer, - fileSystem, - scalarConfig, - new HttpClient(), - credentialStore, - dryRun, - noVerify, - out OrgNuGetUpgrader orgNuGetUpgrader, - out error)) - { - // We were successfully able to load a NuGetUpgrader - use that. - newUpgrader = orgNuGetUpgrader; - return true; - } - else - { - tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create organization based upgrader. {error}"); - newUpgrader = null; - return false; - } - } - else - { - if (NuGetUpgrader.TryCreate( - tracer, - fileSystem, - scalarConfig, - credentialStore, - dryRun, - noVerify, - out NuGetUpgrader nuGetUpgrader, - out bool isConfigured, - out error)) - { - // We were successfully able to load a NuGetUpgrader - use that. - newUpgrader = nuGetUpgrader; - return true; - } - else - { - tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create NuGet based upgrader. {error}"); - newUpgrader = null; - return false; - } - } - } - else - { - newUpgrader = GitHubUpgrader.Create(tracer, fileSystem, scalarConfig, dryRun, noVerify, out error); - if (newUpgrader == null) - { - tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create GitHub based upgrader. {error}"); - return false; - } - - return true; - } - } - - public abstract bool UpgradeAllowed(out string message); - - public abstract bool TryQueryNewestVersion(out Version newVersion, out string message); - - public abstract bool TryDownloadNewestVersion(out string errorMessage); - - public abstract bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error); - - public virtual bool TrySetupUpgradeApplicationDirectory(out string upgradeApplicationPath, out string error) - { - string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); - - if (!this.productUpgraderPlatformStrategy.TryPrepareApplicationDirectory(out error)) - { - upgradeApplicationPath = null; - return false; - } - - string currentPath = ProcessHelper.GetCurrentProcessLocation(); - error = null; - try - { - this.fileSystem.CopyDirectoryRecursive(currentPath, upgradeApplicationDirectory); - } - catch (UnauthorizedAccessException e) - { - error = string.Join( - Environment.NewLine, - "File copy error - " + e.Message, - $"Make sure you have write permissions to directory {upgradeApplicationDirectory} and run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} again."); - } - catch (IOException e) - { - error = "File copy error - " + e.Message; - this.TraceException(e, nameof(this.TrySetupUpgradeApplicationDirectory), $"Error copying {currentPath} to {upgradeApplicationDirectory}."); - } - - if (string.IsNullOrEmpty(error)) - { - // There was no error - set upgradeToolPath and return success. - upgradeApplicationPath = Path.Combine( - upgradeApplicationDirectory, - ScalarPlatform.Instance.Constants.ScalarUpgraderExecutableName); - return true; - } - else - { - // Encountered error - do not set upgrade tool path and return failure. - upgradeApplicationPath = null; - return false; - } - } - - public abstract bool TryCleanup(out string error); - - public void TraceException(Exception exception, string method, string message) - { - this.TraceException(this.tracer, exception, method, message); - } - - public void TraceException(ITracer tracer, Exception exception, string method, string message) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Method", method); - metadata.Add("Exception", exception.ToString()); - tracer.RelatedError(metadata, message); - } - - public virtual void Dispose() - { - } - - protected virtual bool TryCreateAndConfigureDownloadDirectory(ITracer tracer, out string error) - { - return this.productUpgraderPlatformStrategy.TryPrepareDownloadDirectory(out error); - } - - protected virtual void RunInstaller(string path, string args, out int exitCode, out string error) - { - ProcessResult processResult = ProcessHelper.Run(path, args); - - exitCode = processResult.ExitCode; - error = processResult.Errors; - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.NuGetUpgrade; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; + +namespace Scalar.Common +{ + /// + /// Delegate to wrap install action steps in. + /// This can be used to report the beginning / end of each install step. + /// + /// The method to run inside wrapper + /// The message to display + /// success or failure return from the method run. + public delegate bool InstallActionWrapper(Func method, string message); + + public abstract class ProductUpgrader : IDisposable + { + protected readonly Version installedVersion; + protected readonly ITracer tracer; + protected readonly PhysicalFileSystem fileSystem; + + protected bool noVerify; + protected bool dryRun; + protected ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; + + protected ProductUpgrader( + string currentVersion, + ITracer tracer, + bool dryRun, + bool noVerify, + PhysicalFileSystem fileSystem) + : this( + currentVersion, + tracer, + dryRun, + noVerify, + fileSystem, + ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer)) + { + } + + protected ProductUpgrader( + string currentVersion, + ITracer tracer, + bool dryRun, + bool noVerify, + PhysicalFileSystem fileSystem, + ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy) + { + this.installedVersion = new Version(currentVersion); + this.dryRun = dryRun; + this.noVerify = noVerify; + this.tracer = tracer; + this.fileSystem = fileSystem; + this.productUpgraderPlatformStrategy = productUpgraderPlatformStrategy; + } + + /// + /// For mocking purposes only + /// + protected ProductUpgrader() + { + } + + public abstract bool SupportsAnonymousVersionQuery { get; } + + public string UpgradeInstanceId { get; set; } = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + + public static bool TryCreateUpgrader( + ITracer tracer, + PhysicalFileSystem fileSystem, + LocalScalarConfig scalarConfig, + ICredentialStore credentialStore, + bool dryRun, + bool noVerify, + out ProductUpgrader newUpgrader, + out string error) + { + Dictionary entries; + if (!scalarConfig.TryGetAllConfig(out entries, out error)) + { + newUpgrader = null; + return false; + } + + bool containsUpgradeFeedUrl = entries.ContainsKey(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl); + bool containsUpgradePackageName = entries.ContainsKey(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName); + bool containsOrgInfoServerUrl = entries.ContainsKey(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl); + + if (containsUpgradeFeedUrl || containsUpgradePackageName) + { + // We are configured for NuGet - determine if we are using OrgNuGetUpgrader or not + if (containsOrgInfoServerUrl) + { + if (OrgNuGetUpgrader.TryCreate( + tracer, + fileSystem, + scalarConfig, + new HttpClient(), + credentialStore, + dryRun, + noVerify, + out OrgNuGetUpgrader orgNuGetUpgrader, + out error)) + { + // We were successfully able to load a NuGetUpgrader - use that. + newUpgrader = orgNuGetUpgrader; + return true; + } + else + { + tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create organization based upgrader. {error}"); + newUpgrader = null; + return false; + } + } + else + { + if (NuGetUpgrader.TryCreate( + tracer, + fileSystem, + scalarConfig, + credentialStore, + dryRun, + noVerify, + out NuGetUpgrader nuGetUpgrader, + out bool isConfigured, + out error)) + { + // We were successfully able to load a NuGetUpgrader - use that. + newUpgrader = nuGetUpgrader; + return true; + } + else + { + tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create NuGet based upgrader. {error}"); + newUpgrader = null; + return false; + } + } + } + else + { + newUpgrader = GitHubUpgrader.Create(tracer, fileSystem, scalarConfig, dryRun, noVerify, out error); + if (newUpgrader == null) + { + tracer.RelatedError($"{nameof(TryCreateUpgrader)}: Could not create GitHub based upgrader. {error}"); + return false; + } + + return true; + } + } + + public abstract bool UpgradeAllowed(out string message); + + public abstract bool TryQueryNewestVersion(out Version newVersion, out string message); + + public abstract bool TryDownloadNewestVersion(out string errorMessage); + + public abstract bool TryRunInstaller(InstallActionWrapper installActionWrapper, out string error); + + public virtual bool TrySetupUpgradeApplicationDirectory(out string upgradeApplicationPath, out string error) + { + string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); + + if (!this.productUpgraderPlatformStrategy.TryPrepareApplicationDirectory(out error)) + { + upgradeApplicationPath = null; + return false; + } + + string currentPath = ProcessHelper.GetCurrentProcessLocation(); + error = null; + try + { + this.fileSystem.CopyDirectoryRecursive(currentPath, upgradeApplicationDirectory); + } + catch (UnauthorizedAccessException e) + { + error = string.Join( + Environment.NewLine, + "File copy error - " + e.Message, + $"Make sure you have write permissions to directory {upgradeApplicationDirectory} and run {ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm} again."); + } + catch (IOException e) + { + error = "File copy error - " + e.Message; + this.TraceException(e, nameof(this.TrySetupUpgradeApplicationDirectory), $"Error copying {currentPath} to {upgradeApplicationDirectory}."); + } + + if (string.IsNullOrEmpty(error)) + { + // There was no error - set upgradeToolPath and return success. + upgradeApplicationPath = Path.Combine( + upgradeApplicationDirectory, + ScalarPlatform.Instance.Constants.ScalarUpgraderExecutableName); + return true; + } + else + { + // Encountered error - do not set upgrade tool path and return failure. + upgradeApplicationPath = null; + return false; + } + } + + public abstract bool TryCleanup(out string error); + + public void TraceException(Exception exception, string method, string message) + { + this.TraceException(this.tracer, exception, method, message); + } + + public void TraceException(ITracer tracer, Exception exception, string method, string message) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Method", method); + metadata.Add("Exception", exception.ToString()); + tracer.RelatedError(metadata, message); + } + + public virtual void Dispose() + { + } + + protected virtual bool TryCreateAndConfigureDownloadDirectory(ITracer tracer, out string error) + { + return this.productUpgraderPlatformStrategy.TryPrepareDownloadDirectory(out error); + } + + protected virtual void RunInstaller(string path, string args, out int exitCode, out string error) + { + ProcessResult processResult = ProcessHelper.Run(path, args); + + exitCode = processResult.ExitCode; + error = processResult.Errors; + } + } +} diff --git a/Scalar.Common/ProductUpgraderInfo.cs b/Scalar.Common/ProductUpgraderInfo.cs index c025131d6d..1a8e29b875 100644 --- a/Scalar.Common/ProductUpgraderInfo.cs +++ b/Scalar.Common/ProductUpgraderInfo.cs @@ -1,102 +1,102 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; - -namespace Scalar.Common -{ - public partial class ProductUpgraderInfo - { - private ITracer tracer; - private PhysicalFileSystem fileSystem; - - public ProductUpgraderInfo(ITracer tracer, PhysicalFileSystem fileSystem) - { - this.tracer = tracer; - this.fileSystem = fileSystem; - } - - public static string CurrentScalarVersion() - { - return ProcessHelper.GetCurrentProcessVersion(); - } - - public static string GetUpgradeProtectedDataDirectory() - { - return ScalarPlatform.Instance.GetUpgradeProtectedDataDirectory(); - } - - public static string GetUpgradeApplicationDirectory() - { - return Path.Combine( - GetUpgradeProtectedDataDirectory(), - ProductUpgraderInfo.ApplicationDirectory); - } - - public static string GetParentLogDirectoryPath() - { - return ScalarPlatform.Instance.GetUpgradeLogDirectoryParentDirectory(); - } - - public static string GetLogDirectoryPath() - { - return Path.Combine( - ScalarPlatform.Instance.GetUpgradeLogDirectoryParentDirectory(), - ProductUpgraderInfo.LogDirectory); - } - - public static string GetAssetDownloadsPath() - { - return Path.Combine( - ScalarPlatform.Instance.GetUpgradeProtectedDataDirectory(), - ProductUpgraderInfo.DownloadDirectory); - } - - public static string GetHighestAvailableVersionDirectory() - { - return ScalarPlatform.Instance.GetUpgradeHighestAvailableVersionDirectory(); - } - - public void DeleteAllInstallerDownloads() - { - try - { - this.fileSystem.DeleteDirectory(GetAssetDownloadsPath()); - } - catch (Exception ex) - { - if (this.tracer != null) - { - this.tracer.RelatedError($"{nameof(this.DeleteAllInstallerDownloads)}: Could not remove directory: {ProductUpgraderInfo.GetAssetDownloadsPath()}.{ex.ToString()}"); - } - } - } - - public void RecordHighestAvailableVersion(Version highestAvailableVersion) - { - string highestAvailableVersionFile = GetHighestAvailableVersionFilePath(GetHighestAvailableVersionDirectory()); - - if (highestAvailableVersion == null) - { - if (this.fileSystem.FileExists(highestAvailableVersionFile)) - { - this.fileSystem.DeleteFile(highestAvailableVersionFile); - - if (this.tracer != null) - { - this.tracer.RelatedInfo($"{nameof(this.RecordHighestAvailableVersion)}: Deleted upgrade reminder marker file"); - } - } - } - else - { - this.fileSystem.WriteAllText(highestAvailableVersionFile, highestAvailableVersion.ToString()); - - if (this.tracer != null) - { - this.tracer.RelatedInfo($"{nameof(this.RecordHighestAvailableVersion)}: Created upgrade reminder marker file"); - } - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; + +namespace Scalar.Common +{ + public partial class ProductUpgraderInfo + { + private ITracer tracer; + private PhysicalFileSystem fileSystem; + + public ProductUpgraderInfo(ITracer tracer, PhysicalFileSystem fileSystem) + { + this.tracer = tracer; + this.fileSystem = fileSystem; + } + + public static string CurrentScalarVersion() + { + return ProcessHelper.GetCurrentProcessVersion(); + } + + public static string GetUpgradeProtectedDataDirectory() + { + return ScalarPlatform.Instance.GetUpgradeProtectedDataDirectory(); + } + + public static string GetUpgradeApplicationDirectory() + { + return Path.Combine( + GetUpgradeProtectedDataDirectory(), + ProductUpgraderInfo.ApplicationDirectory); + } + + public static string GetParentLogDirectoryPath() + { + return ScalarPlatform.Instance.GetUpgradeLogDirectoryParentDirectory(); + } + + public static string GetLogDirectoryPath() + { + return Path.Combine( + ScalarPlatform.Instance.GetUpgradeLogDirectoryParentDirectory(), + ProductUpgraderInfo.LogDirectory); + } + + public static string GetAssetDownloadsPath() + { + return Path.Combine( + ScalarPlatform.Instance.GetUpgradeProtectedDataDirectory(), + ProductUpgraderInfo.DownloadDirectory); + } + + public static string GetHighestAvailableVersionDirectory() + { + return ScalarPlatform.Instance.GetUpgradeHighestAvailableVersionDirectory(); + } + + public void DeleteAllInstallerDownloads() + { + try + { + this.fileSystem.DeleteDirectory(GetAssetDownloadsPath()); + } + catch (Exception ex) + { + if (this.tracer != null) + { + this.tracer.RelatedError($"{nameof(this.DeleteAllInstallerDownloads)}: Could not remove directory: {ProductUpgraderInfo.GetAssetDownloadsPath()}.{ex.ToString()}"); + } + } + } + + public void RecordHighestAvailableVersion(Version highestAvailableVersion) + { + string highestAvailableVersionFile = GetHighestAvailableVersionFilePath(GetHighestAvailableVersionDirectory()); + + if (highestAvailableVersion == null) + { + if (this.fileSystem.FileExists(highestAvailableVersionFile)) + { + this.fileSystem.DeleteFile(highestAvailableVersionFile); + + if (this.tracer != null) + { + this.tracer.RelatedInfo($"{nameof(this.RecordHighestAvailableVersion)}: Deleted upgrade reminder marker file"); + } + } + } + else + { + this.fileSystem.WriteAllText(highestAvailableVersionFile, highestAvailableVersion.ToString()); + + if (this.tracer != null) + { + this.tracer.RelatedInfo($"{nameof(this.RecordHighestAvailableVersion)}: Created upgrade reminder marker file"); + } + } + } + } +} diff --git a/Scalar.Common/ProductUpgraderPlatformStrategy.cs b/Scalar.Common/ProductUpgraderPlatformStrategy.cs index b925c06b61..3685f68c5b 100644 --- a/Scalar.Common/ProductUpgraderPlatformStrategy.cs +++ b/Scalar.Common/ProductUpgraderPlatformStrategy.cs @@ -1,38 +1,38 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; - -namespace Scalar.Common -{ - public abstract class ProductUpgraderPlatformStrategy - { - public ProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) - { - this.FileSystem = fileSystem; - this.Tracer = tracer; - } - - protected PhysicalFileSystem FileSystem { get; } - protected ITracer Tracer { get; } - - public abstract bool TryPrepareLogDirectory(out string error); - - public abstract bool TryPrepareApplicationDirectory(out string error); - - public abstract bool TryPrepareDownloadDirectory(out string error); - - protected void TraceException(Exception exception, string method, string message) - { - this.TraceException(this.Tracer, exception, method, message); - } - - protected void TraceException(ITracer tracer, Exception exception, string method, string message) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Method", method); - metadata.Add("Exception", exception.ToString()); - tracer.RelatedError(metadata, message); - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; + +namespace Scalar.Common +{ + public abstract class ProductUpgraderPlatformStrategy + { + public ProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) + { + this.FileSystem = fileSystem; + this.Tracer = tracer; + } + + protected PhysicalFileSystem FileSystem { get; } + protected ITracer Tracer { get; } + + public abstract bool TryPrepareLogDirectory(out string error); + + public abstract bool TryPrepareApplicationDirectory(out string error); + + public abstract bool TryPrepareDownloadDirectory(out string error); + + protected void TraceException(Exception exception, string method, string message) + { + this.TraceException(this.Tracer, exception, method, message); + } + + protected void TraceException(ITracer tracer, Exception exception, string method, string message) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Method", method); + metadata.Add("Exception", exception.ToString()); + tracer.RelatedError(metadata, message); + } + } +} diff --git a/Scalar.Common/RepoMetadata.cs b/Scalar.Common/RepoMetadata.cs index 5784ea6994..1af3999e7b 100644 --- a/Scalar.Common/RepoMetadata.cs +++ b/Scalar.Common/RepoMetadata.cs @@ -1,306 +1,306 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.Common -{ - public class RepoMetadata - { - private FileBasedDictionary repoMetadata; - private ITracer tracer; - - private RepoMetadata(ITracer tracer) - { - this.tracer = tracer; - } - - public static RepoMetadata Instance { get; private set; } - - public string EnlistmentId - { - get - { - string value; - if (!this.repoMetadata.TryGetValue(Keys.EnlistmentId, out value)) - { - value = CreateNewEnlistmentId(this.tracer); - this.repoMetadata.SetValueAndFlush(Keys.EnlistmentId, value); - } - - return value; - } - } - - public string DataFilePath - { - get { return this.repoMetadata.DataFilePath; } - } - - public static bool TryInitialize(ITracer tracer, string dotScalarPath, out string error) - { - return TryInitialize(tracer, new PhysicalFileSystem(), dotScalarPath, out error); - } - - public static bool TryInitialize(ITracer tracer, PhysicalFileSystem fileSystem, string dotScalarPath, out string error) - { - string dictionaryPath = Path.Combine(dotScalarPath, ScalarConstants.DotScalar.Databases.RepoMetadata); - if (Instance != null) - { - if (!Instance.repoMetadata.DataFilePath.Equals(dictionaryPath, StringComparison.OrdinalIgnoreCase)) - { - throw new InvalidOperationException( - string.Format( - "TryInitialize should never be called twice with different parameters. Expected: '{0}' Actual: '{1}'", - Instance.repoMetadata.DataFilePath, - dictionaryPath)); - } - } - else - { - Instance = new RepoMetadata(tracer); - if (!FileBasedDictionary.TryCreate( - tracer, - dictionaryPath, - fileSystem, - out Instance.repoMetadata, - out error)) - { - return false; - } - } - - error = null; - return true; - } - - public static void Shutdown() - { - if (Instance != null) - { - if (Instance.repoMetadata != null) - { - Instance.repoMetadata.Dispose(); - Instance.repoMetadata = null; - } - - Instance = null; - } - } - - public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion, out string error) - { - majorVersion = 0; - minorVersion = 0; - - try - { - string value; - if (!this.repoMetadata.TryGetValue(Keys.DiskLayoutMajorVersion, out value)) - { - error = "Enlistment disk layout version not found, check if a breaking change has been made to Scalar since cloning this enlistment."; - return false; - } - - if (!int.TryParse(value, out majorVersion)) - { - error = "Failed to parse persisted disk layout version number: " + value; - return false; - } - - // The minor version is optional, e.g. it could be missing during an upgrade - if (this.repoMetadata.TryGetValue(Keys.DiskLayoutMinorVersion, out value)) - { - if (!int.TryParse(value, out minorVersion)) - { - minorVersion = 0; - } - } - } - catch (FileBasedCollectionException ex) - { - error = ex.Message; - return false; - } - - error = null; - return true; - } - - public void SaveCloneMetadata(ITracer tracer, ScalarEnlistment enlistment) - { - this.repoMetadata.SetValuesAndFlush( - new[] - { - new KeyValuePair(Keys.DiskLayoutMajorVersion, ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion.ToString()), - new KeyValuePair(Keys.DiskLayoutMinorVersion, ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion.ToString()), - new KeyValuePair(Keys.GitObjectsRoot, enlistment.GitObjectsRoot), - new KeyValuePair(Keys.LocalCacheRoot, enlistment.LocalCacheRoot), - new KeyValuePair(Keys.BlobSizesRoot, enlistment.BlobSizesRoot), - new KeyValuePair(Keys.EnlistmentId, CreateNewEnlistmentId(tracer)), - }); - } - - public void SetProjectionInvalid(bool invalid) - { - this.SetInvalid(Keys.ProjectionInvalid, invalid); - } - - public bool GetProjectionInvalid() - { - return this.HasEntry(Keys.ProjectionInvalid); - } - - public void SetPlaceholdersNeedUpdate(bool needUpdate) - { - this.SetInvalid(Keys.PlaceholdersNeedUpdate, needUpdate); - } - - public bool GetPlaceholdersNeedUpdate() - { - return this.HasEntry(Keys.PlaceholdersNeedUpdate); - } - - public void SetProjectionInvalidAndPlaceholdersNeedUpdate() - { - this.repoMetadata.SetValuesAndFlush( - new[] - { - new KeyValuePair(Keys.ProjectionInvalid, bool.TrueString), - new KeyValuePair(Keys.PlaceholdersNeedUpdate, bool.TrueString) - }); - } - - public bool TryGetGitObjectsRoot(out string gitObjectsRoot, out string error) - { - gitObjectsRoot = null; - - try - { - if (!this.repoMetadata.TryGetValue(Keys.GitObjectsRoot, out gitObjectsRoot)) - { - error = "Git objects root not found"; - return false; - } - } - catch (FileBasedCollectionException ex) - { - error = ex.Message; - return false; - } - - error = null; - return true; - } - - public void SetGitObjectsRoot(string gitObjectsRoot) - { - this.repoMetadata.SetValueAndFlush(Keys.GitObjectsRoot, gitObjectsRoot); - } - - public bool TryGetLocalCacheRoot(out string localCacheRoot, out string error) - { - localCacheRoot = null; - - try - { - if (!this.repoMetadata.TryGetValue(Keys.LocalCacheRoot, out localCacheRoot)) - { - error = "Local cache root not found"; - return false; - } - } - catch (FileBasedCollectionException ex) - { - error = ex.Message; - return false; - } - - error = null; - return true; - } - - public void SetLocalCacheRoot(string localCacheRoot) - { - this.repoMetadata.SetValueAndFlush(Keys.LocalCacheRoot, localCacheRoot); - } - - public bool TryGetBlobSizesRoot(out string blobSizesRoot, out string error) - { - blobSizesRoot = null; - - try - { - if (!this.repoMetadata.TryGetValue(Keys.BlobSizesRoot, out blobSizesRoot)) - { - error = "Blob sizes root not found"; - return false; - } - } - catch (FileBasedCollectionException ex) - { - error = ex.Message; - return false; - } - - error = null; - return true; - } - - public void SetBlobSizesRoot(string blobSizesRoot) - { - this.repoMetadata.SetValueAndFlush(Keys.BlobSizesRoot, blobSizesRoot); - } - - public void SetEntry(string keyName, string valueName) - { - this.repoMetadata.SetValueAndFlush(keyName, valueName); - } - - private static string CreateNewEnlistmentId(ITracer tracer) - { - string enlistmentId = Guid.NewGuid().ToString("N"); - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(enlistmentId), enlistmentId); - tracer.RelatedEvent(EventLevel.Informational, nameof(CreateNewEnlistmentId), metadata); - return enlistmentId; - } - - private void SetInvalid(string keyName, bool invalid) - { - if (invalid) - { - this.repoMetadata.SetValueAndFlush(keyName, bool.TrueString); - } - else - { - this.repoMetadata.RemoveAndFlush(keyName); - } - } - - private bool HasEntry(string keyName) - { - string value; - if (this.repoMetadata.TryGetValue(keyName, out value)) - { - return true; - } - - return false; - } - - public static class Keys - { - public const string ProjectionInvalid = "ProjectionInvalid"; - public const string PlaceholdersInvalid = "PlaceholdersInvalid"; - public const string DiskLayoutMajorVersion = "DiskLayoutVersion"; - public const string DiskLayoutMinorVersion = "DiskLayoutMinorVersion"; - public const string PlaceholdersNeedUpdate = "PlaceholdersNeedUpdate"; - public const string GitObjectsRoot = "GitObjectsRoot"; - public const string LocalCacheRoot = "LocalCacheRoot"; - public const string BlobSizesRoot = "BlobSizesRoot"; - public const string EnlistmentId = "EnlistmentId"; - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.Common +{ + public class RepoMetadata + { + private FileBasedDictionary repoMetadata; + private ITracer tracer; + + private RepoMetadata(ITracer tracer) + { + this.tracer = tracer; + } + + public static RepoMetadata Instance { get; private set; } + + public string EnlistmentId + { + get + { + string value; + if (!this.repoMetadata.TryGetValue(Keys.EnlistmentId, out value)) + { + value = CreateNewEnlistmentId(this.tracer); + this.repoMetadata.SetValueAndFlush(Keys.EnlistmentId, value); + } + + return value; + } + } + + public string DataFilePath + { + get { return this.repoMetadata.DataFilePath; } + } + + public static bool TryInitialize(ITracer tracer, string dotScalarPath, out string error) + { + return TryInitialize(tracer, new PhysicalFileSystem(), dotScalarPath, out error); + } + + public static bool TryInitialize(ITracer tracer, PhysicalFileSystem fileSystem, string dotScalarPath, out string error) + { + string dictionaryPath = Path.Combine(dotScalarPath, ScalarConstants.DotScalar.Databases.RepoMetadata); + if (Instance != null) + { + if (!Instance.repoMetadata.DataFilePath.Equals(dictionaryPath, StringComparison.OrdinalIgnoreCase)) + { + throw new InvalidOperationException( + string.Format( + "TryInitialize should never be called twice with different parameters. Expected: '{0}' Actual: '{1}'", + Instance.repoMetadata.DataFilePath, + dictionaryPath)); + } + } + else + { + Instance = new RepoMetadata(tracer); + if (!FileBasedDictionary.TryCreate( + tracer, + dictionaryPath, + fileSystem, + out Instance.repoMetadata, + out error)) + { + return false; + } + } + + error = null; + return true; + } + + public static void Shutdown() + { + if (Instance != null) + { + if (Instance.repoMetadata != null) + { + Instance.repoMetadata.Dispose(); + Instance.repoMetadata = null; + } + + Instance = null; + } + } + + public bool TryGetOnDiskLayoutVersion(out int majorVersion, out int minorVersion, out string error) + { + majorVersion = 0; + minorVersion = 0; + + try + { + string value; + if (!this.repoMetadata.TryGetValue(Keys.DiskLayoutMajorVersion, out value)) + { + error = "Enlistment disk layout version not found, check if a breaking change has been made to Scalar since cloning this enlistment."; + return false; + } + + if (!int.TryParse(value, out majorVersion)) + { + error = "Failed to parse persisted disk layout version number: " + value; + return false; + } + + // The minor version is optional, e.g. it could be missing during an upgrade + if (this.repoMetadata.TryGetValue(Keys.DiskLayoutMinorVersion, out value)) + { + if (!int.TryParse(value, out minorVersion)) + { + minorVersion = 0; + } + } + } + catch (FileBasedCollectionException ex) + { + error = ex.Message; + return false; + } + + error = null; + return true; + } + + public void SaveCloneMetadata(ITracer tracer, ScalarEnlistment enlistment) + { + this.repoMetadata.SetValuesAndFlush( + new[] + { + new KeyValuePair(Keys.DiskLayoutMajorVersion, ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion.ToString()), + new KeyValuePair(Keys.DiskLayoutMinorVersion, ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion.ToString()), + new KeyValuePair(Keys.GitObjectsRoot, enlistment.GitObjectsRoot), + new KeyValuePair(Keys.LocalCacheRoot, enlistment.LocalCacheRoot), + new KeyValuePair(Keys.BlobSizesRoot, enlistment.BlobSizesRoot), + new KeyValuePair(Keys.EnlistmentId, CreateNewEnlistmentId(tracer)), + }); + } + + public void SetProjectionInvalid(bool invalid) + { + this.SetInvalid(Keys.ProjectionInvalid, invalid); + } + + public bool GetProjectionInvalid() + { + return this.HasEntry(Keys.ProjectionInvalid); + } + + public void SetPlaceholdersNeedUpdate(bool needUpdate) + { + this.SetInvalid(Keys.PlaceholdersNeedUpdate, needUpdate); + } + + public bool GetPlaceholdersNeedUpdate() + { + return this.HasEntry(Keys.PlaceholdersNeedUpdate); + } + + public void SetProjectionInvalidAndPlaceholdersNeedUpdate() + { + this.repoMetadata.SetValuesAndFlush( + new[] + { + new KeyValuePair(Keys.ProjectionInvalid, bool.TrueString), + new KeyValuePair(Keys.PlaceholdersNeedUpdate, bool.TrueString) + }); + } + + public bool TryGetGitObjectsRoot(out string gitObjectsRoot, out string error) + { + gitObjectsRoot = null; + + try + { + if (!this.repoMetadata.TryGetValue(Keys.GitObjectsRoot, out gitObjectsRoot)) + { + error = "Git objects root not found"; + return false; + } + } + catch (FileBasedCollectionException ex) + { + error = ex.Message; + return false; + } + + error = null; + return true; + } + + public void SetGitObjectsRoot(string gitObjectsRoot) + { + this.repoMetadata.SetValueAndFlush(Keys.GitObjectsRoot, gitObjectsRoot); + } + + public bool TryGetLocalCacheRoot(out string localCacheRoot, out string error) + { + localCacheRoot = null; + + try + { + if (!this.repoMetadata.TryGetValue(Keys.LocalCacheRoot, out localCacheRoot)) + { + error = "Local cache root not found"; + return false; + } + } + catch (FileBasedCollectionException ex) + { + error = ex.Message; + return false; + } + + error = null; + return true; + } + + public void SetLocalCacheRoot(string localCacheRoot) + { + this.repoMetadata.SetValueAndFlush(Keys.LocalCacheRoot, localCacheRoot); + } + + public bool TryGetBlobSizesRoot(out string blobSizesRoot, out string error) + { + blobSizesRoot = null; + + try + { + if (!this.repoMetadata.TryGetValue(Keys.BlobSizesRoot, out blobSizesRoot)) + { + error = "Blob sizes root not found"; + return false; + } + } + catch (FileBasedCollectionException ex) + { + error = ex.Message; + return false; + } + + error = null; + return true; + } + + public void SetBlobSizesRoot(string blobSizesRoot) + { + this.repoMetadata.SetValueAndFlush(Keys.BlobSizesRoot, blobSizesRoot); + } + + public void SetEntry(string keyName, string valueName) + { + this.repoMetadata.SetValueAndFlush(keyName, valueName); + } + + private static string CreateNewEnlistmentId(ITracer tracer) + { + string enlistmentId = Guid.NewGuid().ToString("N"); + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(enlistmentId), enlistmentId); + tracer.RelatedEvent(EventLevel.Informational, nameof(CreateNewEnlistmentId), metadata); + return enlistmentId; + } + + private void SetInvalid(string keyName, bool invalid) + { + if (invalid) + { + this.repoMetadata.SetValueAndFlush(keyName, bool.TrueString); + } + else + { + this.repoMetadata.RemoveAndFlush(keyName); + } + } + + private bool HasEntry(string keyName) + { + string value; + if (this.repoMetadata.TryGetValue(keyName, out value)) + { + return true; + } + + return false; + } + + public static class Keys + { + public const string ProjectionInvalid = "ProjectionInvalid"; + public const string PlaceholdersInvalid = "PlaceholdersInvalid"; + public const string DiskLayoutMajorVersion = "DiskLayoutVersion"; + public const string DiskLayoutMinorVersion = "DiskLayoutMinorVersion"; + public const string PlaceholdersNeedUpdate = "PlaceholdersNeedUpdate"; + public const string GitObjectsRoot = "GitObjectsRoot"; + public const string LocalCacheRoot = "LocalCacheRoot"; + public const string BlobSizesRoot = "BlobSizesRoot"; + public const string EnlistmentId = "EnlistmentId"; + } + } +} diff --git a/Scalar.Common/RetryBackoff.cs b/Scalar.Common/RetryBackoff.cs index 7e6dfe079c..2afd292ad8 100644 --- a/Scalar.Common/RetryBackoff.cs +++ b/Scalar.Common/RetryBackoff.cs @@ -1,51 +1,51 @@ -using System; - -namespace Scalar.Common -{ - public static class RetryBackoff - { - public const double DefaultExponentialBackoffBase = 2; - - [ThreadStatic] - private static Random threadLocalRandom; - - private static Random ThreadLocalRandom - { - get - { - if (threadLocalRandom == null) - { - threadLocalRandom = new Random(); - } - - return threadLocalRandom; - } - } - - /// - /// Computes the next backoff value in seconds. - /// - /// - /// Current failed attempt using 1-based counting. (i.e. currentFailedAttempt should be 1 if the first attempt just failed - /// - /// Maximum allowed backoff - /// Time to backoff in seconds - /// Computed backoff is randomly adjusted by +- 10% to help prevent clients from hitting servers at the same time - public static double CalculateBackoffSeconds(int currentFailedAttempt, double maxBackoffSeconds, double exponentialBackoffBase = DefaultExponentialBackoffBase) - { - if (currentFailedAttempt <= 1) - { - return 0; - } - - // Exponential backoff - double backOffSeconds = Math.Min(Math.Pow(exponentialBackoffBase, currentFailedAttempt), maxBackoffSeconds); - - // Timeout usually happens when the server is overloaded. If we give all machines the same timeout they will all make - // another request at approximately the same time causing the problem to happen again and again. To avoid that we - // introduce a random timeout. To avoid scaling it too high or too low, it is +- 10% of the average backoff - backOffSeconds *= .9 + (ThreadLocalRandom.NextDouble() * .2); - return backOffSeconds; - } - } -} +using System; + +namespace Scalar.Common +{ + public static class RetryBackoff + { + public const double DefaultExponentialBackoffBase = 2; + + [ThreadStatic] + private static Random threadLocalRandom; + + private static Random ThreadLocalRandom + { + get + { + if (threadLocalRandom == null) + { + threadLocalRandom = new Random(); + } + + return threadLocalRandom; + } + } + + /// + /// Computes the next backoff value in seconds. + /// + /// + /// Current failed attempt using 1-based counting. (i.e. currentFailedAttempt should be 1 if the first attempt just failed + /// + /// Maximum allowed backoff + /// Time to backoff in seconds + /// Computed backoff is randomly adjusted by +- 10% to help prevent clients from hitting servers at the same time + public static double CalculateBackoffSeconds(int currentFailedAttempt, double maxBackoffSeconds, double exponentialBackoffBase = DefaultExponentialBackoffBase) + { + if (currentFailedAttempt <= 1) + { + return 0; + } + + // Exponential backoff + double backOffSeconds = Math.Min(Math.Pow(exponentialBackoffBase, currentFailedAttempt), maxBackoffSeconds); + + // Timeout usually happens when the server is overloaded. If we give all machines the same timeout they will all make + // another request at approximately the same time causing the problem to happen again and again. To avoid that we + // introduce a random timeout. To avoid scaling it too high or too low, it is +- 10% of the average backoff + backOffSeconds *= .9 + (ThreadLocalRandom.NextDouble() * .2); + return backOffSeconds; + } + } +} diff --git a/Scalar.Common/RetryConfig.cs b/Scalar.Common/RetryConfig.cs index 90637f8fb8..1c1dea044f 100644 --- a/Scalar.Common/RetryConfig.cs +++ b/Scalar.Common/RetryConfig.cs @@ -1,138 +1,138 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Linq; - -namespace Scalar.Common -{ - public class RetryConfig - { - public const int DefaultMaxRetries = 6; - public const int DefaultTimeoutSeconds = 30; - public const int FetchAndCloneTimeoutMinutes = 10; - - private const string EtwArea = nameof(RetryConfig); - - private const int MinRetries = 0; - - private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(DefaultTimeoutSeconds); - - public RetryConfig(int maxRetries = DefaultMaxRetries) - : this(maxRetries, DefaultTimeout) - { - } - - public RetryConfig(int maxRetries, TimeSpan timeout) - { - this.MaxRetries = maxRetries; - this.Timeout = timeout; - } - - public int MaxRetries { get; } - public int MaxAttempts - { - get { return this.MaxRetries + 1; } - } - - public TimeSpan Timeout { get; set; } - - public static bool TryLoadFromGitConfig(ITracer tracer, Enlistment enlistment, out RetryConfig retryConfig, out string error) - { - return TryLoadFromGitConfig(tracer, new GitProcess(enlistment), out retryConfig, out error); - } - - public static bool TryLoadFromGitConfig(ITracer tracer, GitProcess git, out RetryConfig retryConfig, out string error) - { - retryConfig = null; - - int maxRetries; - if (!TryLoadMaxRetries(git, out maxRetries, out error)) - { - if (tracer != null) - { - tracer.RelatedError( - new EventMetadata - { - { "Area", EtwArea }, - { "error", error } - }, - "TryLoadConfig: TryLoadMaxRetries failed"); - } - - return false; - } - - TimeSpan timeout; - if (!TryLoadTimeout(git, out timeout, out error)) - { - if (tracer != null) - { - tracer.RelatedError( - new EventMetadata - { - { "Area", EtwArea }, - { "maxRetries", maxRetries }, - { "error", error } - }, - "TryLoadConfig: TryLoadTimeout failed"); - } - - return false; - } - - retryConfig = new RetryConfig(maxRetries, timeout); - - if (tracer != null) - { - tracer.RelatedEvent( - EventLevel.Informational, - "RetryConfig_LoadedRetryConfig", - new EventMetadata - { - { "Area", EtwArea }, - { "Timeout", retryConfig.Timeout }, - { "MaxRetries", retryConfig.MaxRetries }, - { TracingConstants.MessageKey.InfoMessage, "RetryConfigLoaded" } - }); - } - - return true; - } - - private static bool TryLoadMaxRetries(GitProcess git, out int attempts, out string error) - { - return TryGetFromGitConfig( - git, - ScalarConstants.GitConfig.MaxRetriesConfig, - DefaultMaxRetries, - MinRetries, - out attempts, - out error); - } - - private static bool TryLoadTimeout(GitProcess git, out TimeSpan timeout, out string error) - { - timeout = TimeSpan.FromSeconds(0); - int timeoutSeconds; - if (!TryGetFromGitConfig( - git, - ScalarConstants.GitConfig.TimeoutSecondsConfig, - DefaultTimeoutSeconds, - 0, - out timeoutSeconds, - out error)) - { - return false; - } - - timeout = TimeSpan.FromSeconds(timeoutSeconds); - return true; - } - - private static bool TryGetFromGitConfig(GitProcess git, string configName, int defaultValue, int minValue, out int value, out string error) - { - GitProcess.ConfigResult result = git.GetFromConfig(configName); - return result.TryParseAsInt(defaultValue, minValue, out value, out error); - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Linq; + +namespace Scalar.Common +{ + public class RetryConfig + { + public const int DefaultMaxRetries = 6; + public const int DefaultTimeoutSeconds = 30; + public const int FetchAndCloneTimeoutMinutes = 10; + + private const string EtwArea = nameof(RetryConfig); + + private const int MinRetries = 0; + + private static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(DefaultTimeoutSeconds); + + public RetryConfig(int maxRetries = DefaultMaxRetries) + : this(maxRetries, DefaultTimeout) + { + } + + public RetryConfig(int maxRetries, TimeSpan timeout) + { + this.MaxRetries = maxRetries; + this.Timeout = timeout; + } + + public int MaxRetries { get; } + public int MaxAttempts + { + get { return this.MaxRetries + 1; } + } + + public TimeSpan Timeout { get; set; } + + public static bool TryLoadFromGitConfig(ITracer tracer, Enlistment enlistment, out RetryConfig retryConfig, out string error) + { + return TryLoadFromGitConfig(tracer, new GitProcess(enlistment), out retryConfig, out error); + } + + public static bool TryLoadFromGitConfig(ITracer tracer, GitProcess git, out RetryConfig retryConfig, out string error) + { + retryConfig = null; + + int maxRetries; + if (!TryLoadMaxRetries(git, out maxRetries, out error)) + { + if (tracer != null) + { + tracer.RelatedError( + new EventMetadata + { + { "Area", EtwArea }, + { "error", error } + }, + "TryLoadConfig: TryLoadMaxRetries failed"); + } + + return false; + } + + TimeSpan timeout; + if (!TryLoadTimeout(git, out timeout, out error)) + { + if (tracer != null) + { + tracer.RelatedError( + new EventMetadata + { + { "Area", EtwArea }, + { "maxRetries", maxRetries }, + { "error", error } + }, + "TryLoadConfig: TryLoadTimeout failed"); + } + + return false; + } + + retryConfig = new RetryConfig(maxRetries, timeout); + + if (tracer != null) + { + tracer.RelatedEvent( + EventLevel.Informational, + "RetryConfig_LoadedRetryConfig", + new EventMetadata + { + { "Area", EtwArea }, + { "Timeout", retryConfig.Timeout }, + { "MaxRetries", retryConfig.MaxRetries }, + { TracingConstants.MessageKey.InfoMessage, "RetryConfigLoaded" } + }); + } + + return true; + } + + private static bool TryLoadMaxRetries(GitProcess git, out int attempts, out string error) + { + return TryGetFromGitConfig( + git, + ScalarConstants.GitConfig.MaxRetriesConfig, + DefaultMaxRetries, + MinRetries, + out attempts, + out error); + } + + private static bool TryLoadTimeout(GitProcess git, out TimeSpan timeout, out string error) + { + timeout = TimeSpan.FromSeconds(0); + int timeoutSeconds; + if (!TryGetFromGitConfig( + git, + ScalarConstants.GitConfig.TimeoutSecondsConfig, + DefaultTimeoutSeconds, + 0, + out timeoutSeconds, + out error)) + { + return false; + } + + timeout = TimeSpan.FromSeconds(timeoutSeconds); + return true; + } + + private static bool TryGetFromGitConfig(GitProcess git, string configName, int defaultValue, int minValue, out int value, out string error) + { + GitProcess.ConfigResult result = git.GetFromConfig(configName); + return result.TryParseAsInt(defaultValue, minValue, out value, out error); + } + } +} diff --git a/Scalar.Common/RetryWrapper.cs b/Scalar.Common/RetryWrapper.cs index 036404f823..6b1c41815c 100644 --- a/Scalar.Common/RetryWrapper.cs +++ b/Scalar.Common/RetryWrapper.cs @@ -1,216 +1,216 @@ -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.Common -{ - public class RetryWrapper - { - private const float MaxBackoffInSeconds = 300; // 5 minutes - private readonly int maxAttempts; - private readonly double exponentialBackoffBase; - private readonly CancellationToken cancellationToken; - - public RetryWrapper(int maxAttempts, CancellationToken cancellationToken, double exponentialBackoffBase = RetryBackoff.DefaultExponentialBackoffBase) - { - this.maxAttempts = maxAttempts; - this.cancellationToken = cancellationToken; - this.exponentialBackoffBase = exponentialBackoffBase; - } - - public event Action OnFailure = delegate { }; - - public static Action StandardErrorHandler(ITracer tracer, long requestId, string actionName, bool forceLogAsWarning = false) - { - return eArgs => - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("RequestId", requestId); - metadata.Add("AttemptNumber", eArgs.TryCount); - metadata.Add("Operation", actionName); - metadata.Add("WillRetry", eArgs.WillRetry); - string message = null; - if (eArgs.Error != null) - { - message = eArgs.Error.Message; - metadata.Add("Exception", eArgs.Error.ToString()); - - int innerCounter = 1; - Exception e = eArgs.Error.InnerException; - while (e != null) - { - metadata.Add("InnerException" + innerCounter++, e.ToString()); - e = e.InnerException; - } - } - - if (eArgs.WillRetry || forceLogAsWarning) - { - tracer.RelatedWarning(metadata, message, Keywords.Network); - } - else - { - tracer.RelatedError(metadata, message, Keywords.Network); - } - }; - } - - public InvocationResult Invoke(Func toInvoke) - { - // Use 1-based counting. This makes reporting look a lot nicer and saves a lot of +1s - for (int tryCount = 1; tryCount <= this.maxAttempts; ++tryCount) - { - this.cancellationToken.ThrowIfCancellationRequested(); - - try - { - CallbackResult result = toInvoke(tryCount); - if (result.HasErrors) - { - if (!this.ShouldRetry(tryCount, null, result)) - { - return new InvocationResult(tryCount, result.Error, result.Result); - } - } - else - { - return new InvocationResult(tryCount, true, result.Result); - } - } - catch (Exception e) - { - Exception exceptionToReport = - e is AggregateException - ? ((AggregateException)e).Flatten().InnerException - : e; - - if (!this.IsHandlableException(exceptionToReport)) - { - throw; - } - - if (!this.ShouldRetry(tryCount, exceptionToReport, null)) - { - return new InvocationResult(tryCount, exceptionToReport); - } - } - - // Don't wait for the first retry, since it might just be transient. - // Don't wait after the last try. tryCount is 1-based, so last attempt is tryCount == maxAttempts - if (tryCount > 1 && tryCount < this.maxAttempts) - { - double backOffSeconds = RetryBackoff.CalculateBackoffSeconds(tryCount, MaxBackoffInSeconds, this.exponentialBackoffBase); - try - { - Task.Delay(TimeSpan.FromSeconds(backOffSeconds), this.cancellationToken).GetAwaiter().GetResult(); - } - catch (TaskCanceledException) - { - throw new OperationCanceledException(this.cancellationToken); - } - } - } - - // This shouldn't be hit because ShouldRetry will cause a more useful message first. - return new InvocationResult(this.maxAttempts, new Exception("Unexpected failure after retrying")); - } - - private bool IsHandlableException(Exception e) - { - return - e is HttpRequestException || - e is IOException || - e is RetryableException; - } - - private bool ShouldRetry(int tryCount, Exception e, CallbackResult result) - { - bool willRetry = tryCount < this.maxAttempts && - (result == null || result.ShouldRetry); - - if (e != null) - { - this.OnFailure(new ErrorEventArgs(e, tryCount, willRetry)); - } - else - { - this.OnFailure(new ErrorEventArgs(result.Error, tryCount, willRetry)); - } - - return willRetry; - } - - public class ErrorEventArgs - { - public ErrorEventArgs(Exception error, int tryCount, bool willRetry) - { - this.Error = error; - this.TryCount = tryCount; - this.WillRetry = willRetry; - } - - public bool WillRetry { get; } - - public int TryCount { get; } - - public Exception Error { get; } - } - - public class InvocationResult - { - public InvocationResult(int tryCount, bool succeeded, T result) - { - this.Attempts = tryCount; - this.Succeeded = true; - this.Result = result; - } - - public InvocationResult(int tryCount, Exception error) - { - this.Attempts = tryCount; - this.Succeeded = false; - this.Error = error; - } - - public InvocationResult(int tryCount, Exception error, T result) - : this(tryCount, error) - { - this.Result = result; - } - - public T Result { get; } - public int Attempts { get; } - public bool Succeeded { get; } - public Exception Error { get; } - } - - public class CallbackResult - { - public CallbackResult(T result) - { - this.Result = result; - } - - public CallbackResult(Exception error, bool shouldRetry) - { - this.HasErrors = true; - this.Error = error; - this.ShouldRetry = shouldRetry; - } - - public CallbackResult(Exception error, bool shouldRetry, T result) - : this(error, shouldRetry) - { - this.Result = result; - } - - public bool HasErrors { get; } - public Exception Error { get; } - public bool ShouldRetry { get; } - public T Result { get; } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.Common +{ + public class RetryWrapper + { + private const float MaxBackoffInSeconds = 300; // 5 minutes + private readonly int maxAttempts; + private readonly double exponentialBackoffBase; + private readonly CancellationToken cancellationToken; + + public RetryWrapper(int maxAttempts, CancellationToken cancellationToken, double exponentialBackoffBase = RetryBackoff.DefaultExponentialBackoffBase) + { + this.maxAttempts = maxAttempts; + this.cancellationToken = cancellationToken; + this.exponentialBackoffBase = exponentialBackoffBase; + } + + public event Action OnFailure = delegate { }; + + public static Action StandardErrorHandler(ITracer tracer, long requestId, string actionName, bool forceLogAsWarning = false) + { + return eArgs => + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("RequestId", requestId); + metadata.Add("AttemptNumber", eArgs.TryCount); + metadata.Add("Operation", actionName); + metadata.Add("WillRetry", eArgs.WillRetry); + string message = null; + if (eArgs.Error != null) + { + message = eArgs.Error.Message; + metadata.Add("Exception", eArgs.Error.ToString()); + + int innerCounter = 1; + Exception e = eArgs.Error.InnerException; + while (e != null) + { + metadata.Add("InnerException" + innerCounter++, e.ToString()); + e = e.InnerException; + } + } + + if (eArgs.WillRetry || forceLogAsWarning) + { + tracer.RelatedWarning(metadata, message, Keywords.Network); + } + else + { + tracer.RelatedError(metadata, message, Keywords.Network); + } + }; + } + + public InvocationResult Invoke(Func toInvoke) + { + // Use 1-based counting. This makes reporting look a lot nicer and saves a lot of +1s + for (int tryCount = 1; tryCount <= this.maxAttempts; ++tryCount) + { + this.cancellationToken.ThrowIfCancellationRequested(); + + try + { + CallbackResult result = toInvoke(tryCount); + if (result.HasErrors) + { + if (!this.ShouldRetry(tryCount, null, result)) + { + return new InvocationResult(tryCount, result.Error, result.Result); + } + } + else + { + return new InvocationResult(tryCount, true, result.Result); + } + } + catch (Exception e) + { + Exception exceptionToReport = + e is AggregateException + ? ((AggregateException)e).Flatten().InnerException + : e; + + if (!this.IsHandlableException(exceptionToReport)) + { + throw; + } + + if (!this.ShouldRetry(tryCount, exceptionToReport, null)) + { + return new InvocationResult(tryCount, exceptionToReport); + } + } + + // Don't wait for the first retry, since it might just be transient. + // Don't wait after the last try. tryCount is 1-based, so last attempt is tryCount == maxAttempts + if (tryCount > 1 && tryCount < this.maxAttempts) + { + double backOffSeconds = RetryBackoff.CalculateBackoffSeconds(tryCount, MaxBackoffInSeconds, this.exponentialBackoffBase); + try + { + Task.Delay(TimeSpan.FromSeconds(backOffSeconds), this.cancellationToken).GetAwaiter().GetResult(); + } + catch (TaskCanceledException) + { + throw new OperationCanceledException(this.cancellationToken); + } + } + } + + // This shouldn't be hit because ShouldRetry will cause a more useful message first. + return new InvocationResult(this.maxAttempts, new Exception("Unexpected failure after retrying")); + } + + private bool IsHandlableException(Exception e) + { + return + e is HttpRequestException || + e is IOException || + e is RetryableException; + } + + private bool ShouldRetry(int tryCount, Exception e, CallbackResult result) + { + bool willRetry = tryCount < this.maxAttempts && + (result == null || result.ShouldRetry); + + if (e != null) + { + this.OnFailure(new ErrorEventArgs(e, tryCount, willRetry)); + } + else + { + this.OnFailure(new ErrorEventArgs(result.Error, tryCount, willRetry)); + } + + return willRetry; + } + + public class ErrorEventArgs + { + public ErrorEventArgs(Exception error, int tryCount, bool willRetry) + { + this.Error = error; + this.TryCount = tryCount; + this.WillRetry = willRetry; + } + + public bool WillRetry { get; } + + public int TryCount { get; } + + public Exception Error { get; } + } + + public class InvocationResult + { + public InvocationResult(int tryCount, bool succeeded, T result) + { + this.Attempts = tryCount; + this.Succeeded = true; + this.Result = result; + } + + public InvocationResult(int tryCount, Exception error) + { + this.Attempts = tryCount; + this.Succeeded = false; + this.Error = error; + } + + public InvocationResult(int tryCount, Exception error, T result) + : this(tryCount, error) + { + this.Result = result; + } + + public T Result { get; } + public int Attempts { get; } + public bool Succeeded { get; } + public Exception Error { get; } + } + + public class CallbackResult + { + public CallbackResult(T result) + { + this.Result = result; + } + + public CallbackResult(Exception error, bool shouldRetry) + { + this.HasErrors = true; + this.Error = error; + this.ShouldRetry = shouldRetry; + } + + public CallbackResult(Exception error, bool shouldRetry, T result) + : this(error, shouldRetry) + { + this.Result = result; + } + + public bool HasErrors { get; } + public Exception Error { get; } + public bool ShouldRetry { get; } + public T Result { get; } + } + } +} diff --git a/Scalar.Common/RetryableException.cs b/Scalar.Common/RetryableException.cs index 531b111b17..aacedd114f 100644 --- a/Scalar.Common/RetryableException.cs +++ b/Scalar.Common/RetryableException.cs @@ -1,15 +1,15 @@ -using System; - -namespace Scalar.Common -{ - public class RetryableException : Exception - { - public RetryableException(string message, Exception inner) : base(message, inner) - { - } - - public RetryableException(string message) : base(message) - { - } - } -} +using System; + +namespace Scalar.Common +{ + public class RetryableException : Exception + { + public RetryableException(string message, Exception inner) : base(message, inner) + { + } + + public RetryableException(string message) : base(message) + { + } + } +} diff --git a/Scalar.Common/ReturnCode.cs b/Scalar.Common/ReturnCode.cs index 8f6cb4d27c..118e12d98c 100644 --- a/Scalar.Common/ReturnCode.cs +++ b/Scalar.Common/ReturnCode.cs @@ -1,12 +1,12 @@ -namespace Scalar.Common -{ - public enum ReturnCode - { - Success = 0, - ParsingError = 1, - RebootRequired = 2, - GenericError = 3, - FilterError = 4, - NullRequestData = 5 - } -} +namespace Scalar.Common +{ + public enum ReturnCode + { + Success = 0, + ParsingError = 1, + RebootRequired = 2, + GenericError = 3, + FilterError = 4, + NullRequestData = 5 + } +} diff --git a/Scalar.Common/SHA1Util.cs b/Scalar.Common/SHA1Util.cs index 73e141c709..1434682384 100644 --- a/Scalar.Common/SHA1Util.cs +++ b/Scalar.Common/SHA1Util.cs @@ -1,78 +1,78 @@ -using System; -using System.Linq; -using System.Security.Cryptography; -using System.Text; - -namespace Scalar.Common -{ - public static class SHA1Util - { - public static bool IsValidShaFormat(string sha) - { - return sha.Length == 40 && sha.All(c => Uri.IsHexDigit(c)); - } - - public static string SHA1HashStringForUTF8String(string s) - { - return HexStringFromBytes(SHA1ForUTF8String(s)); - } - - public static byte[] SHA1ForUTF8String(string s) - { - byte[] bytes = Encoding.UTF8.GetBytes(s); - - using (SHA1 sha1 = SHA1.Create()) - { - return sha1.ComputeHash(bytes); - } - } - - /// - /// Returns a string representation of a byte array from the first - /// bytes of the buffer. - /// - public static string HexStringFromBytes(byte[] buf, int numBytes = -1) - { - unsafe - { - numBytes = numBytes == -1 ? buf.Length : numBytes; - - fixed (byte* unsafeBuf = buf) - { - int charIndex = 0; - byte* currentByte = unsafeBuf; - char[] chars = new char[numBytes * 2]; - for (int i = 0; i < numBytes; i++) - { - char first = (char)(((*currentByte >> 4) & 0x0F) + 0x30); - char second = (char)((*currentByte & 0x0F) + 0x30); - chars[charIndex++] = first >= 0x3A ? (char)(first + 0x27) : first; - chars[charIndex++] = second >= 0x3A ? (char)(second + 0x27) : second; - - currentByte++; - } - - return new string(chars); - } - } - } - - public static byte[] BytesFromHexString(string sha) - { - byte[] arr = new byte[sha.Length / 2]; - - for (int i = 0; i < arr.Length; ++i) - { - arr[i] = (byte)((GetHexVal(sha[i << 1]) << 4) + GetHexVal(sha[(i << 1) + 1])); - } - - return arr; - } - - private static int GetHexVal(char hex) - { - int val = (int)hex; - return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); - } - } -} +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace Scalar.Common +{ + public static class SHA1Util + { + public static bool IsValidShaFormat(string sha) + { + return sha.Length == 40 && sha.All(c => Uri.IsHexDigit(c)); + } + + public static string SHA1HashStringForUTF8String(string s) + { + return HexStringFromBytes(SHA1ForUTF8String(s)); + } + + public static byte[] SHA1ForUTF8String(string s) + { + byte[] bytes = Encoding.UTF8.GetBytes(s); + + using (SHA1 sha1 = SHA1.Create()) + { + return sha1.ComputeHash(bytes); + } + } + + /// + /// Returns a string representation of a byte array from the first + /// bytes of the buffer. + /// + public static string HexStringFromBytes(byte[] buf, int numBytes = -1) + { + unsafe + { + numBytes = numBytes == -1 ? buf.Length : numBytes; + + fixed (byte* unsafeBuf = buf) + { + int charIndex = 0; + byte* currentByte = unsafeBuf; + char[] chars = new char[numBytes * 2]; + for (int i = 0; i < numBytes; i++) + { + char first = (char)(((*currentByte >> 4) & 0x0F) + 0x30); + char second = (char)((*currentByte & 0x0F) + 0x30); + chars[charIndex++] = first >= 0x3A ? (char)(first + 0x27) : first; + chars[charIndex++] = second >= 0x3A ? (char)(second + 0x27) : second; + + currentByte++; + } + + return new string(chars); + } + } + } + + public static byte[] BytesFromHexString(string sha) + { + byte[] arr = new byte[sha.Length / 2]; + + for (int i = 0; i < arr.Length; ++i) + { + arr[i] = (byte)((GetHexVal(sha[i << 1]) << 4) + GetHexVal(sha[(i << 1) + 1])); + } + + return arr; + } + + private static int GetHexVal(char hex) + { + int val = (int)hex; + return val - (val < 58 ? 48 : (val < 97 ? 55 : 87)); + } + } +} diff --git a/Scalar.Common/Scalar.Common.csproj b/Scalar.Common/Scalar.Common.csproj index 9eda18c197..4c7a9f6efc 100644 --- a/Scalar.Common/Scalar.Common.csproj +++ b/Scalar.Common/Scalar.Common.csproj @@ -1,36 +1,36 @@ - - - - - - - netcoreapp2.1;netstandard2.0 - x64 - true - true - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - - - - - - - - all - - - - - - - - + + + + + + + netcoreapp2.1;netstandard2.0 + x64 + true + true + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + + + + + + + + all + + + + + + + + diff --git a/Scalar.Common/ScalarConstants.cs b/Scalar.Common/ScalarConstants.cs index 3782d54b8a..55cbcdd299 100644 --- a/Scalar.Common/ScalarConstants.cs +++ b/Scalar.Common/ScalarConstants.cs @@ -1,239 +1,239 @@ -using System.IO; - -namespace Scalar.Common -{ - public static partial class ScalarConstants - { - public const int ShaStringLength = 40; - public const int MaxPath = 260; - public const string AllZeroSha = "0000000000000000000000000000000000000000"; - - public const char GitPathSeparator = '/'; - public const string GitPathSeparatorString = "/"; - public const char GitCommentSign = '#'; - - public const string PrefetchPackPrefix = "prefetch"; - - public const string ScalarEtwProviderName = "Microsoft.Git.Scalar"; - public const string WorkingDirectoryRootName = "src"; - public const string UnattendedEnvironmentVariable = "Scalar_UNATTENDED"; - - public const string DefaultScalarCacheFolderName = ".scalarCache"; - - public const string GitIsNotInstalledError = "Could not find git.exe. Ensure that Git is installed."; - - public static class GitConfig - { - public const string ScalarPrefix = "scalar."; - public const string MaxRetriesConfig = ScalarPrefix + "max-retries"; - public const string TimeoutSecondsConfig = ScalarPrefix + "timeout-seconds"; - public const string GitStatusCacheBackoffConfig = ScalarPrefix + "status-cache-backoff-seconds"; - public const string MountId = ScalarPrefix + "mount-id"; - public const string EnlistmentId = ScalarPrefix + "enlistment-id"; - public const string CacheServer = ScalarPrefix + "cache-server"; - public const string DeprecatedCacheEndpointSuffix = ".cache-server-url"; - public const string ScalarTelemetryId = GitConfig.ScalarPrefix + "telemetry-id"; - public const string ScalarTelemetryPipe = GitConfig.ScalarPrefix + "telemetry-pipe"; - public const string IKey = GitConfig.ScalarPrefix + "ikey"; - public const string HooksExtension = ".hooks"; - } - - public static class LocalScalarConfig - { - public const string UpgradeRing = "upgrade.ring"; - public const string UpgradeFeedPackageName = "upgrade.feedpackagename"; - public const string UpgradeFeedUrl = "upgrade.feedurl"; - public const string OrgInfoServerUrl = "upgrade.orgInfoServerUrl"; - } - - public static class Service - { - public const string ServiceName = "Scalar.Service"; - public const string LogDirectory = "Logs"; - public const string UIName = "Scalar.Service.UI"; - } - - public static class MediaTypes - { - public const string PrefetchPackFilesAndIndexesMediaType = "application/x-gvfs-timestamped-packfiles-indexes"; - public const string LooseObjectMediaType = "application/x-git-loose-object"; - public const string CustomLooseObjectsMediaType = "application/x-gvfs-loose-objects"; - public const string PackFileMediaType = "application/x-git-packfile"; - } - - public static class Endpoints - { - public const string ScalarConfig = "/gvfs/config"; - public const string ScalarObjects = "/gvfs/objects"; - public const string ScalarPrefetch = "/gvfs/prefetch"; - public const string ScalarSizes = "/gvfs/sizes"; - public const string InfoRefs = "/info/refs?service=git-upload-pack"; - } - - public static class SpecialGitFiles - { - public const string GitAttributes = ".gitattributes"; - public const string GitIgnore = ".gitignore"; - } - - public static class LogFileTypes - { - public const string MountPrefix = "mount"; - public const string UpgradePrefix = "productupgrade"; - - public const string Clone = "clone"; - public const string Dehydrate = "dehydrate"; - public const string MountVerb = MountPrefix + "_verb"; - public const string MountProcess = MountPrefix + "_process"; - public const string MountUpgrade = MountPrefix + "_repoupgrade"; - public const string Prefetch = "prefetch"; - public const string Repair = "repair"; - public const string Service = "service"; - public const string Sparse = "sparse"; - public const string UpgradeVerb = UpgradePrefix + "_verb"; - public const string UpgradeProcess = UpgradePrefix + "_process"; - } - - public static class DotScalar - { - public const string CorruptObjectsName = "CorruptObjects"; - public const string LogName = "logs"; - - public static class Databases - { - public const string Name = "databases"; - - public static readonly string BackgroundFileSystemTasks = Path.Combine(Name, "BackgroundGitOperations.dat"); - public static readonly string PlaceholderList = Path.Combine(Name, "PlaceholderList.dat"); - public static readonly string ModifiedPaths = Path.Combine(Name, "ModifiedPaths.dat"); - public static readonly string RepoMetadata = Path.Combine(Name, "RepoMetadata.dat"); - public static readonly string Scalar = Path.Combine(Name, "Scalar.sqlite"); - } - - public static class GitStatusCache - { - public const string Name = "gitStatusCache"; - public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat"); - } - } - - public static class DotGit - { - public const string Root = ".git"; - public const string HeadName = "HEAD"; - public const string IndexName = "index"; - public const string PackedRefsName = "packed-refs"; - public const string LockExtension = ".lock"; - - public static readonly string Config = Path.Combine(DotGit.Root, "config"); - public static readonly string Head = Path.Combine(DotGit.Root, HeadName); - public static readonly string BisectStart = Path.Combine(DotGit.Root, "BISECT_START"); - public static readonly string CherryPickHead = Path.Combine(DotGit.Root, "CHERRY_PICK_HEAD"); - public static readonly string MergeHead = Path.Combine(DotGit.Root, "MERGE_HEAD"); - public static readonly string RevertHead = Path.Combine(DotGit.Root, "REVERT_HEAD"); - public static readonly string RebaseApply = Path.Combine(DotGit.Root, "rebase_apply"); - public static readonly string Index = Path.Combine(DotGit.Root, IndexName); - public static readonly string IndexLock = Path.Combine(DotGit.Root, IndexName + LockExtension); - public static readonly string PackedRefs = Path.Combine(DotGit.Root, PackedRefsName); - public static readonly string Shallow = Path.Combine(DotGit.Root, "shallow"); - - public static class Logs - { - public static readonly string HeadName = "HEAD"; - - public static readonly string Root = Path.Combine(DotGit.Root, "logs"); - public static readonly string Head = Path.Combine(Logs.Root, Logs.HeadName); - } - - public static class Hooks - { - public const string ReadObjectName = "read-object"; - public static readonly string Root = Path.Combine(DotGit.Root, "hooks"); - public static readonly string ReadObjectPath = Path.Combine(Hooks.Root, ReadObjectName); - } - - public static class Info - { - public const string Name = "info"; - public const string ExcludeName = "exclude"; - public const string AlwaysExcludeName = "always_exclude"; - public const string SparseCheckoutName = "sparse-checkout"; - - public static readonly string Root = Path.Combine(DotGit.Root, Info.Name); - public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName); - public static readonly string ExcludePath = Path.Combine(Info.Root, ExcludeName); - public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName); - } - - public static class Objects - { - public static readonly string Root = Path.Combine(DotGit.Root, "objects"); - - public static class Info - { - public static readonly string Root = Path.Combine(Objects.Root, "info"); - public static readonly string Alternates = Path.Combine(Info.Root, "alternates"); - } - - public static class Pack - { - public static readonly string Name = "pack"; - public static readonly string Root = Path.Combine(Objects.Root, Name); - } - } - - public static class Refs - { - public static readonly string Root = Path.Combine(DotGit.Root, "refs"); - - public static class Heads - { - public static readonly string Root = Path.Combine(DotGit.Refs.Root, "heads"); - public static readonly string RootFolder = Heads.Root + Path.DirectorySeparatorChar; - } - } - } - - public static class InstallationCapabilityFiles - { - public const string OnDiskVersion16CapableInstallation = "OnDiskVersion16CapableInstallation.dat"; - } - - public static class VerbParameters - { - public const string InternalUseOnly = "internal_use_only"; - - public static class Mount - { - public const string StartedByService = "StartedByService"; - public const string StartedByVerb = "StartedByVerb"; - public const string Verbosity = "verbosity"; - public const string Keywords = "keywords"; - public const string DebugWindow = "debug-window"; - - public const string DefaultVerbosity = "Informational"; - public const string DefaultKeywords = "Any"; - } - - public static class Unmount - { - public const string SkipLock = "skip-wait-for-lock"; - } - } - - public static class UpgradeVerbMessages - { - public const string ScalarUpgrade = "`scalar upgrade`"; - public const string ScalarUpgradeConfirm = "`scalar upgrade --confirm`"; - public const string ScalarUpgradeDryRun = "`scalar upgrade --dry-run`"; - public const string NoUpgradeCheckPerformed = "No upgrade check was performed."; - public const string NoneRingConsoleAlert = "Upgrade ring set to \"None\". " + NoUpgradeCheckPerformed; - public const string NoRingConfigConsoleAlert = "Upgrade ring is not set. " + NoUpgradeCheckPerformed; - public const string InvalidRingConsoleAlert = "Upgrade ring set to unknown value. " + NoUpgradeCheckPerformed; - public const string SetUpgradeRingCommand = "To set or change upgrade ring, run `scalar config " + LocalScalarConfig.UpgradeRing + " [\"Fast\"|\"Slow\"|\"None\"]` from a command prompt."; - public const string ReminderNotification = "A new version of Scalar is available. Run " + UpgradeVerbMessages.ScalarUpgradeConfirm + " from an elevated command prompt to upgrade."; - public const string UnmountRepoWarning = "Upgrade will unmount and remount scalar repos, ensure you are at a stopping point."; - public const string UpgradeInstallAdvice = "When ready, run " + UpgradeVerbMessages.ScalarUpgradeConfirm + " from an elevated command prompt."; - } - } -} +using System.IO; + +namespace Scalar.Common +{ + public static partial class ScalarConstants + { + public const int ShaStringLength = 40; + public const int MaxPath = 260; + public const string AllZeroSha = "0000000000000000000000000000000000000000"; + + public const char GitPathSeparator = '/'; + public const string GitPathSeparatorString = "/"; + public const char GitCommentSign = '#'; + + public const string PrefetchPackPrefix = "prefetch"; + + public const string ScalarEtwProviderName = "Microsoft.Git.Scalar"; + public const string WorkingDirectoryRootName = "src"; + public const string UnattendedEnvironmentVariable = "Scalar_UNATTENDED"; + + public const string DefaultScalarCacheFolderName = ".scalarCache"; + + public const string GitIsNotInstalledError = "Could not find git.exe. Ensure that Git is installed."; + + public static class GitConfig + { + public const string ScalarPrefix = "scalar."; + public const string MaxRetriesConfig = ScalarPrefix + "max-retries"; + public const string TimeoutSecondsConfig = ScalarPrefix + "timeout-seconds"; + public const string GitStatusCacheBackoffConfig = ScalarPrefix + "status-cache-backoff-seconds"; + public const string MountId = ScalarPrefix + "mount-id"; + public const string EnlistmentId = ScalarPrefix + "enlistment-id"; + public const string CacheServer = ScalarPrefix + "cache-server"; + public const string DeprecatedCacheEndpointSuffix = ".cache-server-url"; + public const string ScalarTelemetryId = GitConfig.ScalarPrefix + "telemetry-id"; + public const string ScalarTelemetryPipe = GitConfig.ScalarPrefix + "telemetry-pipe"; + public const string IKey = GitConfig.ScalarPrefix + "ikey"; + public const string HooksExtension = ".hooks"; + } + + public static class LocalScalarConfig + { + public const string UpgradeRing = "upgrade.ring"; + public const string UpgradeFeedPackageName = "upgrade.feedpackagename"; + public const string UpgradeFeedUrl = "upgrade.feedurl"; + public const string OrgInfoServerUrl = "upgrade.orgInfoServerUrl"; + } + + public static class Service + { + public const string ServiceName = "Scalar.Service"; + public const string LogDirectory = "Logs"; + public const string UIName = "Scalar.Service.UI"; + } + + public static class MediaTypes + { + public const string PrefetchPackFilesAndIndexesMediaType = "application/x-gvfs-timestamped-packfiles-indexes"; + public const string LooseObjectMediaType = "application/x-git-loose-object"; + public const string CustomLooseObjectsMediaType = "application/x-gvfs-loose-objects"; + public const string PackFileMediaType = "application/x-git-packfile"; + } + + public static class Endpoints + { + public const string ScalarConfig = "/gvfs/config"; + public const string ScalarObjects = "/gvfs/objects"; + public const string ScalarPrefetch = "/gvfs/prefetch"; + public const string ScalarSizes = "/gvfs/sizes"; + public const string InfoRefs = "/info/refs?service=git-upload-pack"; + } + + public static class SpecialGitFiles + { + public const string GitAttributes = ".gitattributes"; + public const string GitIgnore = ".gitignore"; + } + + public static class LogFileTypes + { + public const string MountPrefix = "mount"; + public const string UpgradePrefix = "productupgrade"; + + public const string Clone = "clone"; + public const string Dehydrate = "dehydrate"; + public const string MountVerb = MountPrefix + "_verb"; + public const string MountProcess = MountPrefix + "_process"; + public const string MountUpgrade = MountPrefix + "_repoupgrade"; + public const string Prefetch = "prefetch"; + public const string Repair = "repair"; + public const string Service = "service"; + public const string Sparse = "sparse"; + public const string UpgradeVerb = UpgradePrefix + "_verb"; + public const string UpgradeProcess = UpgradePrefix + "_process"; + } + + public static class DotScalar + { + public const string CorruptObjectsName = "CorruptObjects"; + public const string LogName = "logs"; + + public static class Databases + { + public const string Name = "databases"; + + public static readonly string BackgroundFileSystemTasks = Path.Combine(Name, "BackgroundGitOperations.dat"); + public static readonly string PlaceholderList = Path.Combine(Name, "PlaceholderList.dat"); + public static readonly string ModifiedPaths = Path.Combine(Name, "ModifiedPaths.dat"); + public static readonly string RepoMetadata = Path.Combine(Name, "RepoMetadata.dat"); + public static readonly string Scalar = Path.Combine(Name, "Scalar.sqlite"); + } + + public static class GitStatusCache + { + public const string Name = "gitStatusCache"; + public static readonly string CachePath = Path.Combine(Name, "GitStatusCache.dat"); + } + } + + public static class DotGit + { + public const string Root = ".git"; + public const string HeadName = "HEAD"; + public const string IndexName = "index"; + public const string PackedRefsName = "packed-refs"; + public const string LockExtension = ".lock"; + + public static readonly string Config = Path.Combine(DotGit.Root, "config"); + public static readonly string Head = Path.Combine(DotGit.Root, HeadName); + public static readonly string BisectStart = Path.Combine(DotGit.Root, "BISECT_START"); + public static readonly string CherryPickHead = Path.Combine(DotGit.Root, "CHERRY_PICK_HEAD"); + public static readonly string MergeHead = Path.Combine(DotGit.Root, "MERGE_HEAD"); + public static readonly string RevertHead = Path.Combine(DotGit.Root, "REVERT_HEAD"); + public static readonly string RebaseApply = Path.Combine(DotGit.Root, "rebase_apply"); + public static readonly string Index = Path.Combine(DotGit.Root, IndexName); + public static readonly string IndexLock = Path.Combine(DotGit.Root, IndexName + LockExtension); + public static readonly string PackedRefs = Path.Combine(DotGit.Root, PackedRefsName); + public static readonly string Shallow = Path.Combine(DotGit.Root, "shallow"); + + public static class Logs + { + public static readonly string HeadName = "HEAD"; + + public static readonly string Root = Path.Combine(DotGit.Root, "logs"); + public static readonly string Head = Path.Combine(Logs.Root, Logs.HeadName); + } + + public static class Hooks + { + public const string ReadObjectName = "read-object"; + public static readonly string Root = Path.Combine(DotGit.Root, "hooks"); + public static readonly string ReadObjectPath = Path.Combine(Hooks.Root, ReadObjectName); + } + + public static class Info + { + public const string Name = "info"; + public const string ExcludeName = "exclude"; + public const string AlwaysExcludeName = "always_exclude"; + public const string SparseCheckoutName = "sparse-checkout"; + + public static readonly string Root = Path.Combine(DotGit.Root, Info.Name); + public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName); + public static readonly string ExcludePath = Path.Combine(Info.Root, ExcludeName); + public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName); + } + + public static class Objects + { + public static readonly string Root = Path.Combine(DotGit.Root, "objects"); + + public static class Info + { + public static readonly string Root = Path.Combine(Objects.Root, "info"); + public static readonly string Alternates = Path.Combine(Info.Root, "alternates"); + } + + public static class Pack + { + public static readonly string Name = "pack"; + public static readonly string Root = Path.Combine(Objects.Root, Name); + } + } + + public static class Refs + { + public static readonly string Root = Path.Combine(DotGit.Root, "refs"); + + public static class Heads + { + public static readonly string Root = Path.Combine(DotGit.Refs.Root, "heads"); + public static readonly string RootFolder = Heads.Root + Path.DirectorySeparatorChar; + } + } + } + + public static class InstallationCapabilityFiles + { + public const string OnDiskVersion16CapableInstallation = "OnDiskVersion16CapableInstallation.dat"; + } + + public static class VerbParameters + { + public const string InternalUseOnly = "internal_use_only"; + + public static class Mount + { + public const string StartedByService = "StartedByService"; + public const string StartedByVerb = "StartedByVerb"; + public const string Verbosity = "verbosity"; + public const string Keywords = "keywords"; + public const string DebugWindow = "debug-window"; + + public const string DefaultVerbosity = "Informational"; + public const string DefaultKeywords = "Any"; + } + + public static class Unmount + { + public const string SkipLock = "skip-wait-for-lock"; + } + } + + public static class UpgradeVerbMessages + { + public const string ScalarUpgrade = "`scalar upgrade`"; + public const string ScalarUpgradeConfirm = "`scalar upgrade --confirm`"; + public const string ScalarUpgradeDryRun = "`scalar upgrade --dry-run`"; + public const string NoUpgradeCheckPerformed = "No upgrade check was performed."; + public const string NoneRingConsoleAlert = "Upgrade ring set to \"None\". " + NoUpgradeCheckPerformed; + public const string NoRingConfigConsoleAlert = "Upgrade ring is not set. " + NoUpgradeCheckPerformed; + public const string InvalidRingConsoleAlert = "Upgrade ring set to unknown value. " + NoUpgradeCheckPerformed; + public const string SetUpgradeRingCommand = "To set or change upgrade ring, run `scalar config " + LocalScalarConfig.UpgradeRing + " [\"Fast\"|\"Slow\"|\"None\"]` from a command prompt."; + public const string ReminderNotification = "A new version of Scalar is available. Run " + UpgradeVerbMessages.ScalarUpgradeConfirm + " from an elevated command prompt to upgrade."; + public const string UnmountRepoWarning = "Upgrade will unmount and remount scalar repos, ensure you are at a stopping point."; + public const string UpgradeInstallAdvice = "When ready, run " + UpgradeVerbMessages.ScalarUpgradeConfirm + " from an elevated command prompt."; + } + } +} diff --git a/Scalar.Common/ScalarContext.cs b/Scalar.Common/ScalarContext.cs index cf23bebe65..1bfaa3628a 100644 --- a/Scalar.Common/ScalarContext.cs +++ b/Scalar.Common/ScalarContext.cs @@ -1,48 +1,48 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; - -namespace Scalar.Common -{ - public class ScalarContext : IDisposable - { - private bool disposedValue = false; - - public ScalarContext(ITracer tracer, PhysicalFileSystem fileSystem, GitRepo repository, ScalarEnlistment enlistment) - { - this.Tracer = tracer; - this.FileSystem = fileSystem; - this.Enlistment = enlistment; - this.Repository = repository; - - this.Unattended = ScalarEnlistment.IsUnattended(this.Tracer); - } - - public ITracer Tracer { get; private set; } - public PhysicalFileSystem FileSystem { get; private set; } - public GitRepo Repository { get; private set; } - public ScalarEnlistment Enlistment { get; private set; } - public bool Unattended { get; private set; } - - public void Dispose() - { - this.Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (!this.disposedValue) - { - if (disposing) - { - this.Repository.Dispose(); - this.Tracer.Dispose(); - this.Tracer = null; - } - - this.disposedValue = true; - } - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; + +namespace Scalar.Common +{ + public class ScalarContext : IDisposable + { + private bool disposedValue = false; + + public ScalarContext(ITracer tracer, PhysicalFileSystem fileSystem, GitRepo repository, ScalarEnlistment enlistment) + { + this.Tracer = tracer; + this.FileSystem = fileSystem; + this.Enlistment = enlistment; + this.Repository = repository; + + this.Unattended = ScalarEnlistment.IsUnattended(this.Tracer); + } + + public ITracer Tracer { get; private set; } + public PhysicalFileSystem FileSystem { get; private set; } + public GitRepo Repository { get; private set; } + public ScalarEnlistment Enlistment { get; private set; } + public bool Unattended { get; private set; } + + public void Dispose() + { + this.Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + this.Repository.Dispose(); + this.Tracer.Dispose(); + this.Tracer = null; + } + + this.disposedValue = true; + } + } + } +} diff --git a/Scalar.Common/ScalarEnlistment.Shared.cs b/Scalar.Common/ScalarEnlistment.Shared.cs index f3a70691fe..330644a91a 100644 --- a/Scalar.Common/ScalarEnlistment.Shared.cs +++ b/Scalar.Common/ScalarEnlistment.Shared.cs @@ -1,29 +1,29 @@ -using Scalar.Common.Tracing; -using System; -using System.Security; - -namespace Scalar.Common -{ - public partial class ScalarEnlistment - { - public static bool IsUnattended(ITracer tracer) - { - try - { - return Environment.GetEnvironmentVariable(ScalarConstants.UnattendedEnvironmentVariable) == "1"; - } - catch (SecurityException e) - { - if (tracer != null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(ScalarEnlistment)); - metadata.Add("Exception", e.ToString()); - tracer.RelatedError(metadata, "Unable to read environment variable " + ScalarConstants.UnattendedEnvironmentVariable); - } - - return false; - } - } - } -} +using Scalar.Common.Tracing; +using System; +using System.Security; + +namespace Scalar.Common +{ + public partial class ScalarEnlistment + { + public static bool IsUnattended(ITracer tracer) + { + try + { + return Environment.GetEnvironmentVariable(ScalarConstants.UnattendedEnvironmentVariable) == "1"; + } + catch (SecurityException e) + { + if (tracer != null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(ScalarEnlistment)); + metadata.Add("Exception", e.ToString()); + tracer.RelatedError(metadata, "Unable to read environment variable " + ScalarConstants.UnattendedEnvironmentVariable); + } + + return false; + } + } + } +} diff --git a/Scalar.Common/ScalarEnlistment.cs b/Scalar.Common/ScalarEnlistment.cs index af89226e21..fa53d91cc2 100644 --- a/Scalar.Common/ScalarEnlistment.cs +++ b/Scalar.Common/ScalarEnlistment.cs @@ -1,261 +1,261 @@ -using Newtonsoft.Json; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Threading; - -namespace Scalar.Common -{ - public partial class ScalarEnlistment : Enlistment - { - public const string BlobSizesCacheName = "blobSizes"; - - private const string GitObjectCacheName = "gitObjects"; - - private string gitVersion; - private string scalarVersion; - - // New enlistment - public ScalarEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, GitAuthentication authentication) - : base( - enlistmentRoot, - Path.Combine(enlistmentRoot, ScalarConstants.WorkingDirectoryRootName), - Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.WorkingDirectoryBackingRootPath), - repoUrl, - gitBinPath, - flushFileBuffersForPacks: true, - authentication: authentication) - { - this.NamedPipeName = ScalarPlatform.Instance.GetNamedPipeName(this.EnlistmentRoot); - this.DotScalarRoot = Path.Combine(this.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); - this.GitStatusCacheFolder = Path.Combine(this.DotScalarRoot, ScalarConstants.DotScalar.GitStatusCache.Name); - this.GitStatusCachePath = Path.Combine(this.DotScalarRoot, ScalarConstants.DotScalar.GitStatusCache.CachePath); - this.ScalarLogsRoot = Path.Combine(this.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.LogName); - this.LocalObjectsRoot = Path.Combine(this.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Objects.Root); - } - - // Existing, configured enlistment - private ScalarEnlistment(string enlistmentRoot, string gitBinPath, GitAuthentication authentication) - : this( - enlistmentRoot, - null, - gitBinPath, - authentication) - { - } - - public string NamedPipeName { get; } - - public string DotScalarRoot { get; } - - public string ScalarLogsRoot { get; } - - public string LocalCacheRoot { get; private set; } - - public string BlobSizesRoot { get; private set; } - - public override string GitObjectsRoot { get; protected set; } - public override string LocalObjectsRoot { get; protected set; } - public override string GitPackRoot { get; protected set; } - public string GitStatusCacheFolder { get; private set; } - public string GitStatusCachePath { get; private set; } - - // These version properties are only used in logging during clone and mount to track version numbers - public string GitVersion - { - get { return this.gitVersion; } - } - - public string ScalarVersion - { - get { return this.scalarVersion; } - } - - public static ScalarEnlistment CreateFromDirectory( - string directory, - string gitBinRoot, - GitAuthentication authentication, - bool createWithoutRepoURL = false) - { - if (Directory.Exists(directory)) - { - string errorMessage; - string enlistmentRoot; - if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(directory, out enlistmentRoot, out errorMessage)) - { - throw new InvalidRepoException($"Could not get enlistment root. Error: {errorMessage}"); - } - - if (createWithoutRepoURL) - { - return new ScalarEnlistment(enlistmentRoot, string.Empty, gitBinRoot, authentication); - } - - return new ScalarEnlistment(enlistmentRoot, gitBinRoot, authentication); - } - - throw new InvalidRepoException($"Directory '{directory}' does not exist"); - } - - public static string GetNewScalarLogFileName( - string logsRoot, - string logFileType, - PhysicalFileSystem fileSystem = null) - { - return Enlistment.GetNewLogFileName( - logsRoot, - "scalar_" + logFileType, - logId: null, - fileSystem: fileSystem); - } - - public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage) - { - string pipeName = ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot); - tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'"); - - errorMessage = null; - using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) - { - tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connecting to '{pipeName}'"); - - int timeout = unattended ? 300000 : 60000; - if (!pipeClient.Connect(timeout)) - { - tracer.RelatedError($"{nameof(WaitUntilMounted)}: Failed to connect to '{pipeName}' after {timeout} ms"); - errorMessage = "Unable to mount because the Scalar.Mount process is not responding."; - return false; - } - - tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connected to '{pipeName}'"); - - while (true) - { - string response = string.Empty; - try - { - pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); - response = pipeClient.ReadRawResponse(); - NamedPipeMessages.GetStatus.Response getStatusResponse = - NamedPipeMessages.GetStatus.Response.FromJson(response); - - if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.Ready) - { - tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Mount process ready"); - return true; - } - else if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.MountFailed) - { - errorMessage = string.Format("Failed to mount at {0}", enlistmentRoot); - tracer.RelatedError($"{nameof(WaitUntilMounted)}: Mount failed: {errorMessage}"); - return false; - } - else - { - tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Waiting 500ms for mount process to be ready"); - Thread.Sleep(500); - } - } - catch (BrokenPipeException e) - { - errorMessage = string.Format("Could not connect to Scalar.Mount: {0}", e); - tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); - return false; - } - catch (JsonReaderException e) - { - errorMessage = string.Format("Failed to parse response from Scalar.Mount.\n {0}", e); - tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); - return false; - } - } - } - } - - public void SetGitVersion(string gitVersion) - { - this.SetOnce(gitVersion, ref this.gitVersion); - } - - public void SetScalarVersion(string scalarVersion) - { - this.SetOnce(scalarVersion, ref this.scalarVersion); - } - - public void InitializeCachePathsFromKey(string localCacheRoot, string localCacheKey) - { - this.InitializeCachePaths( - localCacheRoot, - Path.Combine(localCacheRoot, localCacheKey, GitObjectCacheName), - Path.Combine(localCacheRoot, localCacheKey, BlobSizesCacheName)); - } - - public void InitializeCachePaths(string localCacheRoot, string gitObjectsRoot, string blobSizesRoot) - { - this.LocalCacheRoot = localCacheRoot; - this.GitObjectsRoot = gitObjectsRoot; - this.GitPackRoot = Path.Combine(this.GitObjectsRoot, ScalarConstants.DotGit.Objects.Pack.Name); - this.BlobSizesRoot = blobSizesRoot; - } - - public bool TryCreateEnlistmentFolders() - { - try - { - Directory.CreateDirectory(this.EnlistmentRoot); - ScalarPlatform.Instance.InitializeEnlistmentACLs(this.EnlistmentRoot); - Directory.CreateDirectory(this.WorkingDirectoryRoot); - this.CreateHiddenDirectory(this.DotScalarRoot); - } - catch (IOException) - { - return false; - } - - return true; - } - - public string GetMountId() - { - return this.GetId(ScalarConstants.GitConfig.MountId); - } - - public string GetEnlistmentId() - { - return this.GetId(ScalarConstants.GitConfig.EnlistmentId); - } - - private void SetOnce(T value, ref T valueToSet) - { - if (valueToSet != null) - { - throw new InvalidOperationException("Value already set."); - } - - valueToSet = value; - } - - /// - /// Creates a hidden directory @ the given path. - /// If directory already exists, hides it. - /// - /// Path to desired hidden directory - private void CreateHiddenDirectory(string path) - { - DirectoryInfo dir = Directory.CreateDirectory(path); - dir.Attributes = FileAttributes.Hidden; - } - - private string GetId(string key) - { - GitProcess.ConfigResult configResult = this.CreateGitProcess().GetFromLocalConfig(key); - string value; - string error; - configResult.TryParseAsString(out value, out error, defaultValue: string.Empty); - return value.Trim(); - } - } -} +using Newtonsoft.Json; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Threading; + +namespace Scalar.Common +{ + public partial class ScalarEnlistment : Enlistment + { + public const string BlobSizesCacheName = "blobSizes"; + + private const string GitObjectCacheName = "gitObjects"; + + private string gitVersion; + private string scalarVersion; + + // New enlistment + public ScalarEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, GitAuthentication authentication) + : base( + enlistmentRoot, + Path.Combine(enlistmentRoot, ScalarConstants.WorkingDirectoryRootName), + Path.Combine(enlistmentRoot, ScalarPlatform.Instance.Constants.WorkingDirectoryBackingRootPath), + repoUrl, + gitBinPath, + flushFileBuffersForPacks: true, + authentication: authentication) + { + this.NamedPipeName = ScalarPlatform.Instance.GetNamedPipeName(this.EnlistmentRoot); + this.DotScalarRoot = Path.Combine(this.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); + this.GitStatusCacheFolder = Path.Combine(this.DotScalarRoot, ScalarConstants.DotScalar.GitStatusCache.Name); + this.GitStatusCachePath = Path.Combine(this.DotScalarRoot, ScalarConstants.DotScalar.GitStatusCache.CachePath); + this.ScalarLogsRoot = Path.Combine(this.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot, ScalarConstants.DotScalar.LogName); + this.LocalObjectsRoot = Path.Combine(this.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Objects.Root); + } + + // Existing, configured enlistment + private ScalarEnlistment(string enlistmentRoot, string gitBinPath, GitAuthentication authentication) + : this( + enlistmentRoot, + null, + gitBinPath, + authentication) + { + } + + public string NamedPipeName { get; } + + public string DotScalarRoot { get; } + + public string ScalarLogsRoot { get; } + + public string LocalCacheRoot { get; private set; } + + public string BlobSizesRoot { get; private set; } + + public override string GitObjectsRoot { get; protected set; } + public override string LocalObjectsRoot { get; protected set; } + public override string GitPackRoot { get; protected set; } + public string GitStatusCacheFolder { get; private set; } + public string GitStatusCachePath { get; private set; } + + // These version properties are only used in logging during clone and mount to track version numbers + public string GitVersion + { + get { return this.gitVersion; } + } + + public string ScalarVersion + { + get { return this.scalarVersion; } + } + + public static ScalarEnlistment CreateFromDirectory( + string directory, + string gitBinRoot, + GitAuthentication authentication, + bool createWithoutRepoURL = false) + { + if (Directory.Exists(directory)) + { + string errorMessage; + string enlistmentRoot; + if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(directory, out enlistmentRoot, out errorMessage)) + { + throw new InvalidRepoException($"Could not get enlistment root. Error: {errorMessage}"); + } + + if (createWithoutRepoURL) + { + return new ScalarEnlistment(enlistmentRoot, string.Empty, gitBinRoot, authentication); + } + + return new ScalarEnlistment(enlistmentRoot, gitBinRoot, authentication); + } + + throw new InvalidRepoException($"Directory '{directory}' does not exist"); + } + + public static string GetNewScalarLogFileName( + string logsRoot, + string logFileType, + PhysicalFileSystem fileSystem = null) + { + return Enlistment.GetNewLogFileName( + logsRoot, + "scalar_" + logFileType, + logId: null, + fileSystem: fileSystem); + } + + public static bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage) + { + string pipeName = ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot); + tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Creating NamedPipeClient for pipe '{pipeName}'"); + + errorMessage = null; + using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) + { + tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connecting to '{pipeName}'"); + + int timeout = unattended ? 300000 : 60000; + if (!pipeClient.Connect(timeout)) + { + tracer.RelatedError($"{nameof(WaitUntilMounted)}: Failed to connect to '{pipeName}' after {timeout} ms"); + errorMessage = "Unable to mount because the Scalar.Mount process is not responding."; + return false; + } + + tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Connected to '{pipeName}'"); + + while (true) + { + string response = string.Empty; + try + { + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + response = pipeClient.ReadRawResponse(); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(response); + + if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.Ready) + { + tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Mount process ready"); + return true; + } + else if (getStatusResponse.MountStatus == NamedPipeMessages.GetStatus.MountFailed) + { + errorMessage = string.Format("Failed to mount at {0}", enlistmentRoot); + tracer.RelatedError($"{nameof(WaitUntilMounted)}: Mount failed: {errorMessage}"); + return false; + } + else + { + tracer.RelatedInfo($"{nameof(WaitUntilMounted)}: Waiting 500ms for mount process to be ready"); + Thread.Sleep(500); + } + } + catch (BrokenPipeException e) + { + errorMessage = string.Format("Could not connect to Scalar.Mount: {0}", e); + tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); + return false; + } + catch (JsonReaderException e) + { + errorMessage = string.Format("Failed to parse response from Scalar.Mount.\n {0}", e); + tracer.RelatedError($"{nameof(WaitUntilMounted)}: {errorMessage}"); + return false; + } + } + } + } + + public void SetGitVersion(string gitVersion) + { + this.SetOnce(gitVersion, ref this.gitVersion); + } + + public void SetScalarVersion(string scalarVersion) + { + this.SetOnce(scalarVersion, ref this.scalarVersion); + } + + public void InitializeCachePathsFromKey(string localCacheRoot, string localCacheKey) + { + this.InitializeCachePaths( + localCacheRoot, + Path.Combine(localCacheRoot, localCacheKey, GitObjectCacheName), + Path.Combine(localCacheRoot, localCacheKey, BlobSizesCacheName)); + } + + public void InitializeCachePaths(string localCacheRoot, string gitObjectsRoot, string blobSizesRoot) + { + this.LocalCacheRoot = localCacheRoot; + this.GitObjectsRoot = gitObjectsRoot; + this.GitPackRoot = Path.Combine(this.GitObjectsRoot, ScalarConstants.DotGit.Objects.Pack.Name); + this.BlobSizesRoot = blobSizesRoot; + } + + public bool TryCreateEnlistmentFolders() + { + try + { + Directory.CreateDirectory(this.EnlistmentRoot); + ScalarPlatform.Instance.InitializeEnlistmentACLs(this.EnlistmentRoot); + Directory.CreateDirectory(this.WorkingDirectoryRoot); + this.CreateHiddenDirectory(this.DotScalarRoot); + } + catch (IOException) + { + return false; + } + + return true; + } + + public string GetMountId() + { + return this.GetId(ScalarConstants.GitConfig.MountId); + } + + public string GetEnlistmentId() + { + return this.GetId(ScalarConstants.GitConfig.EnlistmentId); + } + + private void SetOnce(T value, ref T valueToSet) + { + if (valueToSet != null) + { + throw new InvalidOperationException("Value already set."); + } + + valueToSet = value; + } + + /// + /// Creates a hidden directory @ the given path. + /// If directory already exists, hides it. + /// + /// Path to desired hidden directory + private void CreateHiddenDirectory(string path) + { + DirectoryInfo dir = Directory.CreateDirectory(path); + dir.Attributes = FileAttributes.Hidden; + } + + private string GetId(string key) + { + GitProcess.ConfigResult configResult = this.CreateGitProcess().GetFromLocalConfig(key); + string value; + string error; + configResult.TryParseAsString(out value, out error, defaultValue: string.Empty); + return value.Trim(); + } + } +} diff --git a/Scalar.Common/ScalarLock.cs b/Scalar.Common/ScalarLock.cs index ffb4ca1843..6dbcf25f5f 100644 --- a/Scalar.Common/ScalarLock.cs +++ b/Scalar.Common/ScalarLock.cs @@ -1,133 +1,133 @@ -using Scalar.Common.Tracing; -using System.Diagnostics; -using System.Threading; - -namespace Scalar.Common -{ - public class ScalarLock - { - private readonly object acquisitionLock = new object(); - private readonly ITracer tracer; - - public ScalarLock(ITracer tracer) - { - this.tracer = tracer; - this.Stats = new ActiveGitCommandStats(); - } - - public ActiveGitCommandStats Stats - { - get; - private set; - } - - // The lock release event is a convenient place to record stats about things that happened while a git command was running, - // such as duration/count of object downloads during a git command, cache hits during a git command, etc. - public class ActiveGitCommandStats - { - private Stopwatch lockAcquiredTime; - private long lockHeldExternallyTimeMs; - - private long placeholderTotalUpdateTimeMs; - private long placeholderUpdateFilesTimeMs; - private long placeholderUpdateFoldersTimeMs; - private long placeholderWriteAndFlushTimeMs; - private int deleteFolderPlacehoderAttempted; - private int folderPlaceholdersDeleted; - private int folderPlaceholdersPathNotFound; - private long parseGitIndexTimeMs; - private long projectionWriteLockHeldMs; - - private int numBlobs; - private long blobDownloadTimeMs; - - private int numCommitsAndTrees; - private long commitAndTreeDownloadTimeMs; - - private int numSizeQueries; - private long sizeQueryTimeMs; - - public ActiveGitCommandStats() - { - this.lockAcquiredTime = Stopwatch.StartNew(); - } - - public void RecordReleaseExternalLockRequested() - { - this.lockHeldExternallyTimeMs = this.lockAcquiredTime.ElapsedMilliseconds; - } - - public void RecordUpdatePlaceholders( - long durationMs, - long updateFilesMs, - long updateFoldersMs, - long writeAndFlushMs, - int deleteFolderPlacehoderAttempted, - int folderPlaceholdersDeleted, - int folderPlaceholdersPathNotFound) - { - this.placeholderTotalUpdateTimeMs = durationMs; - this.placeholderUpdateFilesTimeMs = updateFilesMs; - this.placeholderUpdateFoldersTimeMs = updateFoldersMs; - this.placeholderWriteAndFlushTimeMs = writeAndFlushMs; - this.deleteFolderPlacehoderAttempted = deleteFolderPlacehoderAttempted; - this.folderPlaceholdersDeleted = folderPlaceholdersDeleted; - this.folderPlaceholdersPathNotFound = folderPlaceholdersPathNotFound; - } - - public void RecordProjectionWriteLockHeld(long durationMs) - { - this.projectionWriteLockHeldMs = durationMs; - } - - public void RecordParseGitIndex(long durationMs) - { - this.parseGitIndexTimeMs = durationMs; - } - - public void RecordObjectDownload(bool isBlob, long downloadTimeMs) - { - if (isBlob) - { - Interlocked.Increment(ref this.numBlobs); - Interlocked.Add(ref this.blobDownloadTimeMs, downloadTimeMs); - } - else - { - Interlocked.Increment(ref this.numCommitsAndTrees); - Interlocked.Add(ref this.commitAndTreeDownloadTimeMs, downloadTimeMs); - } - } - - public void RecordSizeQuery(long queryTimeMs) - { - Interlocked.Increment(ref this.numSizeQueries); - Interlocked.Add(ref this.sizeQueryTimeMs, queryTimeMs); - } - - public void AddStatsToTelemetry(EventMetadata metadata) - { - metadata.Add("DurationMS", this.lockAcquiredTime.ElapsedMilliseconds); - metadata.Add("LockHeldExternallyMS", this.lockHeldExternallyTimeMs); - metadata.Add("ParseGitIndexMS", this.parseGitIndexTimeMs); - metadata.Add("UpdatePlaceholdersMS", this.placeholderTotalUpdateTimeMs); - metadata.Add("UpdateFilePlaceholdersMS", this.placeholderUpdateFilesTimeMs); - metadata.Add("UpdateFolderPlaceholdersMS", this.placeholderUpdateFoldersTimeMs); - metadata.Add("DeleteFolderPlacehoderAttempted", this.deleteFolderPlacehoderAttempted); - metadata.Add("FolderPlaceholdersDeleted", this.folderPlaceholdersDeleted); - metadata.Add("FolderPlaceholdersPathNotFound", this.folderPlaceholdersPathNotFound); - metadata.Add("PlaceholdersWriteAndFlushMS", this.placeholderWriteAndFlushTimeMs); - metadata.Add("ProjectionWriteLockHeldMs", this.projectionWriteLockHeldMs); - - metadata.Add("BlobsDownloaded", this.numBlobs); - metadata.Add("BlobDownloadTimeMS", this.blobDownloadTimeMs); - - metadata.Add("CommitsAndTreesDownloaded", this.numCommitsAndTrees); - metadata.Add("CommitsAndTreesDownloadTimeMS", this.commitAndTreeDownloadTimeMs); - - metadata.Add("SizeQueries", this.numSizeQueries); - metadata.Add("SizeQueryTimeMS", this.sizeQueryTimeMs); - } - } - } -} +using Scalar.Common.Tracing; +using System.Diagnostics; +using System.Threading; + +namespace Scalar.Common +{ + public class ScalarLock + { + private readonly object acquisitionLock = new object(); + private readonly ITracer tracer; + + public ScalarLock(ITracer tracer) + { + this.tracer = tracer; + this.Stats = new ActiveGitCommandStats(); + } + + public ActiveGitCommandStats Stats + { + get; + private set; + } + + // The lock release event is a convenient place to record stats about things that happened while a git command was running, + // such as duration/count of object downloads during a git command, cache hits during a git command, etc. + public class ActiveGitCommandStats + { + private Stopwatch lockAcquiredTime; + private long lockHeldExternallyTimeMs; + + private long placeholderTotalUpdateTimeMs; + private long placeholderUpdateFilesTimeMs; + private long placeholderUpdateFoldersTimeMs; + private long placeholderWriteAndFlushTimeMs; + private int deleteFolderPlacehoderAttempted; + private int folderPlaceholdersDeleted; + private int folderPlaceholdersPathNotFound; + private long parseGitIndexTimeMs; + private long projectionWriteLockHeldMs; + + private int numBlobs; + private long blobDownloadTimeMs; + + private int numCommitsAndTrees; + private long commitAndTreeDownloadTimeMs; + + private int numSizeQueries; + private long sizeQueryTimeMs; + + public ActiveGitCommandStats() + { + this.lockAcquiredTime = Stopwatch.StartNew(); + } + + public void RecordReleaseExternalLockRequested() + { + this.lockHeldExternallyTimeMs = this.lockAcquiredTime.ElapsedMilliseconds; + } + + public void RecordUpdatePlaceholders( + long durationMs, + long updateFilesMs, + long updateFoldersMs, + long writeAndFlushMs, + int deleteFolderPlacehoderAttempted, + int folderPlaceholdersDeleted, + int folderPlaceholdersPathNotFound) + { + this.placeholderTotalUpdateTimeMs = durationMs; + this.placeholderUpdateFilesTimeMs = updateFilesMs; + this.placeholderUpdateFoldersTimeMs = updateFoldersMs; + this.placeholderWriteAndFlushTimeMs = writeAndFlushMs; + this.deleteFolderPlacehoderAttempted = deleteFolderPlacehoderAttempted; + this.folderPlaceholdersDeleted = folderPlaceholdersDeleted; + this.folderPlaceholdersPathNotFound = folderPlaceholdersPathNotFound; + } + + public void RecordProjectionWriteLockHeld(long durationMs) + { + this.projectionWriteLockHeldMs = durationMs; + } + + public void RecordParseGitIndex(long durationMs) + { + this.parseGitIndexTimeMs = durationMs; + } + + public void RecordObjectDownload(bool isBlob, long downloadTimeMs) + { + if (isBlob) + { + Interlocked.Increment(ref this.numBlobs); + Interlocked.Add(ref this.blobDownloadTimeMs, downloadTimeMs); + } + else + { + Interlocked.Increment(ref this.numCommitsAndTrees); + Interlocked.Add(ref this.commitAndTreeDownloadTimeMs, downloadTimeMs); + } + } + + public void RecordSizeQuery(long queryTimeMs) + { + Interlocked.Increment(ref this.numSizeQueries); + Interlocked.Add(ref this.sizeQueryTimeMs, queryTimeMs); + } + + public void AddStatsToTelemetry(EventMetadata metadata) + { + metadata.Add("DurationMS", this.lockAcquiredTime.ElapsedMilliseconds); + metadata.Add("LockHeldExternallyMS", this.lockHeldExternallyTimeMs); + metadata.Add("ParseGitIndexMS", this.parseGitIndexTimeMs); + metadata.Add("UpdatePlaceholdersMS", this.placeholderTotalUpdateTimeMs); + metadata.Add("UpdateFilePlaceholdersMS", this.placeholderUpdateFilesTimeMs); + metadata.Add("UpdateFolderPlaceholdersMS", this.placeholderUpdateFoldersTimeMs); + metadata.Add("DeleteFolderPlacehoderAttempted", this.deleteFolderPlacehoderAttempted); + metadata.Add("FolderPlaceholdersDeleted", this.folderPlaceholdersDeleted); + metadata.Add("FolderPlaceholdersPathNotFound", this.folderPlaceholdersPathNotFound); + metadata.Add("PlaceholdersWriteAndFlushMS", this.placeholderWriteAndFlushTimeMs); + metadata.Add("ProjectionWriteLockHeldMs", this.projectionWriteLockHeldMs); + + metadata.Add("BlobsDownloaded", this.numBlobs); + metadata.Add("BlobDownloadTimeMS", this.blobDownloadTimeMs); + + metadata.Add("CommitsAndTreesDownloaded", this.numCommitsAndTrees); + metadata.Add("CommitsAndTreesDownloadTimeMS", this.commitAndTreeDownloadTimeMs); + + metadata.Add("SizeQueries", this.numSizeQueries); + metadata.Add("SizeQueryTimeMS", this.sizeQueryTimeMs); + } + } + } +} diff --git a/Scalar.Common/ServerScalarConfig.cs b/Scalar.Common/ServerScalarConfig.cs index b1727ea099..1600e807ff 100644 --- a/Scalar.Common/ServerScalarConfig.cs +++ b/Scalar.Common/ServerScalarConfig.cs @@ -1,20 +1,20 @@ -using Scalar.Common.Http; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Scalar.Common -{ - public class ServerScalarConfig - { - public IEnumerable AllowedScalarClientVersions { get; set; } - - public IEnumerable CacheServers { get; set; } = Enumerable.Empty(); - - public class VersionRange - { - public Version Min { get; set; } - public Version Max { get; set; } - } - } +using Scalar.Common.Http; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Scalar.Common +{ + public class ServerScalarConfig + { + public IEnumerable AllowedScalarClientVersions { get; set; } + + public IEnumerable CacheServers { get; set; } = Enumerable.Empty(); + + public class VersionRange + { + public Version Min { get; set; } + public Version Max { get; set; } + } + } } diff --git a/Scalar.Common/StreamUtil.cs b/Scalar.Common/StreamUtil.cs index 1941a65d2d..5774a8635a 100644 --- a/Scalar.Common/StreamUtil.cs +++ b/Scalar.Common/StreamUtil.cs @@ -1,91 +1,91 @@ -using System; -using System.IO; - -namespace Scalar.Common -{ - public class StreamUtil - { - /// - /// .NET default buffer size uses as of 8/30/16 - /// - public const int DefaultCopyBufferSize = 81920; - - /// - /// Copies all bytes from the source stream to the destination stream. This is an exact copy - /// of Stream.CopyTo(), but can uses the supplied buffer instead of allocating a new one. - /// - /// - /// As of .NET 4.6, each call to Stream.CopyTo() allocates a new 80K byte[] buffer, which - /// consumes many more resources than reusing one we already have if the scenario allows it. - /// - /// Source stream to copy from - /// Destination stream to copy to - /// - /// Shared buffer to use. If null, we allocate one with the same size .NET would otherwise use. - /// - public static void CopyToWithBuffer(Stream source, Stream destination, byte[] buffer = null) - { - buffer = buffer ?? new byte[DefaultCopyBufferSize]; - int read; - while (true) - { - try - { - read = source.Read(buffer, 0, buffer.Length); - } - catch (Exception ex) - { - // All exceptions potentially from network should be retried - throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); - } - - if (read == 0) - { - break; - } - - destination.Write(buffer, 0, read); - } - } - - /// - /// Call until either bytes are read or - /// the end of is reached. - /// - /// Buffer to read bytes into. - /// Offset in to start reading into. - /// Maximum number of bytes to read. - /// - /// Number of bytes read, may be less than if end was reached. - /// - public static int TryReadGreedy(Stream stream, byte[] buf, int offset, int count) - { - int totalRead = 0; - int read = 0; - while (totalRead < count) - { - int start = offset + totalRead; - int length = count - totalRead; - - try - { - read = stream.Read(buf, start, length); - } - catch (Exception ex) - { - // All exceptions potentially from network should be retried - throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); - } - - if (read == 0) - { - break; - } - - totalRead += read; - } - - return totalRead; - } - } -} +using System; +using System.IO; + +namespace Scalar.Common +{ + public class StreamUtil + { + /// + /// .NET default buffer size uses as of 8/30/16 + /// + public const int DefaultCopyBufferSize = 81920; + + /// + /// Copies all bytes from the source stream to the destination stream. This is an exact copy + /// of Stream.CopyTo(), but can uses the supplied buffer instead of allocating a new one. + /// + /// + /// As of .NET 4.6, each call to Stream.CopyTo() allocates a new 80K byte[] buffer, which + /// consumes many more resources than reusing one we already have if the scenario allows it. + /// + /// Source stream to copy from + /// Destination stream to copy to + /// + /// Shared buffer to use. If null, we allocate one with the same size .NET would otherwise use. + /// + public static void CopyToWithBuffer(Stream source, Stream destination, byte[] buffer = null) + { + buffer = buffer ?? new byte[DefaultCopyBufferSize]; + int read; + while (true) + { + try + { + read = source.Read(buffer, 0, buffer.Length); + } + catch (Exception ex) + { + // All exceptions potentially from network should be retried + throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); + } + + if (read == 0) + { + break; + } + + destination.Write(buffer, 0, read); + } + } + + /// + /// Call until either bytes are read or + /// the end of is reached. + /// + /// Buffer to read bytes into. + /// Offset in to start reading into. + /// Maximum number of bytes to read. + /// + /// Number of bytes read, may be less than if end was reached. + /// + public static int TryReadGreedy(Stream stream, byte[] buf, int offset, int count) + { + int totalRead = 0; + int read = 0; + while (totalRead < count) + { + int start = offset + totalRead; + int length = count - totalRead; + + try + { + read = stream.Read(buf, start, length); + } + catch (Exception ex) + { + // All exceptions potentially from network should be retried + throw new RetryableException("Exception while reading stream. See inner exception for details.", ex); + } + + if (read == 0) + { + break; + } + + totalRead += read; + } + + return totalRead; + } + } +} diff --git a/Scalar.Common/Tracing/DiagnosticConsoleEventListener.cs b/Scalar.Common/Tracing/DiagnosticConsoleEventListener.cs index f55c4ae32b..d88bad2f23 100644 --- a/Scalar.Common/Tracing/DiagnosticConsoleEventListener.cs +++ b/Scalar.Common/Tracing/DiagnosticConsoleEventListener.cs @@ -1,21 +1,21 @@ -using System; - -namespace Scalar.Common.Tracing -{ - /// - /// An event listener that will print all telemetry messages to the console with timestamps. - /// The format of the message is designed for completeness and parsability, but not for beauty. - /// - public class DiagnosticConsoleEventListener : EventListener - { - public DiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) - : base(maxVerbosity, keywordFilter, eventSink) - { - } - - protected override void RecordMessageInternal(TraceEventMessage message) - { - Console.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); - } - } -} +using System; + +namespace Scalar.Common.Tracing +{ + /// + /// An event listener that will print all telemetry messages to the console with timestamps. + /// The format of the message is designed for completeness and parsability, but not for beauty. + /// + public class DiagnosticConsoleEventListener : EventListener + { + public DiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) + : base(maxVerbosity, keywordFilter, eventSink) + { + } + + protected override void RecordMessageInternal(TraceEventMessage message) + { + Console.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); + } + } +} diff --git a/Scalar.Common/Tracing/EventLevel.cs b/Scalar.Common/Tracing/EventLevel.cs index 5605149e0e..a140378211 100644 --- a/Scalar.Common/Tracing/EventLevel.cs +++ b/Scalar.Common/Tracing/EventLevel.cs @@ -1,14 +1,14 @@ -namespace Scalar.Common.Tracing -{ - // The default EventLevel is Verbose, which does not go to log files by default. - // If you want to log to a file, you need to raise EventLevel to at least Informational - public enum EventLevel - { - LogAlways = 0, - Critical = 1, - Error = 2, - Warning = 3, - Informational = 4, - Verbose = 5 - } +namespace Scalar.Common.Tracing +{ + // The default EventLevel is Verbose, which does not go to log files by default. + // If you want to log to a file, you need to raise EventLevel to at least Informational + public enum EventLevel + { + LogAlways = 0, + Critical = 1, + Error = 2, + Warning = 3, + Informational = 4, + Verbose = 5 + } } diff --git a/Scalar.Common/Tracing/EventListener.cs b/Scalar.Common/Tracing/EventListener.cs index 15a89f40f2..9a9c630e29 100644 --- a/Scalar.Common/Tracing/EventListener.cs +++ b/Scalar.Common/Tracing/EventListener.cs @@ -1,76 +1,76 @@ -using System; -using System.Text; - -namespace Scalar.Common.Tracing -{ - public abstract class EventListener : IDisposable - { - private readonly EventLevel maxVerbosity; - private readonly Keywords keywordFilter; - private readonly IEventListenerEventSink eventSink; - - protected EventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) - { - this.maxVerbosity = maxVerbosity; - this.keywordFilter = keywordFilter; - this.eventSink = eventSink; - } - - public virtual void Dispose() - { - } - - public void RecordMessage(TraceEventMessage message) - { - if (this.IsEnabled(message.Level, message.Keywords)) - { - try - { - this.RecordMessageInternal(message); - } - catch (Exception ex) - { - this.RaiseListenerFailure(ex.ToString()); - } - } - } - - protected abstract void RecordMessageInternal(TraceEventMessage message); - - protected string GetLogString(string eventName, EventOpcode opcode, string jsonPayload) - { - // Make a smarter guess (than 16 characters) about initial size to reduce allocations - StringBuilder message = new StringBuilder(1024); - message.AppendFormat("[{0:yyyy-MM-dd HH:mm:ss.ffff zzz}] {1}", DateTime.Now, eventName); - - if (opcode != 0) - { - message.Append(" (" + opcode + ")"); - } - - if (!string.IsNullOrEmpty(jsonPayload)) - { - message.Append(" " + jsonPayload); - } - - return message.ToString(); - } - - protected bool IsEnabled(EventLevel level, Keywords keyword) - { - return this.keywordFilter != Keywords.None && - this.maxVerbosity >= level && - (this.keywordFilter & keyword) != 0; - } - - protected void RaiseListenerRecovery() - { - this.eventSink?.OnListenerRecovery(this); - } - - protected void RaiseListenerFailure(string errorMessage) - { - this.eventSink?.OnListenerFailure(this, errorMessage); - } - } -} +using System; +using System.Text; + +namespace Scalar.Common.Tracing +{ + public abstract class EventListener : IDisposable + { + private readonly EventLevel maxVerbosity; + private readonly Keywords keywordFilter; + private readonly IEventListenerEventSink eventSink; + + protected EventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) + { + this.maxVerbosity = maxVerbosity; + this.keywordFilter = keywordFilter; + this.eventSink = eventSink; + } + + public virtual void Dispose() + { + } + + public void RecordMessage(TraceEventMessage message) + { + if (this.IsEnabled(message.Level, message.Keywords)) + { + try + { + this.RecordMessageInternal(message); + } + catch (Exception ex) + { + this.RaiseListenerFailure(ex.ToString()); + } + } + } + + protected abstract void RecordMessageInternal(TraceEventMessage message); + + protected string GetLogString(string eventName, EventOpcode opcode, string jsonPayload) + { + // Make a smarter guess (than 16 characters) about initial size to reduce allocations + StringBuilder message = new StringBuilder(1024); + message.AppendFormat("[{0:yyyy-MM-dd HH:mm:ss.ffff zzz}] {1}", DateTime.Now, eventName); + + if (opcode != 0) + { + message.Append(" (" + opcode + ")"); + } + + if (!string.IsNullOrEmpty(jsonPayload)) + { + message.Append(" " + jsonPayload); + } + + return message.ToString(); + } + + protected bool IsEnabled(EventLevel level, Keywords keyword) + { + return this.keywordFilter != Keywords.None && + this.maxVerbosity >= level && + (this.keywordFilter & keyword) != 0; + } + + protected void RaiseListenerRecovery() + { + this.eventSink?.OnListenerRecovery(this); + } + + protected void RaiseListenerFailure(string errorMessage) + { + this.eventSink?.OnListenerFailure(this, errorMessage); + } + } +} diff --git a/Scalar.Common/Tracing/EventMetadata.cs b/Scalar.Common/Tracing/EventMetadata.cs index 9be5b5ea3e..0e579cb4c0 100644 --- a/Scalar.Common/Tracing/EventMetadata.cs +++ b/Scalar.Common/Tracing/EventMetadata.cs @@ -1,18 +1,18 @@ -using System.Collections.Generic; - -namespace Scalar.Common.Tracing -{ - // This is a convenience class to make code around event metadata look nicer. - // It's more obvious to see EventMetadata than Dictionary everywhere. - public class EventMetadata : Dictionary - { - public EventMetadata() - { - } - - public EventMetadata(Dictionary metadata) - : base(metadata) - { - } - } -} +using System.Collections.Generic; + +namespace Scalar.Common.Tracing +{ + // This is a convenience class to make code around event metadata look nicer. + // It's more obvious to see EventMetadata than Dictionary everywhere. + public class EventMetadata : Dictionary + { + public EventMetadata() + { + } + + public EventMetadata(Dictionary metadata) + : base(metadata) + { + } + } +} diff --git a/Scalar.Common/Tracing/EventOpcode.cs b/Scalar.Common/Tracing/EventOpcode.cs index 869eb96950..6294077ed9 100644 --- a/Scalar.Common/Tracing/EventOpcode.cs +++ b/Scalar.Common/Tracing/EventOpcode.cs @@ -1,18 +1,18 @@ -namespace Scalar.Common.Tracing -{ - // Copied from Microsoft.Diagnostics.Tracing.EventOpcode - public enum EventOpcode - { - // Summary: - // An informational event - Info = 0, - - // Summary: - // An activity start event - Start = 1, - - // Summary: - // An activity end event - Stop = 2, - } +namespace Scalar.Common.Tracing +{ + // Copied from Microsoft.Diagnostics.Tracing.EventOpcode + public enum EventOpcode + { + // Summary: + // An informational event + Info = 0, + + // Summary: + // An activity start event + Start = 1, + + // Summary: + // An activity end event + Stop = 2, + } } diff --git a/Scalar.Common/Tracing/ITracer.cs b/Scalar.Common/Tracing/ITracer.cs index e9b9bf6328..66da23574f 100644 --- a/Scalar.Common/Tracing/ITracer.cs +++ b/Scalar.Common/Tracing/ITracer.cs @@ -1,42 +1,42 @@ -using System; - -namespace Scalar.Common.Tracing -{ - public interface ITracer : IDisposable - { - ITracer StartActivity(string activityName, EventLevel level); - - ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata); - ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata); - - void SetGitCommandSessionId(string sessionId); - - void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata); - - void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keywords); - - void RelatedInfo(string message); - - void RelatedInfo(string format, params object[] args); - - void RelatedInfo(EventMetadata metadata, string message); - - void RelatedWarning(EventMetadata metadata, string message); - - void RelatedWarning(EventMetadata metadata, string message, Keywords keywords); - - void RelatedWarning(string message); - - void RelatedWarning(string format, params object[] args); - - void RelatedError(EventMetadata metadata, string message); - - void RelatedError(EventMetadata metadata, string message, Keywords keywords); - - void RelatedError(string message); - - void RelatedError(string format, params object[] args); - - TimeSpan Stop(EventMetadata metadata); - } +using System; + +namespace Scalar.Common.Tracing +{ + public interface ITracer : IDisposable + { + ITracer StartActivity(string activityName, EventLevel level); + + ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata); + ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata); + + void SetGitCommandSessionId(string sessionId); + + void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata); + + void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keywords); + + void RelatedInfo(string message); + + void RelatedInfo(string format, params object[] args); + + void RelatedInfo(EventMetadata metadata, string message); + + void RelatedWarning(EventMetadata metadata, string message); + + void RelatedWarning(EventMetadata metadata, string message, Keywords keywords); + + void RelatedWarning(string message); + + void RelatedWarning(string format, params object[] args); + + void RelatedError(EventMetadata metadata, string message); + + void RelatedError(EventMetadata metadata, string message, Keywords keywords); + + void RelatedError(string message); + + void RelatedError(string format, params object[] args); + + TimeSpan Stop(EventMetadata metadata); + } } diff --git a/Scalar.Common/Tracing/JsonTracer.cs b/Scalar.Common/Tracing/JsonTracer.cs index 86448c964b..43b2425ca6 100644 --- a/Scalar.Common/Tracing/JsonTracer.cs +++ b/Scalar.Common/Tracing/JsonTracer.cs @@ -1,409 +1,409 @@ -using Newtonsoft.Json; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; - -namespace Scalar.Common.Tracing -{ - public class JsonTracer : ITracer, IEventListenerEventSink - { - public const string NetworkErrorEventName = "NetworkError"; - - private readonly ConcurrentBag listeners; - private readonly ConcurrentDictionary failedListeners = new ConcurrentDictionary(); - - private readonly string activityName; - private readonly Guid parentActivityId; - private readonly Guid activityId; - private readonly Stopwatch duration = Stopwatch.StartNew(); - - private readonly EventLevel startStopLevel; - private readonly Keywords startStopKeywords; - - private bool isDisposed = false; - private bool stopped = false; - - public JsonTracer(string providerName, string activityName, bool disableTelemetry = false) - : this(providerName, Guid.Empty, activityName, enlistmentId: null, mountId: null, disableTelemetry: disableTelemetry) - { - } - - public JsonTracer(string providerName, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) - : this(providerName, Guid.Empty, activityName, enlistmentId, mountId, disableTelemetry) - { - } - - public JsonTracer(string providerName, Guid providerActivityId, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) - : this( - null, - providerActivityId, - activityName, - EventLevel.Informational, - Keywords.Telemetry) - { - if (!disableTelemetry) - { - string gitBinRoot = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - - // If we do not have a git binary, then we cannot check if we should set up telemetry - // We also cannot log this, as we are setting up tracer. - if (string.IsNullOrEmpty(gitBinRoot)) - { - return; - } - - TelemetryDaemonEventListener daemonListener = TelemetryDaemonEventListener.CreateIfEnabled(gitBinRoot, providerName, enlistmentId, mountId, this); - if (daemonListener != null) - { - this.listeners.Add(daemonListener); - } - } - } - - private JsonTracer(ConcurrentBag listeners, Guid parentActivityId, string activityName, EventLevel startStopLevel, Keywords startStopKeywords) - { - this.listeners = listeners ?? new ConcurrentBag(); - this.parentActivityId = parentActivityId; - this.activityName = activityName; - this.startStopLevel = startStopLevel; - this.startStopKeywords = startStopKeywords; - - this.activityId = Guid.NewGuid(); - } - - public bool HasLogFileEventListener - { - get - { - return this.listeners.Any(listener => listener is LogFileEventListener); - } - } - - public void SetGitCommandSessionId(string sessionId) - { - TelemetryDaemonEventListener daemonListener = this.listeners.FirstOrDefault(x => x is TelemetryDaemonEventListener) as TelemetryDaemonEventListener; - if (daemonListener != null) - { - daemonListener.GitCommandSessionId = sessionId; - } - } - - public void AddEventListener(EventListener listener) - { - if (this.isDisposed) - { - throw new ObjectDisposedException(nameof(JsonTracer)); - } - - this.listeners.Add(listener); - - // Tell the new listener about others who have previously failed - foreach (KeyValuePair kvp in this.failedListeners) - { - TraceEventMessage failureMessage = CreateListenerFailureMessage(kvp.Key, kvp.Value); - listener.RecordMessage(failureMessage); - } - } - - public void AddDiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) - { - this.AddEventListener(new DiagnosticConsoleEventListener(maxVerbosity, keywordFilter, this)); - } - - public void AddPrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) - { - this.AddEventListener(new PrettyConsoleEventListener(maxVerbosity, keywordFilter, this)); - } - - public void AddLogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter) - { - this.AddEventListener(new LogFileEventListener(logFilePath, maxVerbosity, keywordFilter, this)); - } - - public void Dispose() - { - if (this.isDisposed) - { - // This instance has already been disposed - return; - } - - this.Stop(null); - - // If we have no parent, then we are the root tracer and should dispose our eventsource. - if (this.parentActivityId == Guid.Empty) - { - // Empty the listener bag and dispose of the instances as we remove them. - EventListener listener; - while (this.listeners.TryTake(out listener)) - { - listener.Dispose(); - } - } - - this.isDisposed = true; - } - - public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata) - { - this.RelatedEvent(level, eventName, metadata, Keywords.None); - } - - public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keyword) - { - this.WriteEvent(eventName, level, keyword, metadata, opcode: 0); - } - - public virtual void RelatedInfo(string format, params object[] args) - { - this.RelatedInfo(string.Format(format, args)); - } - - public virtual void RelatedInfo(string message) - { - this.RelatedInfo(new EventMetadata(), message); - } - - public virtual void RelatedInfo(EventMetadata metadata, string message) - { - metadata = metadata ?? new EventMetadata(); - metadata.Add(TracingConstants.MessageKey.InfoMessage, message); - this.RelatedEvent(EventLevel.Informational, "Information", metadata); - } - - public virtual void RelatedWarning(EventMetadata metadata, string message) - { - this.RelatedWarning(metadata, message, Keywords.None); - } - - public virtual void RelatedWarning(EventMetadata metadata, string message, Keywords keywords) - { - metadata = metadata ?? new EventMetadata(); - metadata[TracingConstants.MessageKey.WarningMessage] = message; - this.RelatedEvent(EventLevel.Warning, "Warning", metadata, keywords); - } - - public virtual void RelatedWarning(string message) - { - EventMetadata metadata = new EventMetadata(); - this.RelatedWarning(metadata, message); - } - - public virtual void RelatedWarning(string format, params object[] args) - { - this.RelatedWarning(string.Format(format, args)); - } - - public virtual void RelatedError(EventMetadata metadata, string message) - { - this.RelatedError(metadata, message, Keywords.Telemetry); - } - - public virtual void RelatedError(EventMetadata metadata, string message, Keywords keywords) - { - metadata = metadata ?? new EventMetadata(); - metadata[TracingConstants.MessageKey.ErrorMessage] = message; - this.RelatedEvent(EventLevel.Error, GetCategorizedErrorEventName(keywords), metadata, keywords | Keywords.Telemetry); - } - - public virtual void RelatedError(string message) - { - EventMetadata metadata = new EventMetadata(); - this.RelatedError(metadata, message); - } - - public virtual void RelatedError(string format, params object[] args) - { - this.RelatedError(string.Format(format, args)); - } - - public TimeSpan Stop(EventMetadata metadata) - { - if (this.stopped) - { - return TimeSpan.Zero; - } - - this.duration.Stop(); - this.stopped = true; - - metadata = metadata ?? new EventMetadata(); - metadata.Add("DurationMs", this.duration.ElapsedMilliseconds); - - this.WriteEvent(this.activityName, this.startStopLevel, this.startStopKeywords, metadata, EventOpcode.Stop); - - return this.duration.Elapsed; - } - - public ITracer StartActivity(string childActivityName, EventLevel startStopLevel) - { - return this.StartActivity(childActivityName, startStopLevel, null); - } - - public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, EventMetadata startMetadata) - { - return this.StartActivity(childActivityName, startStopLevel, Keywords.None, startMetadata); - } - - public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, Keywords startStopKeywords, EventMetadata startMetadata) - { - JsonTracer subTracer = new JsonTracer(this.listeners, this.activityId, childActivityName, startStopLevel, startStopKeywords); - - // Write the start event, disabling the Telemetry keyword so we will only dispatch telemetry at the end event. - subTracer.WriteStartEvent(startMetadata, startStopKeywords & ~Keywords.Telemetry); - - return subTracer; - } - - public void WriteStartEvent( - string enlistmentRoot, - string repoUrl, - string cacheServerUrl, - EventMetadata additionalMetadata = null) - { - EventMetadata metadata = new EventMetadata(); - - metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); - - if (enlistmentRoot != null) - { - metadata.Add("EnlistmentRoot", enlistmentRoot); - } - - if (repoUrl != null) - { - metadata.Add("Remote", Uri.EscapeUriString(repoUrl)); - } - - if (cacheServerUrl != null) - { - // Changing this key to CacheServerUrl will mess with our telemetry, so it stays for historical reasons - metadata.Add("ObjectsEndpoint", Uri.EscapeUriString(cacheServerUrl)); - } - - if (additionalMetadata != null) - { - foreach (string key in additionalMetadata.Keys) - { - metadata.Add(key, additionalMetadata[key]); - } - } - - this.WriteStartEvent(metadata, Keywords.Telemetry); - } - - public void WriteStartEvent(EventMetadata metadata, Keywords keywords) - { - this.WriteEvent(this.activityName, this.startStopLevel, keywords, metadata, EventOpcode.Start); - } - - void IEventListenerEventSink.OnListenerRecovery(EventListener listener) - { - // Check ContainsKey first (rather than always calling TryRemove) because ContainsKey - // is lock-free and recoveredListener should rarely be in failedListeners - if (!this.failedListeners.ContainsKey(listener)) - { - // This listener has not failed since the last time it was called, so no need to log recovery - return; - } - - if (this.failedListeners.TryRemove(listener, out _)) - { - TraceEventMessage message = CreateListenerRecoveryMessage(listener); - this.LogMessageToNonFailedListeners(message); - } - } - - void IEventListenerEventSink.OnListenerFailure(EventListener listener, string errorMessage) - { - if (!this.failedListeners.TryAdd(listener, errorMessage)) - { - // We've already logged that this listener has failed so there is no need to do it again - return; - } - - TraceEventMessage message = CreateListenerFailureMessage(listener, errorMessage); - this.LogMessageToNonFailedListeners(message); - } - - private static string GetCategorizedErrorEventName(Keywords keywords) - { - switch (keywords) - { - case Keywords.Network: return NetworkErrorEventName; - default: return "Error"; - } - } - - private static TraceEventMessage CreateListenerRecoveryMessage(EventListener recoveredListener) - { - return new TraceEventMessage - { - EventName = "TraceEventListenerRecovery", - Level = EventLevel.Informational, - Keywords = Keywords.Any, - Opcode = EventOpcode.Info, - Payload = JsonConvert.SerializeObject(new Dictionary - { - ["EventListener"] = recoveredListener.GetType().Name - }) - }; - } - - private static TraceEventMessage CreateListenerFailureMessage(EventListener failedListener, string errorMessage) - { - return new TraceEventMessage - { - EventName = "TraceEventListenerFailure", - Level = EventLevel.Error, - Keywords = Keywords.Any, - Opcode = EventOpcode.Info, - Payload = JsonConvert.SerializeObject(new Dictionary - { - ["EventListener"] = failedListener.GetType().Name, - ["ErrorMessage"] = errorMessage, - }) - }; - } - - private void WriteEvent(string eventName, EventLevel level, Keywords keywords, EventMetadata metadata, EventOpcode opcode) - { - string jsonPayload = metadata != null ? JsonConvert.SerializeObject(metadata) : null; - - if (this.isDisposed) - { - throw new ObjectDisposedException(nameof(JsonTracer)); - } - - var message = new TraceEventMessage - { - EventName = eventName, - ActivityId = this.activityId, - ParentActivityId = this.parentActivityId, - Level = level, - Keywords = keywords, - Opcode = opcode, - Payload = jsonPayload - }; - - // Iterating over the bag is thread-safe as the enumerator returned here - // is of a snapshot of the bag. - foreach (EventListener listener in this.listeners) - { - listener.RecordMessage(message); - } - } - - private void LogMessageToNonFailedListeners(TraceEventMessage message) - { - foreach (EventListener listener in this.listeners.Except(this.failedListeners.Keys)) - { - // To prevent infinitely recursive failures, we won't try and log that we failed to log that a listener failed :) - listener.RecordMessage(message); - } - } - } -} +using Newtonsoft.Json; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; + +namespace Scalar.Common.Tracing +{ + public class JsonTracer : ITracer, IEventListenerEventSink + { + public const string NetworkErrorEventName = "NetworkError"; + + private readonly ConcurrentBag listeners; + private readonly ConcurrentDictionary failedListeners = new ConcurrentDictionary(); + + private readonly string activityName; + private readonly Guid parentActivityId; + private readonly Guid activityId; + private readonly Stopwatch duration = Stopwatch.StartNew(); + + private readonly EventLevel startStopLevel; + private readonly Keywords startStopKeywords; + + private bool isDisposed = false; + private bool stopped = false; + + public JsonTracer(string providerName, string activityName, bool disableTelemetry = false) + : this(providerName, Guid.Empty, activityName, enlistmentId: null, mountId: null, disableTelemetry: disableTelemetry) + { + } + + public JsonTracer(string providerName, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) + : this(providerName, Guid.Empty, activityName, enlistmentId, mountId, disableTelemetry) + { + } + + public JsonTracer(string providerName, Guid providerActivityId, string activityName, string enlistmentId, string mountId, bool disableTelemetry = false) + : this( + null, + providerActivityId, + activityName, + EventLevel.Informational, + Keywords.Telemetry) + { + if (!disableTelemetry) + { + string gitBinRoot = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + + // If we do not have a git binary, then we cannot check if we should set up telemetry + // We also cannot log this, as we are setting up tracer. + if (string.IsNullOrEmpty(gitBinRoot)) + { + return; + } + + TelemetryDaemonEventListener daemonListener = TelemetryDaemonEventListener.CreateIfEnabled(gitBinRoot, providerName, enlistmentId, mountId, this); + if (daemonListener != null) + { + this.listeners.Add(daemonListener); + } + } + } + + private JsonTracer(ConcurrentBag listeners, Guid parentActivityId, string activityName, EventLevel startStopLevel, Keywords startStopKeywords) + { + this.listeners = listeners ?? new ConcurrentBag(); + this.parentActivityId = parentActivityId; + this.activityName = activityName; + this.startStopLevel = startStopLevel; + this.startStopKeywords = startStopKeywords; + + this.activityId = Guid.NewGuid(); + } + + public bool HasLogFileEventListener + { + get + { + return this.listeners.Any(listener => listener is LogFileEventListener); + } + } + + public void SetGitCommandSessionId(string sessionId) + { + TelemetryDaemonEventListener daemonListener = this.listeners.FirstOrDefault(x => x is TelemetryDaemonEventListener) as TelemetryDaemonEventListener; + if (daemonListener != null) + { + daemonListener.GitCommandSessionId = sessionId; + } + } + + public void AddEventListener(EventListener listener) + { + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(JsonTracer)); + } + + this.listeners.Add(listener); + + // Tell the new listener about others who have previously failed + foreach (KeyValuePair kvp in this.failedListeners) + { + TraceEventMessage failureMessage = CreateListenerFailureMessage(kvp.Key, kvp.Value); + listener.RecordMessage(failureMessage); + } + } + + public void AddDiagnosticConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) + { + this.AddEventListener(new DiagnosticConsoleEventListener(maxVerbosity, keywordFilter, this)); + } + + public void AddPrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter) + { + this.AddEventListener(new PrettyConsoleEventListener(maxVerbosity, keywordFilter, this)); + } + + public void AddLogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter) + { + this.AddEventListener(new LogFileEventListener(logFilePath, maxVerbosity, keywordFilter, this)); + } + + public void Dispose() + { + if (this.isDisposed) + { + // This instance has already been disposed + return; + } + + this.Stop(null); + + // If we have no parent, then we are the root tracer and should dispose our eventsource. + if (this.parentActivityId == Guid.Empty) + { + // Empty the listener bag and dispose of the instances as we remove them. + EventListener listener; + while (this.listeners.TryTake(out listener)) + { + listener.Dispose(); + } + } + + this.isDisposed = true; + } + + public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata) + { + this.RelatedEvent(level, eventName, metadata, Keywords.None); + } + + public virtual void RelatedEvent(EventLevel level, string eventName, EventMetadata metadata, Keywords keyword) + { + this.WriteEvent(eventName, level, keyword, metadata, opcode: 0); + } + + public virtual void RelatedInfo(string format, params object[] args) + { + this.RelatedInfo(string.Format(format, args)); + } + + public virtual void RelatedInfo(string message) + { + this.RelatedInfo(new EventMetadata(), message); + } + + public virtual void RelatedInfo(EventMetadata metadata, string message) + { + metadata = metadata ?? new EventMetadata(); + metadata.Add(TracingConstants.MessageKey.InfoMessage, message); + this.RelatedEvent(EventLevel.Informational, "Information", metadata); + } + + public virtual void RelatedWarning(EventMetadata metadata, string message) + { + this.RelatedWarning(metadata, message, Keywords.None); + } + + public virtual void RelatedWarning(EventMetadata metadata, string message, Keywords keywords) + { + metadata = metadata ?? new EventMetadata(); + metadata[TracingConstants.MessageKey.WarningMessage] = message; + this.RelatedEvent(EventLevel.Warning, "Warning", metadata, keywords); + } + + public virtual void RelatedWarning(string message) + { + EventMetadata metadata = new EventMetadata(); + this.RelatedWarning(metadata, message); + } + + public virtual void RelatedWarning(string format, params object[] args) + { + this.RelatedWarning(string.Format(format, args)); + } + + public virtual void RelatedError(EventMetadata metadata, string message) + { + this.RelatedError(metadata, message, Keywords.Telemetry); + } + + public virtual void RelatedError(EventMetadata metadata, string message, Keywords keywords) + { + metadata = metadata ?? new EventMetadata(); + metadata[TracingConstants.MessageKey.ErrorMessage] = message; + this.RelatedEvent(EventLevel.Error, GetCategorizedErrorEventName(keywords), metadata, keywords | Keywords.Telemetry); + } + + public virtual void RelatedError(string message) + { + EventMetadata metadata = new EventMetadata(); + this.RelatedError(metadata, message); + } + + public virtual void RelatedError(string format, params object[] args) + { + this.RelatedError(string.Format(format, args)); + } + + public TimeSpan Stop(EventMetadata metadata) + { + if (this.stopped) + { + return TimeSpan.Zero; + } + + this.duration.Stop(); + this.stopped = true; + + metadata = metadata ?? new EventMetadata(); + metadata.Add("DurationMs", this.duration.ElapsedMilliseconds); + + this.WriteEvent(this.activityName, this.startStopLevel, this.startStopKeywords, metadata, EventOpcode.Stop); + + return this.duration.Elapsed; + } + + public ITracer StartActivity(string childActivityName, EventLevel startStopLevel) + { + return this.StartActivity(childActivityName, startStopLevel, null); + } + + public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, EventMetadata startMetadata) + { + return this.StartActivity(childActivityName, startStopLevel, Keywords.None, startMetadata); + } + + public ITracer StartActivity(string childActivityName, EventLevel startStopLevel, Keywords startStopKeywords, EventMetadata startMetadata) + { + JsonTracer subTracer = new JsonTracer(this.listeners, this.activityId, childActivityName, startStopLevel, startStopKeywords); + + // Write the start event, disabling the Telemetry keyword so we will only dispatch telemetry at the end event. + subTracer.WriteStartEvent(startMetadata, startStopKeywords & ~Keywords.Telemetry); + + return subTracer; + } + + public void WriteStartEvent( + string enlistmentRoot, + string repoUrl, + string cacheServerUrl, + EventMetadata additionalMetadata = null) + { + EventMetadata metadata = new EventMetadata(); + + metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); + + if (enlistmentRoot != null) + { + metadata.Add("EnlistmentRoot", enlistmentRoot); + } + + if (repoUrl != null) + { + metadata.Add("Remote", Uri.EscapeUriString(repoUrl)); + } + + if (cacheServerUrl != null) + { + // Changing this key to CacheServerUrl will mess with our telemetry, so it stays for historical reasons + metadata.Add("ObjectsEndpoint", Uri.EscapeUriString(cacheServerUrl)); + } + + if (additionalMetadata != null) + { + foreach (string key in additionalMetadata.Keys) + { + metadata.Add(key, additionalMetadata[key]); + } + } + + this.WriteStartEvent(metadata, Keywords.Telemetry); + } + + public void WriteStartEvent(EventMetadata metadata, Keywords keywords) + { + this.WriteEvent(this.activityName, this.startStopLevel, keywords, metadata, EventOpcode.Start); + } + + void IEventListenerEventSink.OnListenerRecovery(EventListener listener) + { + // Check ContainsKey first (rather than always calling TryRemove) because ContainsKey + // is lock-free and recoveredListener should rarely be in failedListeners + if (!this.failedListeners.ContainsKey(listener)) + { + // This listener has not failed since the last time it was called, so no need to log recovery + return; + } + + if (this.failedListeners.TryRemove(listener, out _)) + { + TraceEventMessage message = CreateListenerRecoveryMessage(listener); + this.LogMessageToNonFailedListeners(message); + } + } + + void IEventListenerEventSink.OnListenerFailure(EventListener listener, string errorMessage) + { + if (!this.failedListeners.TryAdd(listener, errorMessage)) + { + // We've already logged that this listener has failed so there is no need to do it again + return; + } + + TraceEventMessage message = CreateListenerFailureMessage(listener, errorMessage); + this.LogMessageToNonFailedListeners(message); + } + + private static string GetCategorizedErrorEventName(Keywords keywords) + { + switch (keywords) + { + case Keywords.Network: return NetworkErrorEventName; + default: return "Error"; + } + } + + private static TraceEventMessage CreateListenerRecoveryMessage(EventListener recoveredListener) + { + return new TraceEventMessage + { + EventName = "TraceEventListenerRecovery", + Level = EventLevel.Informational, + Keywords = Keywords.Any, + Opcode = EventOpcode.Info, + Payload = JsonConvert.SerializeObject(new Dictionary + { + ["EventListener"] = recoveredListener.GetType().Name + }) + }; + } + + private static TraceEventMessage CreateListenerFailureMessage(EventListener failedListener, string errorMessage) + { + return new TraceEventMessage + { + EventName = "TraceEventListenerFailure", + Level = EventLevel.Error, + Keywords = Keywords.Any, + Opcode = EventOpcode.Info, + Payload = JsonConvert.SerializeObject(new Dictionary + { + ["EventListener"] = failedListener.GetType().Name, + ["ErrorMessage"] = errorMessage, + }) + }; + } + + private void WriteEvent(string eventName, EventLevel level, Keywords keywords, EventMetadata metadata, EventOpcode opcode) + { + string jsonPayload = metadata != null ? JsonConvert.SerializeObject(metadata) : null; + + if (this.isDisposed) + { + throw new ObjectDisposedException(nameof(JsonTracer)); + } + + var message = new TraceEventMessage + { + EventName = eventName, + ActivityId = this.activityId, + ParentActivityId = this.parentActivityId, + Level = level, + Keywords = keywords, + Opcode = opcode, + Payload = jsonPayload + }; + + // Iterating over the bag is thread-safe as the enumerator returned here + // is of a snapshot of the bag. + foreach (EventListener listener in this.listeners) + { + listener.RecordMessage(message); + } + } + + private void LogMessageToNonFailedListeners(TraceEventMessage message) + { + foreach (EventListener listener in this.listeners.Except(this.failedListeners.Keys)) + { + // To prevent infinitely recursive failures, we won't try and log that we failed to log that a listener failed :) + listener.RecordMessage(message); + } + } + } +} diff --git a/Scalar.Common/Tracing/Keywords.cs b/Scalar.Common/Tracing/Keywords.cs index 51ce557d66..29513f50f1 100644 --- a/Scalar.Common/Tracing/Keywords.cs +++ b/Scalar.Common/Tracing/Keywords.cs @@ -1,12 +1,12 @@ -namespace Scalar.Common.Tracing -{ - public enum Keywords : long - { - None = 1 << 0, - Network = 1 << 1, - DEPRECATED = 1 << 2, - Telemetry = 1 << 3, - - Any = ~0, - } +namespace Scalar.Common.Tracing +{ + public enum Keywords : long + { + None = 1 << 0, + Network = 1 << 1, + DEPRECATED = 1 << 2, + Telemetry = 1 << 3, + + Any = ~0, + } } diff --git a/Scalar.Common/Tracing/LogFileEventListener.cs b/Scalar.Common/Tracing/LogFileEventListener.cs index 0593a1b8ba..eb7ad126da 100644 --- a/Scalar.Common/Tracing/LogFileEventListener.cs +++ b/Scalar.Common/Tracing/LogFileEventListener.cs @@ -1,46 +1,46 @@ -using System; -using System.IO; - -namespace Scalar.Common.Tracing -{ - public class LogFileEventListener : EventListener - { - private FileStream logFile; - private TextWriter writer; - - public LogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) - : base(maxVerbosity, keywordFilter, eventSink) - { - this.SetLogFilePath(logFilePath); - } - - public override void Dispose() - { - if (this.writer != null) - { - this.writer.Dispose(); - this.writer = null; - } - - if (this.logFile != null) - { - this.logFile.Dispose(); - this.logFile = null; - } - } - - protected override void RecordMessageInternal(TraceEventMessage message) - { - this.writer.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); - this.writer.Flush(); - } - - protected void SetLogFilePath(string newfilePath) - { - Directory.CreateDirectory(Path.GetDirectoryName(newfilePath)); - this.logFile = File.Open(newfilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); - this.logFile.Seek(0, SeekOrigin.End); - this.writer = StreamWriter.Synchronized(new StreamWriter(this.logFile)); - } - } -} +using System; +using System.IO; + +namespace Scalar.Common.Tracing +{ + public class LogFileEventListener : EventListener + { + private FileStream logFile; + private TextWriter writer; + + public LogFileEventListener(string logFilePath, EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) + : base(maxVerbosity, keywordFilter, eventSink) + { + this.SetLogFilePath(logFilePath); + } + + public override void Dispose() + { + if (this.writer != null) + { + this.writer.Dispose(); + this.writer = null; + } + + if (this.logFile != null) + { + this.logFile.Dispose(); + this.logFile = null; + } + } + + protected override void RecordMessageInternal(TraceEventMessage message) + { + this.writer.WriteLine(this.GetLogString(message.EventName, message.Opcode, message.Payload)); + this.writer.Flush(); + } + + protected void SetLogFilePath(string newfilePath) + { + Directory.CreateDirectory(Path.GetDirectoryName(newfilePath)); + this.logFile = File.Open(newfilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); + this.logFile.Seek(0, SeekOrigin.End); + this.writer = StreamWriter.Synchronized(new StreamWriter(this.logFile)); + } + } +} diff --git a/Scalar.Common/Tracing/PrettyConsoleEventListener.cs b/Scalar.Common/Tracing/PrettyConsoleEventListener.cs index cad23eafce..8b1781d41a 100644 --- a/Scalar.Common/Tracing/PrettyConsoleEventListener.cs +++ b/Scalar.Common/Tracing/PrettyConsoleEventListener.cs @@ -1,68 +1,68 @@ -using System; -using Newtonsoft.Json; - -namespace Scalar.Common.Tracing -{ - /// - /// An event listener that will print any message that it can nicely format for the console - /// that matches the verbosity level it is given. At the moment, this means only messages - /// with an "ErrorMessage" attribute will get displayed. - /// - public class PrettyConsoleEventListener : EventListener - { - private static object consoleLock = new object(); - - public PrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) - : base(maxVerbosity, keywordFilter, eventSink) - { - } - - protected override void RecordMessageInternal(TraceEventMessage message) - { - if (string.IsNullOrEmpty(message.Payload)) - { - return; - } - - ConsoleOutputPayload payload = JsonConvert.DeserializeObject(message.Payload); - if (string.IsNullOrEmpty(payload.ErrorMessage)) - { - return; - } - - // It's necessary to do a lock here because this can be called in a multi-threaded - // environment and we want to make sure that ForegroundColor is restored correctly. - lock (consoleLock) - { - ConsoleColor prevColor = Console.ForegroundColor; - string prefix; - switch (message.Level) - { - case EventLevel.Critical: - case EventLevel.Error: - case EventLevel.LogAlways: - prefix = "Error"; - Console.ForegroundColor = ConsoleColor.Red; - break; - case EventLevel.Warning: - prefix = "Warning"; - Console.ForegroundColor = ConsoleColor.Yellow; - break; - default: - prefix = "Info"; - break; - } - - // The leading \r interacts with the spinner, which always leaves the - // cursor at the end of the line, rather than the start. - Console.WriteLine($"\r{prefix}: {payload.ErrorMessage}"); - Console.ForegroundColor = prevColor; - } - } - - private class ConsoleOutputPayload - { - public string ErrorMessage { get; set; } - } - } +using System; +using Newtonsoft.Json; + +namespace Scalar.Common.Tracing +{ + /// + /// An event listener that will print any message that it can nicely format for the console + /// that matches the verbosity level it is given. At the moment, this means only messages + /// with an "ErrorMessage" attribute will get displayed. + /// + public class PrettyConsoleEventListener : EventListener + { + private static object consoleLock = new object(); + + public PrettyConsoleEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) + : base(maxVerbosity, keywordFilter, eventSink) + { + } + + protected override void RecordMessageInternal(TraceEventMessage message) + { + if (string.IsNullOrEmpty(message.Payload)) + { + return; + } + + ConsoleOutputPayload payload = JsonConvert.DeserializeObject(message.Payload); + if (string.IsNullOrEmpty(payload.ErrorMessage)) + { + return; + } + + // It's necessary to do a lock here because this can be called in a multi-threaded + // environment and we want to make sure that ForegroundColor is restored correctly. + lock (consoleLock) + { + ConsoleColor prevColor = Console.ForegroundColor; + string prefix; + switch (message.Level) + { + case EventLevel.Critical: + case EventLevel.Error: + case EventLevel.LogAlways: + prefix = "Error"; + Console.ForegroundColor = ConsoleColor.Red; + break; + case EventLevel.Warning: + prefix = "Warning"; + Console.ForegroundColor = ConsoleColor.Yellow; + break; + default: + prefix = "Info"; + break; + } + + // The leading \r interacts with the spinner, which always leaves the + // cursor at the end of the line, rather than the start. + Console.WriteLine($"\r{prefix}: {payload.ErrorMessage}"); + Console.ForegroundColor = prevColor; + } + } + + private class ConsoleOutputPayload + { + public string ErrorMessage { get; set; } + } + } } diff --git a/Scalar.Common/Tracing/TelemetryDaemonEventListener.cs b/Scalar.Common/Tracing/TelemetryDaemonEventListener.cs index 1c4fda6e1c..df0267e1db 100644 --- a/Scalar.Common/Tracing/TelemetryDaemonEventListener.cs +++ b/Scalar.Common/Tracing/TelemetryDaemonEventListener.cs @@ -1,168 +1,168 @@ -using Newtonsoft.Json; -using Scalar.Common.Git; -using System; -using System.IO.Pipes; - -namespace Scalar.Common.Tracing -{ - public class TelemetryDaemonEventListener : EventListener, IQueuedPipeStringWriterEventSink - { - private readonly string providerName; - private readonly string enlistmentId; - private readonly string mountId; - private readonly string vfsVersion; - - private QueuedPipeStringWriter pipeWriter; - - private TelemetryDaemonEventListener( - string providerName, - string enlistmentId, - string mountId, - string pipeName, - IEventListenerEventSink eventSink) - : base(EventLevel.Verbose, Keywords.Telemetry, eventSink) - { - this.providerName = providerName; - this.enlistmentId = enlistmentId; - this.mountId = mountId; - this.vfsVersion = ProcessHelper.GetCurrentProcessVersion(); - - this.pipeWriter = new QueuedPipeStringWriter( - () => new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous), - this); - this.pipeWriter.Start(); - } - - public string GitCommandSessionId { get; set; } - - public static TelemetryDaemonEventListener CreateIfEnabled(string gitBinRoot, string providerName, string enlistmentId, string mountId, IEventListenerEventSink eventSink) - { - // This listener is disabled unless the user specifies the proper git config setting. - string telemetryPipe = GetConfigValue(gitBinRoot, ScalarConstants.GitConfig.ScalarTelemetryPipe); - if (!string.IsNullOrEmpty(telemetryPipe)) - { - return new TelemetryDaemonEventListener(providerName, enlistmentId, mountId, telemetryPipe, eventSink); - } - else - { - return null; - } - } - - public override void Dispose() - { - if (this.pipeWriter != null) - { - this.pipeWriter.Stop(); - this.pipeWriter.Dispose(); - this.pipeWriter = null; - } - - base.Dispose(); - } - - void IQueuedPipeStringWriterEventSink.OnStateChanged( - QueuedPipeStringWriter writer, - QueuedPipeStringWriterState state, - Exception exception) - { - switch (state) - { - case QueuedPipeStringWriterState.Failing: - this.RaiseListenerFailure(exception?.ToString()); - break; - case QueuedPipeStringWriterState.Healthy: - this.RaiseListenerRecovery(); - break; - } - } - - protected override void RecordMessageInternal(TraceEventMessage message) - { - string pipeMessage = this.CreatePipeMessage(message); - - bool dropped = !this.pipeWriter.TryEnqueue(pipeMessage); - - if (dropped) - { - this.RaiseListenerFailure("Pipe delivery queue is full. Message was dropped."); - } - } - - private static string GetConfigValue(string gitBinRoot, string configKey) - { - string value = string.Empty; - string error; - - GitProcess.ConfigResult result = GitProcess.GetFromSystemConfig(gitBinRoot, configKey); - if (!result.TryParseAsString(out value, out error, defaultValue: string.Empty) || string.IsNullOrWhiteSpace(value)) - { - result = GitProcess.GetFromGlobalConfig(gitBinRoot, configKey); - result.TryParseAsString(out value, out error, defaultValue: string.Empty); - } - - return value.TrimEnd('\r', '\n'); - } - - private string CreatePipeMessage(TraceEventMessage message) - { - var pipeMessage = new PipeMessage - { - Version = this.vfsVersion, - ProviderName = this.providerName, - EventName = message.EventName, - EventLevel = message.Level, - EventOpcode = message.Opcode, - Payload = new PipeMessage.PipeMessagePayload - { - EnlistmentId = this.enlistmentId, - MountId = this.mountId, - GitCommandSessionId = this.GitCommandSessionId, - Json = message.Payload - }, - - // Other TraceEventMessage properties are not used - }; - - return pipeMessage.ToJson(); - } - - public class PipeMessage - { - [JsonProperty("version")] - public string Version { get; set; } - [JsonProperty("providerName")] - public string ProviderName { get; set; } - [JsonProperty("eventName")] - public string EventName { get; set; } - [JsonProperty("eventLevel")] - public EventLevel EventLevel { get; set; } - [JsonProperty("eventOpcode")] - public EventOpcode EventOpcode { get; set; } - [JsonProperty("payload")] - public PipeMessagePayload Payload { get; set; } - - public static PipeMessage FromJson(string json) - { - return JsonConvert.DeserializeObject(json); - } - - public string ToJson() - { - return JsonConvert.SerializeObject(this); - } - - public class PipeMessagePayload - { - [JsonProperty("enlistmentId")] - public string EnlistmentId { get; set; } - [JsonProperty("mountId")] - public string MountId { get; set; } - [JsonProperty("gitCommandSessionId")] - public string GitCommandSessionId { get; set; } - [JsonProperty("json")] - public string Json { get; set; } - } - } - } -} +using Newtonsoft.Json; +using Scalar.Common.Git; +using System; +using System.IO.Pipes; + +namespace Scalar.Common.Tracing +{ + public class TelemetryDaemonEventListener : EventListener, IQueuedPipeStringWriterEventSink + { + private readonly string providerName; + private readonly string enlistmentId; + private readonly string mountId; + private readonly string vfsVersion; + + private QueuedPipeStringWriter pipeWriter; + + private TelemetryDaemonEventListener( + string providerName, + string enlistmentId, + string mountId, + string pipeName, + IEventListenerEventSink eventSink) + : base(EventLevel.Verbose, Keywords.Telemetry, eventSink) + { + this.providerName = providerName; + this.enlistmentId = enlistmentId; + this.mountId = mountId; + this.vfsVersion = ProcessHelper.GetCurrentProcessVersion(); + + this.pipeWriter = new QueuedPipeStringWriter( + () => new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous), + this); + this.pipeWriter.Start(); + } + + public string GitCommandSessionId { get; set; } + + public static TelemetryDaemonEventListener CreateIfEnabled(string gitBinRoot, string providerName, string enlistmentId, string mountId, IEventListenerEventSink eventSink) + { + // This listener is disabled unless the user specifies the proper git config setting. + string telemetryPipe = GetConfigValue(gitBinRoot, ScalarConstants.GitConfig.ScalarTelemetryPipe); + if (!string.IsNullOrEmpty(telemetryPipe)) + { + return new TelemetryDaemonEventListener(providerName, enlistmentId, mountId, telemetryPipe, eventSink); + } + else + { + return null; + } + } + + public override void Dispose() + { + if (this.pipeWriter != null) + { + this.pipeWriter.Stop(); + this.pipeWriter.Dispose(); + this.pipeWriter = null; + } + + base.Dispose(); + } + + void IQueuedPipeStringWriterEventSink.OnStateChanged( + QueuedPipeStringWriter writer, + QueuedPipeStringWriterState state, + Exception exception) + { + switch (state) + { + case QueuedPipeStringWriterState.Failing: + this.RaiseListenerFailure(exception?.ToString()); + break; + case QueuedPipeStringWriterState.Healthy: + this.RaiseListenerRecovery(); + break; + } + } + + protected override void RecordMessageInternal(TraceEventMessage message) + { + string pipeMessage = this.CreatePipeMessage(message); + + bool dropped = !this.pipeWriter.TryEnqueue(pipeMessage); + + if (dropped) + { + this.RaiseListenerFailure("Pipe delivery queue is full. Message was dropped."); + } + } + + private static string GetConfigValue(string gitBinRoot, string configKey) + { + string value = string.Empty; + string error; + + GitProcess.ConfigResult result = GitProcess.GetFromSystemConfig(gitBinRoot, configKey); + if (!result.TryParseAsString(out value, out error, defaultValue: string.Empty) || string.IsNullOrWhiteSpace(value)) + { + result = GitProcess.GetFromGlobalConfig(gitBinRoot, configKey); + result.TryParseAsString(out value, out error, defaultValue: string.Empty); + } + + return value.TrimEnd('\r', '\n'); + } + + private string CreatePipeMessage(TraceEventMessage message) + { + var pipeMessage = new PipeMessage + { + Version = this.vfsVersion, + ProviderName = this.providerName, + EventName = message.EventName, + EventLevel = message.Level, + EventOpcode = message.Opcode, + Payload = new PipeMessage.PipeMessagePayload + { + EnlistmentId = this.enlistmentId, + MountId = this.mountId, + GitCommandSessionId = this.GitCommandSessionId, + Json = message.Payload + }, + + // Other TraceEventMessage properties are not used + }; + + return pipeMessage.ToJson(); + } + + public class PipeMessage + { + [JsonProperty("version")] + public string Version { get; set; } + [JsonProperty("providerName")] + public string ProviderName { get; set; } + [JsonProperty("eventName")] + public string EventName { get; set; } + [JsonProperty("eventLevel")] + public EventLevel EventLevel { get; set; } + [JsonProperty("eventOpcode")] + public EventOpcode EventOpcode { get; set; } + [JsonProperty("payload")] + public PipeMessagePayload Payload { get; set; } + + public static PipeMessage FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public string ToJson() + { + return JsonConvert.SerializeObject(this); + } + + public class PipeMessagePayload + { + [JsonProperty("enlistmentId")] + public string EnlistmentId { get; set; } + [JsonProperty("mountId")] + public string MountId { get; set; } + [JsonProperty("gitCommandSessionId")] + public string GitCommandSessionId { get; set; } + [JsonProperty("json")] + public string Json { get; set; } + } + } + } +} diff --git a/Scalar.Common/Tracing/TraceEventMessage.cs b/Scalar.Common/Tracing/TraceEventMessage.cs index 9e6fb65ff1..191242b1f3 100644 --- a/Scalar.Common/Tracing/TraceEventMessage.cs +++ b/Scalar.Common/Tracing/TraceEventMessage.cs @@ -1,15 +1,15 @@ -using System; - -namespace Scalar.Common.Tracing -{ - public class TraceEventMessage - { - public string EventName { get; set; } - public Guid ActivityId { get; set; } - public Guid ParentActivityId { get; set; } - public EventLevel Level { get; set; } - public Keywords Keywords { get; set; } - public EventOpcode Opcode { get; set; } - public string Payload { get; set; } - } -} +using System; + +namespace Scalar.Common.Tracing +{ + public class TraceEventMessage + { + public string EventName { get; set; } + public Guid ActivityId { get; set; } + public Guid ParentActivityId { get; set; } + public EventLevel Level { get; set; } + public Keywords Keywords { get; set; } + public EventOpcode Opcode { get; set; } + public string Payload { get; set; } + } +} diff --git a/Scalar.Common/Tracing/TracingConstants.cs b/Scalar.Common/Tracing/TracingConstants.cs index de179bc7c0..643eddd6ef 100644 --- a/Scalar.Common/Tracing/TracingConstants.cs +++ b/Scalar.Common/Tracing/TracingConstants.cs @@ -1,15 +1,15 @@ -namespace Scalar.Common.Tracing -{ - public static class TracingConstants - { - public static class MessageKey - { - public const string LogAlwaysMessage = ErrorMessage; - public const string CriticalMessage = ErrorMessage; - public const string ErrorMessage = "ErrorMessage"; - public const string WarningMessage = "WarningMessage"; - public const string InfoMessage = "Message"; - public const string VerboseMessage = InfoMessage; - } - } -} +namespace Scalar.Common.Tracing +{ + public static class TracingConstants + { + public static class MessageKey + { + public const string LogAlwaysMessage = ErrorMessage; + public const string CriticalMessage = ErrorMessage; + public const string ErrorMessage = "ErrorMessage"; + public const string WarningMessage = "WarningMessage"; + public const string InfoMessage = "Message"; + public const string VerboseMessage = InfoMessage; + } + } +} diff --git a/Scalar.FunctionalTests/AssemblyAttributes.cs b/Scalar.FunctionalTests/AssemblyAttributes.cs index b00af1e2c1..9ae285d9ea 100644 --- a/Scalar.FunctionalTests/AssemblyAttributes.cs +++ b/Scalar.FunctionalTests/AssemblyAttributes.cs @@ -1,3 +1,3 @@ -using NUnit.Framework; - -[assembly: Parallelizable(ParallelScope.Fixtures)] +using NUnit.Framework; + +[assembly: Parallelizable(ParallelScope.Fixtures)] diff --git a/Scalar.FunctionalTests/Categories.cs b/Scalar.FunctionalTests/Categories.cs index ef64b6be52..58aa560ae8 100644 --- a/Scalar.FunctionalTests/Categories.cs +++ b/Scalar.FunctionalTests/Categories.cs @@ -1,39 +1,39 @@ -namespace Scalar.FunctionalTests -{ - public static class Categories - { - public const string ExtraCoverage = "ExtraCoverage"; - public const string FastFetch = "FastFetch"; - public const string GitCommands = "GitCommands"; - - public const string WindowsOnly = "WindowsOnly"; - public const string MacOnly = "MacOnly"; - - public const string NeedsUpdatesForNonVirtualizedMode = "NeedsUpdatesForNonVirtualizedMode"; - - public static class MacTODO - { - // Tests that require #360 (detecting/handling new empty folders) - public const string NeedsNewFolderCreateNotification = "NeedsNewFolderCreateNotification"; - - // Tests that require the Status Cache to be built - public const string NeedsStatusCache = "NeedsStatusCache"; - - // Tests that require Config to be built - public const string NeedsScalarConfig = "NeedsConfig"; - - // Tests that require Scalar Service - public const string NeedsServiceVerb = "NeedsServiceVerb"; - - // Requires both functional and test fixes - public const string NeedsDehydrate = "NeedsDehydrate"; - - // Tests requires code updates so that we lock the file instead of looking for a .lock file - public const string TestNeedsToLockFile = "TestNeedsToLockFile"; - - // Tests that have been flaky on build servers and need additional logging and\or - // investigation - public const string FlakyTest = "MacFlakyTest"; - } - } -} +namespace Scalar.FunctionalTests +{ + public static class Categories + { + public const string ExtraCoverage = "ExtraCoverage"; + public const string FastFetch = "FastFetch"; + public const string GitCommands = "GitCommands"; + + public const string WindowsOnly = "WindowsOnly"; + public const string MacOnly = "MacOnly"; + + public const string NeedsUpdatesForNonVirtualizedMode = "NeedsUpdatesForNonVirtualizedMode"; + + public static class MacTODO + { + // Tests that require #360 (detecting/handling new empty folders) + public const string NeedsNewFolderCreateNotification = "NeedsNewFolderCreateNotification"; + + // Tests that require the Status Cache to be built + public const string NeedsStatusCache = "NeedsStatusCache"; + + // Tests that require Config to be built + public const string NeedsScalarConfig = "NeedsConfig"; + + // Tests that require Scalar Service + public const string NeedsServiceVerb = "NeedsServiceVerb"; + + // Requires both functional and test fixes + public const string NeedsDehydrate = "NeedsDehydrate"; + + // Tests requires code updates so that we lock the file instead of looking for a .lock file + public const string TestNeedsToLockFile = "TestNeedsToLockFile"; + + // Tests that have been flaky on build servers and need additional logging and\or + // investigation + public const string FlakyTest = "MacFlakyTest"; + } + } +} diff --git a/Scalar.FunctionalTests/FileSystemRunners/BashRunner.cs b/Scalar.FunctionalTests/FileSystemRunners/BashRunner.cs index 62f072250b..80dd7295cd 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/BashRunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/BashRunner.cs @@ -1,350 +1,350 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.Tests.Should; -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public class BashRunner : ShellRunner - { - private static string[] fileNotFoundMessages = new string[] - { - "cannot stat", - "cannot remove", - "No such file or directory" - }; - - private static string[] invalidMovePathMessages = new string[] - { - "cannot move", - "No such file or directory" - }; - - private static string[] moveDirectoryNotSupportedMessage = new string[] - { - "Function not implemented" - }; - - private static string[] windowsPermissionDeniedMessage = new string[] - { - "Permission denied" - }; - - private static string[] macPermissionDeniedMessage = new string[] - { - "Resource temporarily unavailable" - }; - - private readonly string pathToBash; - - public BashRunner() - { - if (File.Exists(Settings.Default.PathToBash)) - { - this.pathToBash = Settings.Default.PathToBash; - } - else - { - this.pathToBash = "bash.exe"; - } - } - - private enum FileType - { - Invalid, - File, - Directory, - SymLink, - } - - protected override string FileName - { - get - { - return this.pathToBash; - } - } - - public static void DeleteDirectoryWithUnlimitedRetries(string path) - { - BashRunner runner = new BashRunner(); - bool pathExists = Directory.Exists(path); - int retryCount = 0; - while (pathExists) - { - string output = runner.DeleteDirectory(path); - pathExists = Directory.Exists(path); - if (pathExists) - { - ++retryCount; - Thread.Sleep(500); - if (retryCount > 10) - { - retryCount = 0; - if (Debugger.IsAttached) - { - Debugger.Break(); - } - } - } - } - } - - public bool IsSymbolicLink(string path) - { - return this.FileExistsOnDisk(path, FileType.SymLink); - } - - public void CreateSymbolicLink(string newLinkFilePath, string existingFilePath) - { - string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); - string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); - - this.RunProcess(string.Format("-c \"ln -s -F '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); - } - - public override bool FileExists(string path) - { - return this.FileExistsOnDisk(path, FileType.File); - } - - public override string MoveFile(string sourcePath, string targetPath) - { - string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); - string targetBashPath = this.ConvertWinPathToBashPath(targetPath); - - return this.RunProcess(string.Format("-c \"mv '{0}' '{1}'\"", sourceBashPath, targetBashPath)); - } - - public override void MoveFileShouldFail(string sourcePath, string targetPath) - { - // BashRunner does nothing special when a failure is expected, so just confirm source file is still present - this.MoveFile(sourcePath, targetPath); - this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); - } - - public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); - } - - public override string ReplaceFile(string sourcePath, string targetPath) - { - string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); - string targetBashPath = this.ConvertWinPathToBashPath(targetPath); - - return this.RunProcess(string.Format("-c \"mv -f '{0}' '{1}'\"", sourceBashPath, targetBashPath)); - } - - public override string DeleteFile(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - return this.RunProcess(string.Format("-c \"rm '{0}'\"", bashPath)); - } - - public override string ReadAllText(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - string output = this.RunProcess(string.Format("-c \"cat '{0}'\"", bashPath)); - - // Bash sometimes sticks a trailing "\n" at the end of the output that we need to remove - // Until we can figure out why we cannot use this runner with files that have trailing newlines - if (output.Length > 0 && - output.Substring(output.Length - 1).Equals("\n", StringComparison.InvariantCultureIgnoreCase) && - !(output.Length > 1 && - output.Substring(output.Length - 2).Equals("\r\n", StringComparison.InvariantCultureIgnoreCase))) - { - output = output.Remove(output.Length - 1, 1); - } - - return output; - } - - public override void AppendAllText(string path, string contents) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - this.RunProcess(string.Format("-c \"echo -n \\\"{0}\\\" >> '{1}'\"", contents, bashPath)); - } - - public override void CreateEmptyFile(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - this.RunProcess(string.Format("-c \"touch '{0}'\"", bashPath)); - } - - public override void CreateHardLink(string newLinkFilePath, string existingFilePath) - { - string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); - string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); - - this.RunProcess(string.Format("-c \"ln '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); - } - - public override void WriteAllText(string path, string contents) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - this.RunProcess(string.Format("-c \"echo \\\"{0}\\\" > '{1}'\"", contents, bashPath)); - } - - public override void WriteAllTextShouldFail(string path, string contents) - { - // BashRunner does nothing special when a failure is expected - this.WriteAllText(path, contents); - } - - public override bool DirectoryExists(string path) - { - return this.FileExistsOnDisk(path, FileType.Directory); - } - - public override void MoveDirectory(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath); - } - - public override void RenameDirectory(string workingDirectory, string source, string target) - { - this.MoveDirectory(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target)); - } - - public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); - } - - public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(invalidMovePathMessages); - } - - public override void CreateDirectory(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - this.RunProcess(string.Format("-c \"mkdir '{0}'\"", bashPath)); - } - - public override string DeleteDirectory(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - return this.RunProcess(string.Format("-c \"rm -rf '{0}'\"", bashPath)); - } - - public override string EnumerateDirectory(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - return this.RunProcess(string.Format("-c \"ls '{0}'\"", bashPath)); - } - - public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); - } - - public override void DeleteFile_FileShouldNotBeFound(string path) - { - this.DeleteFile(path).ShouldContainOneOf(fileNotFoundMessages); - } - - public override void DeleteFile_AccessShouldBeDenied(string path) - { - // bash does not report any error messages when access is denied, so just confirm the file still exists - this.DeleteFile(path); - this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); - } - - public override void ReadAllText_FileShouldNotBeFound(string path) - { - this.ReadAllText(path).ShouldContainOneOf(fileNotFoundMessages); - } - - public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) - { - // Delete directory silently succeeds when deleting a non-existent path - this.DeleteDirectory(path); - } - - public override void ChangeMode(string path, ushort mode) - { - string octalMode = Convert.ToString(mode, 8); - string bashPath = this.ConvertWinPathToBashPath(path); - string command = $"-c \"chmod {octalMode} '{bashPath}'\""; - this.RunProcess(command); - } - - public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) - { - Assert.Fail("Unlike the other runners, bash.exe does not check folder handle before recusively deleting"); - } - - public override long FileSize(string path) - { - string bashPath = this.ConvertWinPathToBashPath(path); - - string statCommand = null; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - statCommand = string.Format("-c \"stat -f \"%z\" '{0}'\"", bashPath); - } - else - { - statCommand = string.Format("-c \"stat --format \"%s\" '{0}'\"", bashPath); - } - - return long.Parse(this.RunProcess(statCommand)); - } - - public override void CreateFileWithoutClose(string path) - { - throw new NotImplementedException(); - } - - public override void OpenFileAndWriteWithoutClose(string path, string data) - { - throw new NotImplementedException(); - } - - private bool FileExistsOnDisk(string path, FileType type) - { - string checkArgument = string.Empty; - switch (type) - { - case FileType.File: - checkArgument = "-f"; - break; - case FileType.Directory: - checkArgument = "-d"; - break; - case FileType.SymLink: - checkArgument = "-h"; - break; - default: - Assert.Fail($"{nameof(this.FileExistsOnDisk)} does not support {nameof(FileType)} {type}"); - break; - } - - string bashPath = this.ConvertWinPathToBashPath(path); - string command = $"-c \"[ {checkArgument} '{bashPath}' ] && echo {ShellRunner.SuccessOutput} || echo {ShellRunner.FailureOutput}\""; - string output = this.RunProcess(command).Trim(); - return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); - } - - private string ConvertWinPathToBashPath(string winPath) - { - string bashPath = string.Concat("/", winPath); - bashPath = bashPath.Replace(":\\", "/"); - bashPath = bashPath.Replace('\\', '/'); - return bashPath; - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public class BashRunner : ShellRunner + { + private static string[] fileNotFoundMessages = new string[] + { + "cannot stat", + "cannot remove", + "No such file or directory" + }; + + private static string[] invalidMovePathMessages = new string[] + { + "cannot move", + "No such file or directory" + }; + + private static string[] moveDirectoryNotSupportedMessage = new string[] + { + "Function not implemented" + }; + + private static string[] windowsPermissionDeniedMessage = new string[] + { + "Permission denied" + }; + + private static string[] macPermissionDeniedMessage = new string[] + { + "Resource temporarily unavailable" + }; + + private readonly string pathToBash; + + public BashRunner() + { + if (File.Exists(Settings.Default.PathToBash)) + { + this.pathToBash = Settings.Default.PathToBash; + } + else + { + this.pathToBash = "bash.exe"; + } + } + + private enum FileType + { + Invalid, + File, + Directory, + SymLink, + } + + protected override string FileName + { + get + { + return this.pathToBash; + } + } + + public static void DeleteDirectoryWithUnlimitedRetries(string path) + { + BashRunner runner = new BashRunner(); + bool pathExists = Directory.Exists(path); + int retryCount = 0; + while (pathExists) + { + string output = runner.DeleteDirectory(path); + pathExists = Directory.Exists(path); + if (pathExists) + { + ++retryCount; + Thread.Sleep(500); + if (retryCount > 10) + { + retryCount = 0; + if (Debugger.IsAttached) + { + Debugger.Break(); + } + } + } + } + } + + public bool IsSymbolicLink(string path) + { + return this.FileExistsOnDisk(path, FileType.SymLink); + } + + public void CreateSymbolicLink(string newLinkFilePath, string existingFilePath) + { + string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); + string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); + + this.RunProcess(string.Format("-c \"ln -s -F '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); + } + + public override bool FileExists(string path) + { + return this.FileExistsOnDisk(path, FileType.File); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); + string targetBashPath = this.ConvertWinPathToBashPath(targetPath); + + return this.RunProcess(string.Format("-c \"mv '{0}' '{1}'\"", sourceBashPath, targetBashPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // BashRunner does nothing special when a failure is expected, so just confirm source file is still present + this.MoveFile(sourcePath, targetPath); + this.FileExists(sourcePath).ShouldBeTrue($"{sourcePath} does not exist when it should"); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + string sourceBashPath = this.ConvertWinPathToBashPath(sourcePath); + string targetBashPath = this.ConvertWinPathToBashPath(targetPath); + + return this.RunProcess(string.Format("-c \"mv -f '{0}' '{1}'\"", sourceBashPath, targetBashPath)); + } + + public override string DeleteFile(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + return this.RunProcess(string.Format("-c \"rm '{0}'\"", bashPath)); + } + + public override string ReadAllText(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + string output = this.RunProcess(string.Format("-c \"cat '{0}'\"", bashPath)); + + // Bash sometimes sticks a trailing "\n" at the end of the output that we need to remove + // Until we can figure out why we cannot use this runner with files that have trailing newlines + if (output.Length > 0 && + output.Substring(output.Length - 1).Equals("\n", StringComparison.InvariantCultureIgnoreCase) && + !(output.Length > 1 && + output.Substring(output.Length - 2).Equals("\r\n", StringComparison.InvariantCultureIgnoreCase))) + { + output = output.Remove(output.Length - 1, 1); + } + + return output; + } + + public override void AppendAllText(string path, string contents) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"echo -n \\\"{0}\\\" >> '{1}'\"", contents, bashPath)); + } + + public override void CreateEmptyFile(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"touch '{0}'\"", bashPath)); + } + + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + string existingFileBashPath = this.ConvertWinPathToBashPath(existingFilePath); + string newLinkBashPath = this.ConvertWinPathToBashPath(newLinkFilePath); + + this.RunProcess(string.Format("-c \"ln '{0}' '{1}'\"", existingFileBashPath, newLinkBashPath)); + } + + public override void WriteAllText(string path, string contents) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"echo \\\"{0}\\\" > '{1}'\"", contents, bashPath)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // BashRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + return this.FileExistsOnDisk(path, FileType.Directory); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void RenameDirectory(string workingDirectory, string source, string target) + { + this.MoveDirectory(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target)); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(invalidMovePathMessages); + } + + public override void CreateDirectory(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + this.RunProcess(string.Format("-c \"mkdir '{0}'\"", bashPath)); + } + + public override string DeleteDirectory(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + return this.RunProcess(string.Format("-c \"rm -rf '{0}'\"", bashPath)); + } + + public override string EnumerateDirectory(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + return this.RunProcess(string.Format("-c \"ls '{0}'\"", bashPath)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + // bash does not report any error messages when access is denied, so just confirm the file still exists + this.DeleteFile(path); + this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(fileNotFoundMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + // Delete directory silently succeeds when deleting a non-existent path + this.DeleteDirectory(path); + } + + public override void ChangeMode(string path, ushort mode) + { + string octalMode = Convert.ToString(mode, 8); + string bashPath = this.ConvertWinPathToBashPath(path); + string command = $"-c \"chmod {octalMode} '{bashPath}'\""; + this.RunProcess(command); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + Assert.Fail("Unlike the other runners, bash.exe does not check folder handle before recusively deleting"); + } + + public override long FileSize(string path) + { + string bashPath = this.ConvertWinPathToBashPath(path); + + string statCommand = null; + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + statCommand = string.Format("-c \"stat -f \"%z\" '{0}'\"", bashPath); + } + else + { + statCommand = string.Format("-c \"stat --format \"%s\" '{0}'\"", bashPath); + } + + return long.Parse(this.RunProcess(statCommand)); + } + + public override void CreateFileWithoutClose(string path) + { + throw new NotImplementedException(); + } + + public override void OpenFileAndWriteWithoutClose(string path, string data) + { + throw new NotImplementedException(); + } + + private bool FileExistsOnDisk(string path, FileType type) + { + string checkArgument = string.Empty; + switch (type) + { + case FileType.File: + checkArgument = "-f"; + break; + case FileType.Directory: + checkArgument = "-d"; + break; + case FileType.SymLink: + checkArgument = "-h"; + break; + default: + Assert.Fail($"{nameof(this.FileExistsOnDisk)} does not support {nameof(FileType)} {type}"); + break; + } + + string bashPath = this.ConvertWinPathToBashPath(path); + string command = $"-c \"[ {checkArgument} '{bashPath}' ] && echo {ShellRunner.SuccessOutput} || echo {ShellRunner.FailureOutput}\""; + string output = this.RunProcess(command).Trim(); + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + + private string ConvertWinPathToBashPath(string winPath) + { + string bashPath = string.Concat("/", winPath); + bashPath = bashPath.Replace(":\\", "/"); + bashPath = bashPath.Replace('\\', '/'); + return bashPath; + } + } +} diff --git a/Scalar.FunctionalTests/FileSystemRunners/CmdRunner.cs b/Scalar.FunctionalTests/FileSystemRunners/CmdRunner.cs index 70cf96b6b4..992f994754 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/CmdRunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/CmdRunner.cs @@ -1,247 +1,247 @@ -using Scalar.Tests.Should; -using System; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public class CmdRunner : ShellRunner - { - private const string ProcessName = "CMD.exe"; - - private static string[] missingFileErrorMessages = new string[] - { - "The system cannot find the file specified.", - "The system cannot find the path specified.", - "Could Not Find" - }; - - private static string[] moveDirectoryFailureMessage = new string[] - { - "0 dir(s) moved" - }; - - private static string[] fileUsedByAnotherProcessMessage = new string[] - { - "The process cannot access the file because it is being used by another process" - }; - - protected override string FileName - { - get - { - return ProcessName; - } - } - - public static void DeleteDirectoryWithUnlimitedRetries(string path) - { - CmdRunner runner = new CmdRunner(); - bool pathExists = Directory.Exists(path); - int retryCount = 0; - while (pathExists) - { - string output = runner.DeleteDirectory(path); - pathExists = Directory.Exists(path); - if (pathExists) - { - ++retryCount; - Thread.Sleep(500); - if (retryCount > 10) - { - retryCount = 0; - if (Debugger.IsAttached) - { - Debugger.Break(); - } - } - } - } - } - - public override bool FileExists(string path) - { - if (this.DirectoryExists(path)) - { - return false; - } - - string output = this.RunProcess(string.Format("/C if exist \"{0}\" (echo {1}) else (echo {2})", path, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); - - return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); - } - - public override string MoveFile(string sourcePath, string targetPath) - { - return this.RunProcess(string.Format("/C move \"{0}\" \"{1}\"", sourcePath, targetPath)); - } - - public override void MoveFileShouldFail(string sourcePath, string targetPath) - { - // CmdRunner does nothing special when a failure is expected - this.MoveFile(sourcePath, targetPath); - } - - public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); - } - - public override string ReplaceFile(string sourcePath, string targetPath) - { - return this.RunProcess(string.Format("/C move /Y \"{0}\" \"{1}\"", sourcePath, targetPath)); - } - - public override string DeleteFile(string path) - { - return this.RunProcess(string.Format("/C del \"{0}\"", path)); - } - - public override string ReadAllText(string path) - { - return this.RunProcess(string.Format("/C type \"{0}\"", path)); - } - - public override void CreateEmptyFile(string path) - { - this.RunProcess(string.Format("/C type NUL > \"{0}\"", path)); - } - - public override void CreateHardLink(string newLinkFilePath, string existingFilePath) - { - this.RunProcess(string.Format("/C mklink /H \"{0}\" \"{1}\"", newLinkFilePath, existingFilePath)); - } - - public override void AppendAllText(string path, string contents) - { - // Use echo|set /p with "" to avoid adding any trailing whitespace or newline - // to the contents - this.RunProcess(string.Format("/C echo|set /p =\"{0}\" >> {1}", contents, path)); - } - - public override void WriteAllText(string path, string contents) - { - // Use echo|set /p with "" to avoid adding any trailing whitespace or newline - // to the contents - this.RunProcess(string.Format("/C echo|set /p =\"{0}\" > {1}", contents, path)); - } - - public override void WriteAllTextShouldFail(string path, string contents) - { - // CmdRunner does nothing special when a failure is expected - this.WriteAllText(path, contents); - } - - public override bool DirectoryExists(string path) - { - string parentDirectory = Path.GetDirectoryName(path); - string targetName = Path.GetFileName(path); - - string output = this.RunProcess(string.Format("/C dir /A:d /B {0}", parentDirectory)); - string[] directories = output.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); - - foreach (string directory in directories) - { - if (directory.Equals(targetName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - public override void CreateDirectory(string path) - { - this.RunProcess(string.Format("/C mkdir \"{0}\"", path)); - } - - public override string DeleteDirectory(string path) - { - return this.RunProcess(string.Format("/C rmdir /q /s \"{0}\"", path)); - } - - public override string EnumerateDirectory(string path) - { - return this.RunProcess(string.Format("/C dir \"{0}\"", path)); - } - - public override void MoveDirectory(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath); - } - - public override void RenameDirectory(string workingDirectory, string source, string target) - { - this.RunProcess(string.Format("/C ren \"{0}\" \"{1}\"", source, target), workingDirectory); - } - - public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); - } - - public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); - } - - public string RunCommand(string command) - { - return this.RunProcess(string.Format("/C {0}", command)); - } - - public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteFile_FileShouldNotBeFound(string path) - { - this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteFile_AccessShouldBeDenied(string path) - { - // CMD does not report any error messages when access is denied, so just confirm the file still exists - this.DeleteFile(path); - this.FileExists(path).ShouldEqual(true); - } - - public override void ReadAllText_FileShouldNotBeFound(string path) - { - this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) - { - this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) - { - this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); - } - +using Scalar.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public class CmdRunner : ShellRunner + { + private const string ProcessName = "CMD.exe"; + + private static string[] missingFileErrorMessages = new string[] + { + "The system cannot find the file specified.", + "The system cannot find the path specified.", + "Could Not Find" + }; + + private static string[] moveDirectoryFailureMessage = new string[] + { + "0 dir(s) moved" + }; + + private static string[] fileUsedByAnotherProcessMessage = new string[] + { + "The process cannot access the file because it is being used by another process" + }; + + protected override string FileName + { + get + { + return ProcessName; + } + } + + public static void DeleteDirectoryWithUnlimitedRetries(string path) + { + CmdRunner runner = new CmdRunner(); + bool pathExists = Directory.Exists(path); + int retryCount = 0; + while (pathExists) + { + string output = runner.DeleteDirectory(path); + pathExists = Directory.Exists(path); + if (pathExists) + { + ++retryCount; + Thread.Sleep(500); + if (retryCount > 10) + { + retryCount = 0; + if (Debugger.IsAttached) + { + Debugger.Break(); + } + } + } + } + } + + public override bool FileExists(string path) + { + if (this.DirectoryExists(path)) + { + return false; + } + + string output = this.RunProcess(string.Format("/C if exist \"{0}\" (echo {1}) else (echo {2})", path, ShellRunner.SuccessOutput, ShellRunner.FailureOutput)).Trim(); + + return output.Equals(ShellRunner.SuccessOutput, StringComparison.InvariantCulture); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("/C move \"{0}\" \"{1}\"", sourcePath, targetPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // CmdRunner does nothing special when a failure is expected + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("/C move /Y \"{0}\" \"{1}\"", sourcePath, targetPath)); + } + + public override string DeleteFile(string path) + { + return this.RunProcess(string.Format("/C del \"{0}\"", path)); + } + + public override string ReadAllText(string path) + { + return this.RunProcess(string.Format("/C type \"{0}\"", path)); + } + + public override void CreateEmptyFile(string path) + { + this.RunProcess(string.Format("/C type NUL > \"{0}\"", path)); + } + + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + this.RunProcess(string.Format("/C mklink /H \"{0}\" \"{1}\"", newLinkFilePath, existingFilePath)); + } + + public override void AppendAllText(string path, string contents) + { + // Use echo|set /p with "" to avoid adding any trailing whitespace or newline + // to the contents + this.RunProcess(string.Format("/C echo|set /p =\"{0}\" >> {1}", contents, path)); + } + + public override void WriteAllText(string path, string contents) + { + // Use echo|set /p with "" to avoid adding any trailing whitespace or newline + // to the contents + this.RunProcess(string.Format("/C echo|set /p =\"{0}\" > {1}", contents, path)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // CmdRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + string parentDirectory = Path.GetDirectoryName(path); + string targetName = Path.GetFileName(path); + + string output = this.RunProcess(string.Format("/C dir /A:d /B {0}", parentDirectory)); + string[] directories = output.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries); + + foreach (string directory in directories) + { + if (directory.Equals(targetName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + public override void CreateDirectory(string path) + { + this.RunProcess(string.Format("/C mkdir \"{0}\"", path)); + } + + public override string DeleteDirectory(string path) + { + return this.RunProcess(string.Format("/C rmdir /q /s \"{0}\"", path)); + } + + public override string EnumerateDirectory(string path) + { + return this.RunProcess(string.Format("/C dir \"{0}\"", path)); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void RenameDirectory(string workingDirectory, string source, string target) + { + this.RunProcess(string.Format("/C ren \"{0}\" \"{1}\"", source, target), workingDirectory); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryFailureMessage); + } + + public string RunCommand(string command) + { + return this.RunProcess(string.Format("/C {0}", command)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + // CMD does not report any error messages when access is denied, so just confirm the file still exists + this.DeleteFile(path); + this.FileExists(path).ShouldEqual(true); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); + } + public override void ChangeMode(string path, ushort mode) { throw new NotSupportedException(); } - public override void CreateFileWithoutClose(string path) + public override void CreateFileWithoutClose(string path) { - throw new NotImplementedException(); + throw new NotImplementedException(); } public override void OpenFileAndWriteWithoutClose(string path, string data) - { - throw new NotImplementedException(); - } - - public override long FileSize(string path) - { - return long.Parse(this.RunProcess(string.Format("/C for %I in ({0}) do @echo %~zI", path))); - } - } -} + { + throw new NotImplementedException(); + } + + public override long FileSize(string path) + { + return long.Parse(this.RunProcess(string.Format("/C for %I in ({0}) do @echo %~zI", path))); + } + } +} diff --git a/Scalar.FunctionalTests/FileSystemRunners/FileSystemRunner.cs b/Scalar.FunctionalTests/FileSystemRunners/FileSystemRunner.cs index 111e587c95..fdde7458aa 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/FileSystemRunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/FileSystemRunner.cs @@ -1,117 +1,117 @@ -using NUnit.Framework; -using System; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public abstract class FileSystemRunner - { - private static FileSystemRunner defaultRunner = new SystemIORunner(); - - public static object[] AllWindowsRunners { get; } = - new[] - { - new object[] { new SystemIORunner() }, - new object[] { new CmdRunner() }, - new object[] { new PowerShellRunner() }, - new object[] { new BashRunner() }, - }; - - public static object[] AllMacRunners { get; } = - new[] - { - new object[] { new SystemIORunner() }, - new object[] { new BashRunner() }, - }; - - public static object[] DefaultRunners { get; } = - new[] - { - new object[] { defaultRunner } - }; - - public static object[] Runners - { - get { return ScalarTestConfig.FileSystemRunners; } - } - - /// - /// Default runner to use (for tests that do not need to be run with multiple runners) - /// - public static FileSystemRunner DefaultRunner - { - get { return defaultRunner; } - } - - // File methods - public abstract bool FileExists(string path); - public abstract string MoveFile(string sourcePath, string targetPath); - - /// - /// Attempt to move the specified file to the specifed target path. By calling this method the caller is - /// indicating that they expect the move to fail. However, the caller is responsible for verifying that - /// the move failed. - /// - /// Path to existing file - /// Path to target file (target of the move) - public abstract void MoveFileShouldFail(string sourcePath, string targetPath); - public abstract void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath); - public abstract string ReplaceFile(string sourcePath, string targetPath); - public abstract void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath); - public abstract string DeleteFile(string path); - public abstract void DeleteFile_FileShouldNotBeFound(string path); - public abstract void DeleteFile_AccessShouldBeDenied(string path); - public abstract string ReadAllText(string path); - public abstract void ReadAllText_FileShouldNotBeFound(string path); - - public abstract void CreateEmptyFile(string path); - public abstract void CreateHardLink(string newLinkFilePath, string existingFilePath); - public abstract void ChangeMode(string path, ushort mode); - - /// - /// Write the specified contents to the specified file. By calling this method the caller is - /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that - /// the write succeeded. - /// - /// Path to file - /// File contents +using NUnit.Framework; +using System; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public abstract class FileSystemRunner + { + private static FileSystemRunner defaultRunner = new SystemIORunner(); + + public static object[] AllWindowsRunners { get; } = + new[] + { + new object[] { new SystemIORunner() }, + new object[] { new CmdRunner() }, + new object[] { new PowerShellRunner() }, + new object[] { new BashRunner() }, + }; + + public static object[] AllMacRunners { get; } = + new[] + { + new object[] { new SystemIORunner() }, + new object[] { new BashRunner() }, + }; + + public static object[] DefaultRunners { get; } = + new[] + { + new object[] { defaultRunner } + }; + + public static object[] Runners + { + get { return ScalarTestConfig.FileSystemRunners; } + } + + /// + /// Default runner to use (for tests that do not need to be run with multiple runners) + /// + public static FileSystemRunner DefaultRunner + { + get { return defaultRunner; } + } + + // File methods + public abstract bool FileExists(string path); + public abstract string MoveFile(string sourcePath, string targetPath); + + /// + /// Attempt to move the specified file to the specifed target path. By calling this method the caller is + /// indicating that they expect the move to fail. However, the caller is responsible for verifying that + /// the move failed. + /// + /// Path to existing file + /// Path to target file (target of the move) + public abstract void MoveFileShouldFail(string sourcePath, string targetPath); + public abstract void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath); + public abstract string ReplaceFile(string sourcePath, string targetPath); + public abstract void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath); + public abstract string DeleteFile(string path); + public abstract void DeleteFile_FileShouldNotBeFound(string path); + public abstract void DeleteFile_AccessShouldBeDenied(string path); + public abstract string ReadAllText(string path); + public abstract void ReadAllText_FileShouldNotBeFound(string path); + + public abstract void CreateEmptyFile(string path); + public abstract void CreateHardLink(string newLinkFilePath, string existingFilePath); + public abstract void ChangeMode(string path, ushort mode); + + /// + /// Write the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that + /// the write succeeded. + /// + /// Path to file + /// File contents public abstract void WriteAllText(string path, string contents); - public abstract void CreateFileWithoutClose(string path); - public abstract void OpenFileAndWriteWithoutClose(string path, string data); - - /// - /// Append the specified contents to the specified file. By calling this method the caller is - /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that - /// the write succeeded. - /// - /// Path to file - /// File contents - public abstract void AppendAllText(string path, string contents); - - /// - /// Attempt to write the specified contents to the specified file. By calling this method the caller is - /// indicating that they expect the write to fail. However, the caller is responsible for verifying that - /// the write failed. - /// - /// Expected type of exception to be thrown - /// Path to file - /// File contents - public abstract void WriteAllTextShouldFail(string path, string contents) where ExceptionType : Exception; - - // Directory methods - public abstract bool DirectoryExists(string path); - public abstract void MoveDirectory(string sourcePath, string targetPath); - public abstract void RenameDirectory(string workingDirectory, string source, string target); - public abstract void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath); - public abstract void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath); - public abstract void CreateDirectory(string path); - public abstract string EnumerateDirectory(string path); - public abstract long FileSize(string path); - - /// - /// A recursive delete of a directory - /// - public abstract string DeleteDirectory(string path); - public abstract void DeleteDirectory_DirectoryShouldNotBeFound(string path); - public abstract void DeleteDirectory_ShouldBeBlockedByProcess(string path); - } -} + public abstract void CreateFileWithoutClose(string path); + public abstract void OpenFileAndWriteWithoutClose(string path, string data); + + /// + /// Append the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to succeed. However, the caller is responsible for verifying that + /// the write succeeded. + /// + /// Path to file + /// File contents + public abstract void AppendAllText(string path, string contents); + + /// + /// Attempt to write the specified contents to the specified file. By calling this method the caller is + /// indicating that they expect the write to fail. However, the caller is responsible for verifying that + /// the write failed. + /// + /// Expected type of exception to be thrown + /// Path to file + /// File contents + public abstract void WriteAllTextShouldFail(string path, string contents) where ExceptionType : Exception; + + // Directory methods + public abstract bool DirectoryExists(string path); + public abstract void MoveDirectory(string sourcePath, string targetPath); + public abstract void RenameDirectory(string workingDirectory, string source, string target); + public abstract void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath); + public abstract void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath); + public abstract void CreateDirectory(string path); + public abstract string EnumerateDirectory(string path); + public abstract long FileSize(string path); + + /// + /// A recursive delete of a directory + /// + public abstract string DeleteDirectory(string path); + public abstract void DeleteDirectory_DirectoryShouldNotBeFound(string path); + public abstract void DeleteDirectory_ShouldBeBlockedByProcess(string path); + } +} diff --git a/Scalar.FunctionalTests/FileSystemRunners/PowerShellRunner.cs b/Scalar.FunctionalTests/FileSystemRunners/PowerShellRunner.cs index 5333f64ec5..a042a3ec63 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/PowerShellRunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/PowerShellRunner.cs @@ -1,228 +1,228 @@ -using Scalar.Tests.Should; -using System.IO; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public class PowerShellRunner : ShellRunner - { - private const string ProcessName = "powershell.exe"; - - private static string[] missingFileErrorMessages = new string[] - { - "Cannot find path" - }; - - private static string[] invalidPathErrorMessages = new string[] - { - "Could not find a part of the path" - }; - - private static string[] moveDirectoryNotSupportedMessage = new string[] - { - "The request is not supported." - }; - - private static string[] fileUsedByAnotherProcessMessage = new string[] - { - "The process cannot access the file because it is being used by another process" - }; - - private static string[] permissionDeniedMessage = new string[] - { - "PermissionDenied" - }; - - protected override string FileName - { - get - { - return ProcessName; - } - } - - public override bool FileExists(string path) - { - string parentDirectory = Path.GetDirectoryName(path); - string targetName = Path.GetFileName(path); - - // Use -force so that hidden items are returned as well - string command = string.Format("-Command \"&{{ Get-ChildItem -force {0} | where {{$_.Attributes -NotLike '*Directory*'}} | where {{$_.Name -eq '{1}' }} }}\"", parentDirectory, targetName); - string output = this.RunProcess(command).Trim(); - - if (output.Length == 0 || output.Contains("PathNotFound") || output.Contains("ItemNotFound")) - { - return false; - } - - return true; - } - - public override string MoveFile(string sourcePath, string targetPath) - { - return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force}}\"", sourcePath, targetPath)); - } - - public override void MoveFileShouldFail(string sourcePath, string targetPath) - { - // PowerShellRunner does nothing special when a failure is expected - this.MoveFile(sourcePath, targetPath); - } - - public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); - } - - public override string ReplaceFile(string sourcePath, string targetPath) - { - return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force }}\"", sourcePath, targetPath)); - } - - public override string DeleteFile(string path) - { - return this.RunProcess(string.Format("-Command \"& {{ Remove-Item {0} }}\"", path)); - } - - public override string ReadAllText(string path) - { - string output = this.RunProcess(string.Format("-Command \"& {{ Get-Content -Raw {0} }}\"", path), errorMsgDelimeter: "\r\n"); - - // Get-Content insists on sticking a trailing "\r\n" at the end of the output that we need to remove - output.Length.ShouldBeAtLeast(2, $"File content was not long enough for {path}"); - output.Substring(output.Length - 2).ShouldEqual("\r\n"); - output = output.Remove(output.Length - 2, 2); - - return output; - } - - public override void AppendAllText(string path, string contents) - { - this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -Append -NoNewline}}\"", path, contents)); - } - - public override void CreateEmptyFile(string path) - { - this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType file {0}}}\"", path)); - } - - public override void CreateHardLink(string newLinkFilePath, string existingFilePath) - { - this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType HardLink -Path {0} -Value {1}}}\"", newLinkFilePath, existingFilePath)); - } - - public override void WriteAllText(string path, string contents) - { - this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -NoNewline}}\"", path, contents)); - } - - public override void WriteAllTextShouldFail(string path, string contents) - { - // PowerShellRunner does nothing special when a failure is expected - this.WriteAllText(path, contents); - } - - public override bool DirectoryExists(string path) - { - string command = string.Format("-Command \"&{{ Test-Path {0} -PathType Container }}\"", path); - string output = this.RunProcess(command).Trim(); - - if (output.Contains("True")) - { - return true; - } - - return false; - } - - public override void MoveDirectory(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath); - } - - public override void RenameDirectory(string workingDirectory, string source, string target) - { - this.RunProcess(string.Format("-Command \"& {{ Rename-Item -Path {0} -NewName {1} -force }}\"", Path.Combine(workingDirectory, source), target)); - } - - public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); - } - - public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) - { - this.MoveFile(sourcePath, targetPath).ShouldContain(invalidPathErrorMessages); - } - - public override void CreateDirectory(string path) - { - this.RunProcess(string.Format("-Command \"&{{ New-Item {0} -type directory}}\"", path)); - } - - public override string DeleteDirectory(string path) - { - return this.RunProcess(string.Format("-Command \"&{{ Remove-Item -Force -Recurse {0} }}\"", path)); - } - - public override string EnumerateDirectory(string path) - { - return this.RunProcess(string.Format("-Command \"&{{ Get-ChildItem {0} }}\"", path)); - } - - public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteFile_FileShouldNotBeFound(string path) - { - this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteFile_AccessShouldBeDenied(string path) - { - this.DeleteFile(path).ShouldContain(permissionDeniedMessage); - this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); - } - - public override void ReadAllText_FileShouldNotBeFound(string path) - { - this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) - { - this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); - } - - public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) - { - this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); - } - - public override long FileSize(string path) - { - return long.Parse(this.RunProcess(string.Format("-Command \"&{{ (Get-Item {0}).length}}\"", path))); - } - +using Scalar.Tests.Should; +using System.IO; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public class PowerShellRunner : ShellRunner + { + private const string ProcessName = "powershell.exe"; + + private static string[] missingFileErrorMessages = new string[] + { + "Cannot find path" + }; + + private static string[] invalidPathErrorMessages = new string[] + { + "Could not find a part of the path" + }; + + private static string[] moveDirectoryNotSupportedMessage = new string[] + { + "The request is not supported." + }; + + private static string[] fileUsedByAnotherProcessMessage = new string[] + { + "The process cannot access the file because it is being used by another process" + }; + + private static string[] permissionDeniedMessage = new string[] + { + "PermissionDenied" + }; + + protected override string FileName + { + get + { + return ProcessName; + } + } + + public override bool FileExists(string path) + { + string parentDirectory = Path.GetDirectoryName(path); + string targetName = Path.GetFileName(path); + + // Use -force so that hidden items are returned as well + string command = string.Format("-Command \"&{{ Get-ChildItem -force {0} | where {{$_.Attributes -NotLike '*Directory*'}} | where {{$_.Name -eq '{1}' }} }}\"", parentDirectory, targetName); + string output = this.RunProcess(command).Trim(); + + if (output.Length == 0 || output.Contains("PathNotFound") || output.Contains("ItemNotFound")) + { + return false; + } + + return true; + } + + public override string MoveFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force}}\"", sourcePath, targetPath)); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + // PowerShellRunner does nothing special when a failure is expected + this.MoveFile(sourcePath, targetPath); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + return this.RunProcess(string.Format("-Command \"& {{ Move-Item {0} {1} -force }}\"", sourcePath, targetPath)); + } + + public override string DeleteFile(string path) + { + return this.RunProcess(string.Format("-Command \"& {{ Remove-Item {0} }}\"", path)); + } + + public override string ReadAllText(string path) + { + string output = this.RunProcess(string.Format("-Command \"& {{ Get-Content -Raw {0} }}\"", path), errorMsgDelimeter: "\r\n"); + + // Get-Content insists on sticking a trailing "\r\n" at the end of the output that we need to remove + output.Length.ShouldBeAtLeast(2, $"File content was not long enough for {path}"); + output.Substring(output.Length - 2).ShouldEqual("\r\n"); + output = output.Remove(output.Length - 2, 2); + + return output; + } + + public override void AppendAllText(string path, string contents) + { + this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -Append -NoNewline}}\"", path, contents)); + } + + public override void CreateEmptyFile(string path) + { + this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType file {0}}}\"", path)); + } + + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + this.RunProcess(string.Format("-Command \"&{{ New-Item -ItemType HardLink -Path {0} -Value {1}}}\"", newLinkFilePath, existingFilePath)); + } + + public override void WriteAllText(string path, string contents) + { + this.RunProcess(string.Format("-Command \"&{{ Out-File -FilePath {0} -InputObject '{1}' -Encoding ascii -NoNewline}}\"", path, contents)); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + // PowerShellRunner does nothing special when a failure is expected + this.WriteAllText(path, contents); + } + + public override bool DirectoryExists(string path) + { + string command = string.Format("-Command \"&{{ Test-Path {0} -PathType Container }}\"", path); + string output = this.RunProcess(command).Trim(); + + if (output.Contains("True")) + { + return true; + } + + return false; + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath); + } + + public override void RenameDirectory(string workingDirectory, string source, string target) + { + this.RunProcess(string.Format("-Command \"& {{ Rename-Item -Path {0} -NewName {1} -force }}\"", Path.Combine(workingDirectory, source), target)); + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(moveDirectoryNotSupportedMessage); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + this.MoveFile(sourcePath, targetPath).ShouldContain(invalidPathErrorMessages); + } + + public override void CreateDirectory(string path) + { + this.RunProcess(string.Format("-Command \"&{{ New-Item {0} -type directory}}\"", path)); + } + + public override string DeleteDirectory(string path) + { + return this.RunProcess(string.Format("-Command \"&{{ Remove-Item -Force -Recurse {0} }}\"", path)); + } + + public override string EnumerateDirectory(string path) + { + return this.RunProcess(string.Format("-Command \"&{{ Get-ChildItem {0} }}\"", path)); + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ReplaceFile(sourcePath, targetPath).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + this.DeleteFile(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + this.DeleteFile(path).ShouldContain(permissionDeniedMessage); + this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ReadAllText(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.DeleteDirectory(path).ShouldContainOneOf(missingFileErrorMessages); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + this.DeleteDirectory(path).ShouldContain(fileUsedByAnotherProcessMessage); + } + + public override long FileSize(string path) + { + return long.Parse(this.RunProcess(string.Format("-Command \"&{{ (Get-Item {0}).length}}\"", path))); + } + public override void ChangeMode(string path, ushort mode) { throw new System.NotSupportedException(); } - public override void CreateFileWithoutClose(string path) - { - throw new System.NotSupportedException(); + public override void CreateFileWithoutClose(string path) + { + throw new System.NotSupportedException(); } public override void OpenFileAndWriteWithoutClose(string path, string data) - { - throw new System.NotSupportedException(); - } - - protected override string RunProcess(string command, string workingDirectory = "", string errorMsgDelimeter = "") - { - return base.RunProcess("-NoProfile " + command, workingDirectory, errorMsgDelimeter); - } - } -} + { + throw new System.NotSupportedException(); + } + + protected override string RunProcess(string command, string workingDirectory = "", string errorMsgDelimeter = "") + { + return base.RunProcess("-NoProfile " + command, workingDirectory, errorMsgDelimeter); + } + } +} diff --git a/Scalar.FunctionalTests/FileSystemRunners/ShellRunner.cs b/Scalar.FunctionalTests/FileSystemRunners/ShellRunner.cs index 4376c9dc70..8ced9195ab 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/ShellRunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/ShellRunner.cs @@ -1,28 +1,28 @@ -using Scalar.FunctionalTests.Tools; -using System.Diagnostics; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public abstract class ShellRunner : FileSystemRunner - { - protected const string SuccessOutput = "True"; - protected const string FailureOutput = "False"; - - protected abstract string FileName { get; } - - protected virtual string RunProcess(string arguments, string workingDirectory = "", string errorMsgDelimeter = "") - { - ProcessStartInfo startInfo = new ProcessStartInfo(); - startInfo.UseShellExecute = false; - startInfo.RedirectStandardOutput = true; - startInfo.RedirectStandardError = true; - startInfo.CreateNoWindow = true; - startInfo.FileName = this.FileName; - startInfo.Arguments = arguments; - startInfo.WorkingDirectory = workingDirectory; - - ProcessResult result = ProcessHelper.Run(startInfo, errorMsgDelimeter: errorMsgDelimeter); - return !string.IsNullOrEmpty(result.Output) ? result.Output : result.Errors; - } - } +using Scalar.FunctionalTests.Tools; +using System.Diagnostics; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public abstract class ShellRunner : FileSystemRunner + { + protected const string SuccessOutput = "True"; + protected const string FailureOutput = "False"; + + protected abstract string FileName { get; } + + protected virtual string RunProcess(string arguments, string workingDirectory = "", string errorMsgDelimeter = "") + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.UseShellExecute = false; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.CreateNoWindow = true; + startInfo.FileName = this.FileName; + startInfo.Arguments = arguments; + startInfo.WorkingDirectory = workingDirectory; + + ProcessResult result = ProcessHelper.Run(startInfo, errorMsgDelimeter: errorMsgDelimeter); + return !string.IsNullOrEmpty(result.Output) ? result.Output : result.Errors; + } + } } diff --git a/Scalar.FunctionalTests/FileSystemRunners/SystemIORunner.cs b/Scalar.FunctionalTests/FileSystemRunners/SystemIORunner.cs index fadfa303d5..4883bea4fd 100644 --- a/Scalar.FunctionalTests/FileSystemRunners/SystemIORunner.cs +++ b/Scalar.FunctionalTests/FileSystemRunners/SystemIORunner.cs @@ -1,270 +1,270 @@ -using NUnit.Framework; -using Scalar.Tests.Should; -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Scalar.FunctionalTests.FileSystemRunners -{ - public class SystemIORunner : FileSystemRunner +using NUnit.Framework; +using Scalar.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; + +namespace Scalar.FunctionalTests.FileSystemRunners +{ + public class SystemIORunner : FileSystemRunner { - public override bool FileExists(string path) - { - return File.Exists(path); - } - - public override string MoveFile(string sourcePath, string targetPath) - { - File.Move(sourcePath, targetPath); - return string.Empty; - } - - public override void CreateFileWithoutClose(string path) - { - File.Create(path); - } - - public override void OpenFileAndWriteWithoutClose(string path, string content) - { - StreamWriter file = new StreamWriter(path); - file.Write(content); - } - - public override void MoveFileShouldFail(string sourcePath, string targetPath) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("MoveFileShouldFail should not be run with the debugger attached"); - } - - this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); - } - - public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); - } - - public override string ReplaceFile(string sourcePath, string targetPath) - { - File.Replace(sourcePath, targetPath, null); - return string.Empty; - } - - public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) - { - this.ShouldFail(() => { this.ReplaceFile(sourcePath, targetPath); }); - } - - public override string DeleteFile(string path) - { - File.Delete(path); - return string.Empty; - } - - public override void DeleteFile_FileShouldNotBeFound(string path) - { - // Delete file silently succeeds when file is non-existent - this.DeleteFile(path); - } - - public override void DeleteFile_AccessShouldBeDenied(string path) - { - this.ShouldFail(() => { this.DeleteFile(path); }); - this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); - } - - public override string ReadAllText(string path) - { - return File.ReadAllText(path); - } - - public override void CreateEmptyFile(string path) - { - using (FileStream fs = File.Create(path)) - { - } - } - - public override void CreateHardLink(string newLinkFilePath, string existingFilePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - WindowsCreateHardLink(newLinkFilePath, existingFilePath, IntPtr.Zero).ShouldBeTrue($"Failed to create hard link: {Marshal.GetLastWin32Error()}"); + public override bool FileExists(string path) + { + return File.Exists(path); + } + + public override string MoveFile(string sourcePath, string targetPath) + { + File.Move(sourcePath, targetPath); + return string.Empty; + } + + public override void CreateFileWithoutClose(string path) + { + File.Create(path); + } + + public override void OpenFileAndWriteWithoutClose(string path, string content) + { + StreamWriter file = new StreamWriter(path); + file.Write(content); + } + + public override void MoveFileShouldFail(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveFileShouldFail should not be run with the debugger attached"); } - else - { - MacCreateHardLink(existingFilePath, newLinkFilePath).ShouldEqual(0, $"Failed to create hard link: {Marshal.GetLastWin32Error()}"); - } - } - - public override void WriteAllText(string path, string contents) - { - File.WriteAllText(path, contents); - } - - public override void AppendAllText(string path, string contents) - { - File.AppendAllText(path, contents); - } - - public override void WriteAllTextShouldFail(string path, string contents) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("WriteAllTextShouldFail should not be run with the debugger attached"); - } - - this.ShouldFail(() => { this.WriteAllText(path, contents); }); - } - - public override bool DirectoryExists(string path) - { - return Directory.Exists(path); - } - - public override void MoveDirectory(string sourcePath, string targetPath) - { - Directory.Move(sourcePath, targetPath); - } - - public override void RenameDirectory(string workingDirectory, string source, string target) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + + this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); + } + + public override void MoveFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ShouldFail(() => { this.MoveFile(sourcePath, targetPath); }); + } + + public override string ReplaceFile(string sourcePath, string targetPath) + { + File.Replace(sourcePath, targetPath, null); + return string.Empty; + } + + public override void ReplaceFile_FileShouldNotBeFound(string sourcePath, string targetPath) + { + this.ShouldFail(() => { this.ReplaceFile(sourcePath, targetPath); }); + } + + public override string DeleteFile(string path) + { + File.Delete(path); + return string.Empty; + } + + public override void DeleteFile_FileShouldNotBeFound(string path) + { + // Delete file silently succeeds when file is non-existent + this.DeleteFile(path); + } + + public override void DeleteFile_AccessShouldBeDenied(string path) + { + this.ShouldFail(() => { this.DeleteFile(path); }); + this.FileExists(path).ShouldBeTrue($"{path} does not exist when it should"); + } + + public override string ReadAllText(string path) + { + return File.ReadAllText(path); + } + + public override void CreateEmptyFile(string path) + { + using (FileStream fs = File.Create(path)) + { + } + } + + public override void CreateHardLink(string newLinkFilePath, string existingFilePath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + WindowsCreateHardLink(newLinkFilePath, existingFilePath, IntPtr.Zero).ShouldBeTrue($"Failed to create hard link: {Marshal.GetLastWin32Error()}"); + } + else + { + MacCreateHardLink(existingFilePath, newLinkFilePath).ShouldEqual(0, $"Failed to create hard link: {Marshal.GetLastWin32Error()}"); + } + } + + public override void WriteAllText(string path, string contents) + { + File.WriteAllText(path, contents); + } + + public override void AppendAllText(string path, string contents) + { + File.AppendAllText(path, contents); + } + + public override void WriteAllTextShouldFail(string path, string contents) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("WriteAllTextShouldFail should not be run with the debugger attached"); + } + + this.ShouldFail(() => { this.WriteAllText(path, contents); }); + } + + public override bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + public override void MoveDirectory(string sourcePath, string targetPath) + { + Directory.Move(sourcePath, targetPath); + } + + public override void RenameDirectory(string workingDirectory, string source, string target) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { MoveFileEx(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target), 0); - } + } else { Rename(Path.Combine(workingDirectory, source), Path.Combine(workingDirectory, target)); - } - } - - public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("MoveDirectory_RequestShouldNotBeSupported should not be run with the debugger attached"); - } - - Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); - } - - public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) - { - if (Debugger.IsAttached) - { - throw new InvalidOperationException("MoveDirectory_TargetShouldBeInvalid should not be run with the debugger attached"); - } - - Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); - } - - public override void CreateDirectory(string path) - { - Directory.CreateDirectory(path); - } - - public override string DeleteDirectory(string path) - { - DirectoryInfo directory = new DirectoryInfo(path); - - foreach (FileInfo file in directory.GetFiles()) - { - file.Attributes = FileAttributes.Normal; - - RetryOnException(() => file.Delete()); - } - - foreach (DirectoryInfo subDirectory in directory.GetDirectories()) - { - this.DeleteDirectory(subDirectory.FullName); - } - - RetryOnException(() => directory.Delete()); - return string.Empty; - } - - public override string EnumerateDirectory(string path) - { - return string.Join(Environment.NewLine, Directory.GetFileSystemEntries(path)); - } - - public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) - { - this.ShouldFail(() => { this.DeleteDirectory(path); }); - } - - public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) - { - Assert.Fail("DeleteDirectory_ShouldBeBlockedByProcess not supported by SystemIORunner"); - } - - public override void ReadAllText_FileShouldNotBeFound(string path) - { - this.ShouldFail(() => { this.ReadAllText(path); }); - } - + } + } + + public override void MoveDirectory_RequestShouldNotBeSupported(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveDirectory_RequestShouldNotBeSupported should not be run with the debugger attached"); + } + + Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); + } + + public override void MoveDirectory_TargetShouldBeInvalid(string sourcePath, string targetPath) + { + if (Debugger.IsAttached) + { + throw new InvalidOperationException("MoveDirectory_TargetShouldBeInvalid should not be run with the debugger attached"); + } + + Assert.Catch(() => this.MoveDirectory(sourcePath, targetPath)); + } + + public override void CreateDirectory(string path) + { + Directory.CreateDirectory(path); + } + + public override string DeleteDirectory(string path) + { + DirectoryInfo directory = new DirectoryInfo(path); + + foreach (FileInfo file in directory.GetFiles()) + { + file.Attributes = FileAttributes.Normal; + + RetryOnException(() => file.Delete()); + } + + foreach (DirectoryInfo subDirectory in directory.GetDirectories()) + { + this.DeleteDirectory(subDirectory.FullName); + } + + RetryOnException(() => directory.Delete()); + return string.Empty; + } + + public override string EnumerateDirectory(string path) + { + return string.Join(Environment.NewLine, Directory.GetFileSystemEntries(path)); + } + + public override void DeleteDirectory_DirectoryShouldNotBeFound(string path) + { + this.ShouldFail(() => { this.DeleteDirectory(path); }); + } + + public override void DeleteDirectory_ShouldBeBlockedByProcess(string path) + { + Assert.Fail("DeleteDirectory_ShouldBeBlockedByProcess not supported by SystemIORunner"); + } + + public override void ReadAllText_FileShouldNotBeFound(string path) + { + this.ShouldFail(() => { this.ReadAllText(path); }); + } + public override void ChangeMode(string path, ushort mode) { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - throw new NotSupportedException(); - } - else - { - Chmod(path, mode).ShouldEqual(0, $"Failed to chmod: {Marshal.GetLastWin32Error()}"); - } - } - - public override long FileSize(string path) - { - return new FileInfo(path).Length; - } - - [DllImport("kernel32", SetLastError = true)] - private static extern bool MoveFileEx(string existingFileName, string newFileName, int flags); - - [DllImport("libc", EntryPoint = "link", SetLastError = true)] - private static extern int MacCreateHardLink(string oldPath, string newPath); - - [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + throw new NotSupportedException(); + } + else + { + Chmod(path, mode).ShouldEqual(0, $"Failed to chmod: {Marshal.GetLastWin32Error()}"); + } + } + + public override long FileSize(string path) + { + return new FileInfo(path).Length; + } + + [DllImport("kernel32", SetLastError = true)] + private static extern bool MoveFileEx(string existingFileName, string newFileName, int flags); + + [DllImport("libc", EntryPoint = "link", SetLastError = true)] + private static extern int MacCreateHardLink(string oldPath, string newPath); + + [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] private static extern int Chmod(string pathname, ushort mode); - [DllImport("libc", EntryPoint = "rename", SetLastError = true)] + [DllImport("libc", EntryPoint = "rename", SetLastError = true)] private static extern int Rename(string oldPath, string newPath); - [DllImport("kernel32.dll", EntryPoint = "CreateHardLink", SetLastError = true, CharSet = CharSet.Unicode)] - private static extern bool WindowsCreateHardLink( - string newLinkFileName, - string existingFileName, - IntPtr securityAttributes); - - private static void RetryOnException(Action action) - { - for (int i = 0; i < 10; i++) - { - try - { - action(); - break; - } - catch (IOException) - { - Thread.Sleep(500); - } - catch (UnauthorizedAccessException) - { - Thread.Sleep(500); - } - } - } - - private void ShouldFail(Action action) where ExceptionType : Exception - { - Assert.Catch(() => action()); - } - } -} + [DllImport("kernel32.dll", EntryPoint = "CreateHardLink", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool WindowsCreateHardLink( + string newLinkFileName, + string existingFileName, + IntPtr securityAttributes); + + private static void RetryOnException(Action action) + { + for (int i = 0; i < 10; i++) + { + try + { + action(); + break; + } + catch (IOException) + { + Thread.Sleep(500); + } + catch (UnauthorizedAccessException) + { + Thread.Sleep(500); + } + } + } + + private void ShouldFail(Action action) where ExceptionType : Exception + { + Assert.Catch(() => action()); + } + } +} diff --git a/Scalar.FunctionalTests/GlobalSetup.cs b/Scalar.FunctionalTests/GlobalSetup.cs index 50e103eb6f..c795081e75 100644 --- a/Scalar.FunctionalTests/GlobalSetup.cs +++ b/Scalar.FunctionalTests/GlobalSetup.cs @@ -1,41 +1,41 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tests; -using Scalar.FunctionalTests.Tools; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests -{ - [SetUpFixture] - public class GlobalSetup - { - [OneTimeSetUp] - public void RunBeforeAnyTests() - { - } - - [OneTimeTearDown] - public void RunAfterAllTests() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - string serviceLogFolder = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "Scalar", - ScalarServiceProcess.TestServiceName, - "Logs"); - - Console.WriteLine("Scalar.Service logs at '{0}' attached below.\n\n", serviceLogFolder); - foreach (string filename in TestResultsHelper.GetAllFilesInDirectory(serviceLogFolder)) - { - TestResultsHelper.OutputFileContents(filename); - } - - ScalarServiceProcess.UninstallService(); - } - - PrintTestCaseStats.PrintRunTimeStats(); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tests; +using Scalar.FunctionalTests.Tools; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests +{ + [SetUpFixture] + public class GlobalSetup + { + [OneTimeSetUp] + public void RunBeforeAnyTests() + { + } + + [OneTimeTearDown] + public void RunAfterAllTests() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + string serviceLogFolder = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Scalar", + ScalarServiceProcess.TestServiceName, + "Logs"); + + Console.WriteLine("Scalar.Service logs at '{0}' attached below.\n\n", serviceLogFolder); + foreach (string filename in TestResultsHelper.GetAllFilesInDirectory(serviceLogFolder)) + { + TestResultsHelper.OutputFileContents(filename); + } + + ScalarServiceProcess.UninstallService(); + } + + PrintTestCaseStats.PrintRunTimeStats(); + } + } +} diff --git a/Scalar.FunctionalTests/Program.cs b/Scalar.FunctionalTests/Program.cs index f148dd2ee4..d9d81e015e 100644 --- a/Scalar.FunctionalTests/Program.cs +++ b/Scalar.FunctionalTests/Program.cs @@ -1,157 +1,157 @@ -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests -{ - public class Program - { - public static void Main(string[] args) - { - Properties.Settings.Default.Initialize(); - NUnitRunner runner = new NUnitRunner(args); - - if (runner.HasCustomArg("--no-shared-scalar-cache")) - { - Console.WriteLine("Running without a shared git object cache"); - ScalarTestConfig.NoSharedCache = true; - } - - if (runner.HasCustomArg("--test-scalar-on-path")) - { - Console.WriteLine("Running tests against Scalar on path"); - ScalarTestConfig.TestScalarOnPath = true; - } - - ScalarTestConfig.LocalCacheRoot = runner.GetCustomArgWithParam("--shared-scalar-cache-root"); - - HashSet includeCategories = new HashSet(); - HashSet excludeCategories = new HashSet(); - - if (runner.HasCustomArg("--full-suite")) - { - Console.WriteLine("Running the full suite of tests"); - - List modes = new List(); - foreach (Settings.ValidateWorkingTreeMode mode in Enum.GetValues(typeof(Settings.ValidateWorkingTreeMode))) - { - modes.Add(new object[] { mode }); - } - - ScalarTestConfig.GitRepoTestsValidateWorkTree = modes.ToArray(); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.AllWindowsRunners; - } - else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.AllMacRunners; - } - } - else - { - Settings.ValidateWorkingTreeMode validateMode = Settings.ValidateWorkingTreeMode.Full; - - if (runner.HasCustomArg("--sparse-mode")) - { - validateMode = Settings.ValidateWorkingTreeMode.SparseMode; - - // Only test the git commands in sparse mode for splitting out tests in builds - includeCategories.Add(Categories.GitCommands); - } - - ScalarTestConfig.GitRepoTestsValidateWorkTree = - new object[] - { - new object[] { validateMode }, - }; - - if (runner.HasCustomArg("--extra-only")) - { - Console.WriteLine("Running only the tests marked as ExtraCoverage"); - includeCategories.Add(Categories.ExtraCoverage); - } - else - { - excludeCategories.Add(Categories.ExtraCoverage); - } - - ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.DefaultRunners; - } - - if (runner.HasCustomArg("--windows-only")) - { - includeCategories.Add(Categories.WindowsOnly); - - // RunTests unions all includeCategories. Remove ExtraCoverage to - // ensure that we only run tests flagged as WindowsOnly - includeCategories.Remove(Categories.ExtraCoverage); - } - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - excludeCategories.Add(Categories.MacTODO.NeedsNewFolderCreateNotification); - excludeCategories.Add(Categories.MacTODO.NeedsScalarConfig); - excludeCategories.Add(Categories.MacTODO.NeedsDehydrate); - excludeCategories.Add(Categories.MacTODO.NeedsServiceVerb); - excludeCategories.Add(Categories.MacTODO.NeedsStatusCache); - excludeCategories.Add(Categories.MacTODO.TestNeedsToLockFile); - excludeCategories.Add(Categories.WindowsOnly); - } - else - { - excludeCategories.Add(Categories.MacOnly); - } - - // For now, run all of the tests not flagged as needing to be updated to work - // with the non-virtualized solution - includeCategories.Clear(); - excludeCategories.Clear(); - excludeCategories.Add(Categories.NeedsUpdatesForNonVirtualizedMode); - if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - excludeCategories.Add(Categories.MacOnly); - } - - ScalarTestConfig.DotScalarRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? ".scalar" : ".scalar"; - - ScalarTestConfig.RepoToClone = - runner.GetCustomArgWithParam("--repo-to-clone") - ?? Properties.Settings.Default.RepoToClone; - - RunBeforeAnyTests(); - Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories); - - if (Debugger.IsAttached) - { - Console.WriteLine("Tests completed. Press Enter to exit."); - Console.ReadLine(); - } - } - - private static void RunBeforeAnyTests() - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - ScalarServiceProcess.InstallService(); - - string statusCacheVersionTokenPath = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), - "Scalar", - "Scalar.Service", - "EnableGitStatusCacheToken.dat"); - - if (!File.Exists(statusCacheVersionTokenPath)) - { - File.WriteAllText(statusCacheVersionTokenPath, string.Empty); - } - } - } - } -} +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests +{ + public class Program + { + public static void Main(string[] args) + { + Properties.Settings.Default.Initialize(); + NUnitRunner runner = new NUnitRunner(args); + + if (runner.HasCustomArg("--no-shared-scalar-cache")) + { + Console.WriteLine("Running without a shared git object cache"); + ScalarTestConfig.NoSharedCache = true; + } + + if (runner.HasCustomArg("--test-scalar-on-path")) + { + Console.WriteLine("Running tests against Scalar on path"); + ScalarTestConfig.TestScalarOnPath = true; + } + + ScalarTestConfig.LocalCacheRoot = runner.GetCustomArgWithParam("--shared-scalar-cache-root"); + + HashSet includeCategories = new HashSet(); + HashSet excludeCategories = new HashSet(); + + if (runner.HasCustomArg("--full-suite")) + { + Console.WriteLine("Running the full suite of tests"); + + List modes = new List(); + foreach (Settings.ValidateWorkingTreeMode mode in Enum.GetValues(typeof(Settings.ValidateWorkingTreeMode))) + { + modes.Add(new object[] { mode }); + } + + ScalarTestConfig.GitRepoTestsValidateWorkTree = modes.ToArray(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.AllWindowsRunners; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.AllMacRunners; + } + } + else + { + Settings.ValidateWorkingTreeMode validateMode = Settings.ValidateWorkingTreeMode.Full; + + if (runner.HasCustomArg("--sparse-mode")) + { + validateMode = Settings.ValidateWorkingTreeMode.SparseMode; + + // Only test the git commands in sparse mode for splitting out tests in builds + includeCategories.Add(Categories.GitCommands); + } + + ScalarTestConfig.GitRepoTestsValidateWorkTree = + new object[] + { + new object[] { validateMode }, + }; + + if (runner.HasCustomArg("--extra-only")) + { + Console.WriteLine("Running only the tests marked as ExtraCoverage"); + includeCategories.Add(Categories.ExtraCoverage); + } + else + { + excludeCategories.Add(Categories.ExtraCoverage); + } + + ScalarTestConfig.FileSystemRunners = FileSystemRunners.FileSystemRunner.DefaultRunners; + } + + if (runner.HasCustomArg("--windows-only")) + { + includeCategories.Add(Categories.WindowsOnly); + + // RunTests unions all includeCategories. Remove ExtraCoverage to + // ensure that we only run tests flagged as WindowsOnly + includeCategories.Remove(Categories.ExtraCoverage); + } + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + excludeCategories.Add(Categories.MacTODO.NeedsNewFolderCreateNotification); + excludeCategories.Add(Categories.MacTODO.NeedsScalarConfig); + excludeCategories.Add(Categories.MacTODO.NeedsDehydrate); + excludeCategories.Add(Categories.MacTODO.NeedsServiceVerb); + excludeCategories.Add(Categories.MacTODO.NeedsStatusCache); + excludeCategories.Add(Categories.MacTODO.TestNeedsToLockFile); + excludeCategories.Add(Categories.WindowsOnly); + } + else + { + excludeCategories.Add(Categories.MacOnly); + } + + // For now, run all of the tests not flagged as needing to be updated to work + // with the non-virtualized solution + includeCategories.Clear(); + excludeCategories.Clear(); + excludeCategories.Add(Categories.NeedsUpdatesForNonVirtualizedMode); + if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + excludeCategories.Add(Categories.MacOnly); + } + + ScalarTestConfig.DotScalarRoot = RuntimeInformation.IsOSPlatform(OSPlatform.Linux) ? ".scalar" : ".scalar"; + + ScalarTestConfig.RepoToClone = + runner.GetCustomArgWithParam("--repo-to-clone") + ?? Properties.Settings.Default.RepoToClone; + + RunBeforeAnyTests(); + Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories); + + if (Debugger.IsAttached) + { + Console.WriteLine("Tests completed. Press Enter to exit."); + Console.ReadLine(); + } + } + + private static void RunBeforeAnyTests() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + ScalarServiceProcess.InstallService(); + + string statusCacheVersionTokenPath = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), + "Scalar", + "Scalar.Service", + "EnableGitStatusCacheToken.dat"); + + if (!File.Exists(statusCacheVersionTokenPath)) + { + File.WriteAllText(statusCacheVersionTokenPath, string.Empty); + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Scalar.FunctionalTests.csproj b/Scalar.FunctionalTests/Scalar.FunctionalTests.csproj index 5b1d841de2..77da6dfc27 100644 --- a/Scalar.FunctionalTests/Scalar.FunctionalTests.csproj +++ b/Scalar.FunctionalTests/Scalar.FunctionalTests.csproj @@ -1,52 +1,52 @@ - - - - Exe - netcoreapp2.1 - x64 - - true - true - win-x64;osx-x64 - - - - Scalar.FunctionalTests - Scalar.FunctionalTests - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - - - - all - - - - - - - - - - - - - - - - - - - - - - - + + + + Exe + netcoreapp2.1 + x64 + + true + true + win-x64;osx-x64 + + + + Scalar.FunctionalTests + Scalar.FunctionalTests + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + + + + all + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.FunctionalTests/ScalarTestConfig.cs b/Scalar.FunctionalTests/ScalarTestConfig.cs index f189b39c7b..cc8b5c9868 100644 --- a/Scalar.FunctionalTests/ScalarTestConfig.cs +++ b/Scalar.FunctionalTests/ScalarTestConfig.cs @@ -1,32 +1,32 @@ -using System.IO; - -namespace Scalar.FunctionalTests -{ - public static class ScalarTestConfig - { - public static string RepoToClone { get; set; } - - public static bool NoSharedCache { get; set; } - - public static string LocalCacheRoot { get; set; } - - public static object[] FileSystemRunners { get; set; } - - public static object[] GitRepoTestsValidateWorkTree { get; set; } - - public static bool TestScalarOnPath { get; set; } - - public static string PathToScalar - { - get - { - return - TestScalarOnPath ? - Properties.Settings.Default.PathToScalar : - Path.Combine(Properties.Settings.Default.CurrentDirectory, Properties.Settings.Default.PathToScalar); - } - } - - public static string DotScalarRoot { get; set; } - } -} +using System.IO; + +namespace Scalar.FunctionalTests +{ + public static class ScalarTestConfig + { + public static string RepoToClone { get; set; } + + public static bool NoSharedCache { get; set; } + + public static string LocalCacheRoot { get; set; } + + public static object[] FileSystemRunners { get; set; } + + public static object[] GitRepoTestsValidateWorkTree { get; set; } + + public static bool TestScalarOnPath { get; set; } + + public static string PathToScalar + { + get + { + return + TestScalarOnPath ? + Properties.Settings.Default.PathToScalar : + Path.Combine(Properties.Settings.Default.CurrentDirectory, Properties.Settings.Default.PathToScalar); + } + } + + public static string DotScalarRoot { get; set; } + } +} diff --git a/Scalar.FunctionalTests/Settings.cs b/Scalar.FunctionalTests/Settings.cs index 9ed6c970d4..e3f0992e5b 100644 --- a/Scalar.FunctionalTests/Settings.cs +++ b/Scalar.FunctionalTests/Settings.cs @@ -1,72 +1,72 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Properties -{ - public static class Settings - { - public enum ValidateWorkingTreeMode - { - None = 0, - Full = 1, - SparseMode = 2, - } - - public static class Default - { - public static string CurrentDirectory { get; private set; } - - public static string RepoToClone { get; set; } - public static string PathToBash { get; set; } - public static string PathToScalar { get; set; } - public static string Commitish { get; set; } - public static string ControlGitRepoRoot { get; set; } - public static string EnlistmentRoot { get; set; } - public static string FastFetchBaseRoot { get; set; } - public static string FastFetchRoot { get; set; } - public static string FastFetchControl { get; set; } - public static string PathToGit { get; set; } - public static string PathToScalarService { get; set; } - public static string BinaryFileNameExtension { get; set; } - - public static void Initialize() - { - CurrentDirectory = Path.GetFullPath(Path.GetDirectoryName(Environment.GetCommandLineArgs()[0])); - - RepoToClone = @"https://gvfs.visualstudio.com/ci/_git/ForTests"; - Commitish = @"FunctionalTests/20180214"; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - EnlistmentRoot = @"C:\Repos\ScalarFunctionalTests\enlistment"; - PathToScalar = @"Scalar.exe"; - PathToGit = @"C:\Program Files\Git\cmd\git.exe"; - PathToBash = @"C:\Program Files\Git\bin\bash.exe"; - - ControlGitRepoRoot = @"C:\Repos\ScalarFunctionalTests\ControlRepo"; - FastFetchBaseRoot = @"C:\Repos\ScalarFunctionalTests\FastFetch"; - FastFetchRoot = Path.Combine(FastFetchBaseRoot, "test"); - FastFetchControl = Path.Combine(FastFetchBaseRoot, "control"); - PathToScalarService = @"Scalar.Service.exe"; - BinaryFileNameExtension = ".exe"; - } - else - { - string root = Path.Combine( - Environment.GetEnvironmentVariable("HOME"), - "Scalar.FT"); - EnlistmentRoot = Path.Combine(root, "test"); - ControlGitRepoRoot = Path.Combine(root, "control"); - FastFetchBaseRoot = Path.Combine(root, "FastFetch"); - FastFetchRoot = Path.Combine(FastFetchBaseRoot, "test"); - FastFetchControl = Path.Combine(FastFetchBaseRoot, "control"); - PathToScalar = "scalar"; - PathToGit = "/usr/local/bin/git"; - PathToBash = "/bin/bash"; - BinaryFileNameExtension = string.Empty; - } - } - } - } -} +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Properties +{ + public static class Settings + { + public enum ValidateWorkingTreeMode + { + None = 0, + Full = 1, + SparseMode = 2, + } + + public static class Default + { + public static string CurrentDirectory { get; private set; } + + public static string RepoToClone { get; set; } + public static string PathToBash { get; set; } + public static string PathToScalar { get; set; } + public static string Commitish { get; set; } + public static string ControlGitRepoRoot { get; set; } + public static string EnlistmentRoot { get; set; } + public static string FastFetchBaseRoot { get; set; } + public static string FastFetchRoot { get; set; } + public static string FastFetchControl { get; set; } + public static string PathToGit { get; set; } + public static string PathToScalarService { get; set; } + public static string BinaryFileNameExtension { get; set; } + + public static void Initialize() + { + CurrentDirectory = Path.GetFullPath(Path.GetDirectoryName(Environment.GetCommandLineArgs()[0])); + + RepoToClone = @"https://gvfs.visualstudio.com/ci/_git/ForTests"; + Commitish = @"FunctionalTests/20180214"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + EnlistmentRoot = @"C:\Repos\ScalarFunctionalTests\enlistment"; + PathToScalar = @"Scalar.exe"; + PathToGit = @"C:\Program Files\Git\cmd\git.exe"; + PathToBash = @"C:\Program Files\Git\bin\bash.exe"; + + ControlGitRepoRoot = @"C:\Repos\ScalarFunctionalTests\ControlRepo"; + FastFetchBaseRoot = @"C:\Repos\ScalarFunctionalTests\FastFetch"; + FastFetchRoot = Path.Combine(FastFetchBaseRoot, "test"); + FastFetchControl = Path.Combine(FastFetchBaseRoot, "control"); + PathToScalarService = @"Scalar.Service.exe"; + BinaryFileNameExtension = ".exe"; + } + else + { + string root = Path.Combine( + Environment.GetEnvironmentVariable("HOME"), + "Scalar.FT"); + EnlistmentRoot = Path.Combine(root, "test"); + ControlGitRepoRoot = Path.Combine(root, "control"); + FastFetchBaseRoot = Path.Combine(root, "FastFetch"); + FastFetchRoot = Path.Combine(FastFetchBaseRoot, "test"); + FastFetchControl = Path.Combine(FastFetchBaseRoot, "control"); + PathToScalar = "scalar"; + PathToGit = "/usr/local/bin/git"; + PathToBash = "/bin/bash"; + BinaryFileNameExtension = string.Empty; + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Should/FileSystemShouldExtensions.cs b/Scalar.FunctionalTests/Should/FileSystemShouldExtensions.cs index e90213468a..6fc0064fe3 100644 --- a/Scalar.FunctionalTests/Should/FileSystemShouldExtensions.cs +++ b/Scalar.FunctionalTests/Should/FileSystemShouldExtensions.cs @@ -1,397 +1,397 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.FunctionalTests.Should -{ - public static class FileSystemShouldExtensions - { - // This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, - // FILE_BOTH_DIR_INFORMATION, etc.). When this attribute is set, it means that the file or - // directory has no physical representation on the local system; the item is virtual. Opening the - // item will be more expensive than normal, e.g. it will cause at least some of it to be fetched - // from a remote store. - // - // #define FILE_ATTRIBUTE_RECALL_ON_OPEN 0x00040000 // winnt - public const int FileAttributeRecallOnOpen = 0x00040000; - - // When this attribute is set, it means that the file or directory is not fully present locally. - // For a file that means that not all of its data is on local storage (e.g. it is sparse with some - // data still in remote storage). For a directory it means that some of the directory contents are - // being virtualized from another location. Reading the file / enumerating the directory will be - // more expensive than normal, e.g. it will cause at least some of the file/directory content to be - // fetched from a remote store. Only kernel-mode callers can set this bit. - // - // #define FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS 0x00400000 // winnt - public const int FileAttributeRecallOnDataAccess = 0x00400000; - - public static FileAdapter ShouldBeAFile(this string path, FileSystemRunner runner) - { - return new FileAdapter(path, runner); - } - - public static FileAdapter ShouldBeAFile(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) - { - return new FileAdapter(fileSystemInfo.FullName, runner); - } - - public static DirectoryAdapter ShouldBeADirectory(this string path, FileSystemRunner runner) - { - return new DirectoryAdapter(path, runner); - } - - public static DirectoryAdapter ShouldBeADirectory(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) - { - return new DirectoryAdapter(fileSystemInfo.FullName, runner); - } - - public static string ShouldNotExistOnDisk(this string path, FileSystemRunner runner) - { - runner.FileExists(path).ShouldEqual(false, "File " + path + " exists when it should not"); - runner.DirectoryExists(path).ShouldEqual(false, "Directory " + path + " exists when it should not"); - return path; - } - - public class FileAdapter - { - private const int MaxWaitMS = 2000; - private const int ThreadSleepMS = 100; - - private FileSystemRunner runner; - - public FileAdapter(string path, FileSystemRunner runner) - { - this.runner = runner; - this.runner.FileExists(path).ShouldEqual(true, "Path does NOT exist: " + path); - this.Path = path; - } - - public string Path - { - get; private set; - } - - public string WithContents() - { - return this.runner.ReadAllText(this.Path); - } - - public FileAdapter WithContents(string expectedContents) - { - this.runner.ReadAllText(this.Path).ShouldEqual(expectedContents, "The contents of " + this.Path + " do not match what was expected"); - return this; - } - - public FileAdapter WithCaseMatchingName(string expectedName) - { - FileInfo fileInfo = new FileInfo(this.Path); - string parentPath = System.IO.Path.GetDirectoryName(this.Path); - DirectoryInfo parentInfo = new DirectoryInfo(parentPath); - expectedName.Equals(parentInfo.GetFileSystemInfos(fileInfo.Name)[0].Name, StringComparison.Ordinal) - .ShouldEqual(true, this.Path + " does not have the correct case"); - return this; - } - - public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) - { - FileInfo info = new FileInfo(this.Path); - info.CreationTime.ShouldEqual(creation, "Creation time does not match"); - info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); - info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); - - return info; - } - - public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes) - { - FileInfo info = this.WithInfo(creation, lastWrite, lastAccess); - info.Attributes.ShouldEqual(attributes, "Attributes do not match"); - return info; - } - - public FileInfo WithAttribute(FileAttributes attribute) - { - FileInfo info = new FileInfo(this.Path); - info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); - return info; - } - - public FileInfo WithoutAttribute(FileAttributes attribute) - { - FileInfo info = new FileInfo(this.Path); - info.Attributes.HasFlag(attribute).ShouldEqual(false, "Attributes have incorrect flag: " + attribute); - return info; - } - } - - public class DirectoryAdapter - { - private FileSystemRunner runner; - - public DirectoryAdapter(string path, FileSystemRunner runner) - { - this.runner = runner; - this.runner.DirectoryExists(path).ShouldEqual(true, "Directory " + path + " does not exist"); - this.Path = path; - } - - public string Path - { - get; private set; - } - - public void WithNoItems() - { - Directory.EnumerateFileSystemEntries(this.Path).ShouldBeEmpty(this.Path + " is not empty"); - } - - public void WithNoItems(string searchPattern) - { - Directory.EnumerateFileSystemEntries(this.Path, searchPattern).ShouldBeEmpty(this.Path + " is not empty"); - } - - public FileSystemInfo WithOneItem() - { - return this.WithItems(1).Single(); - } - - public IEnumerable WithItems(int expectedCount) - { - IEnumerable items = this.WithItems(); - items.Count().ShouldEqual(expectedCount, this.Path + " has an invalid number of items"); - return items; - } - - public IEnumerable WithItems() - { - return this.WithItems("*"); - } - - public IEnumerable WithFiles() - { - IEnumerable items = this.WithItems(); - IEnumerable files = items.Where(info => info is FileInfo).Cast(); - files.Any().ShouldEqual(true, this.Path + " does not have any files. Contents: " + string.Join(",", items)); - return files; - } - - public IEnumerable WithDirectories() - { - IEnumerable items = this.WithItems(); - IEnumerable directories = items.Where(info => info is DirectoryInfo).Cast(); - directories.Any().ShouldEqual(true, this.Path + " does not have any directories. Contents: " + string.Join(",", items)); - return directories; - } - - public IEnumerable WithItems(string searchPattern) - { - DirectoryInfo directory = new DirectoryInfo(this.Path); - IEnumerable items = directory.GetFileSystemInfos(searchPattern); - items.Any().ShouldEqual(true, this.Path + " does not have any items"); - return items; - } - - public DirectoryAdapter WithDeepStructure( - FileSystemRunner fileSystem, - string otherPath, - bool ignoreCase = false, - bool compareContent = false, - string[] withinPrefixes = null) - { - otherPath.ShouldBeADirectory(this.runner); - CompareDirectories(fileSystem, otherPath, this.Path, ignoreCase, compareContent, withinPrefixes); - return this; - } - - public DirectoryAdapter WithCaseMatchingName(string expectedName) - { - DirectoryInfo info = new DirectoryInfo(this.Path); - string parentPath = System.IO.Path.GetDirectoryName(this.Path); - DirectoryInfo parentInfo = new DirectoryInfo(parentPath); - expectedName.Equals(parentInfo.GetDirectories(info.Name)[0].Name, StringComparison.Ordinal) - .ShouldEqual(true, this.Path + " does not have the correct case"); - return this; - } - - public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) - { - DirectoryInfo info = new DirectoryInfo(this.Path); - info.CreationTime.ShouldEqual(creation, "Creation time does not match"); - info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); - info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); - - return info; - } - - public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes, bool ignoreRecallAttributes) - { - DirectoryInfo info = this.WithInfo(creation, lastWrite, lastAccess); - if (ignoreRecallAttributes) - { - FileAttributes attributesWithoutRecall = info.Attributes & (FileAttributes)~(FileAttributeRecallOnOpen | FileAttributeRecallOnDataAccess); - attributesWithoutRecall.ShouldEqual(attributes, "Attributes do not match"); - } - else - { - info.Attributes.ShouldEqual(attributes, "Attributes do not match"); - } - - return info; - } - - public DirectoryInfo WithAttribute(FileAttributes attribute) - { - DirectoryInfo info = new DirectoryInfo(this.Path); - info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); - return info; - } - - private static bool IsMatchedPath(FileSystemInfo info, string repoRoot, string[] prefixes) - { - if (prefixes == null || prefixes.Length == 0) - { - return true; - } - - string localPath = info.FullName.Substring(repoRoot.Length + 1); - - if (localPath.Equals(".git", StringComparison.OrdinalIgnoreCase)) - { - // Include _just_ the .git folder. - // All sub-items are not included in the enumerator. - return true; - } - - if (!localPath.Contains(System.IO.Path.DirectorySeparatorChar) && - (info.Attributes & FileAttributes.Directory) != FileAttributes.Directory) - { - // If it is a file in the root folder, then include it. - return true; - } - - foreach (string prefixDir in prefixes) - { - if (localPath.StartsWith(prefixDir, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - - if (prefixDir.StartsWith(localPath, StringComparison.OrdinalIgnoreCase) && - Directory.Exists(info.FullName)) - { - // For example: localPath = "Scalar" and prefix is "Scalar\\Scalar". - return true; - } - } - - return false; - } - - private static void CompareDirectories( - FileSystemRunner fileSystem, - string expectedPath, - string actualPath, - bool ignoreCase, - bool compareContent, - string[] withinPrefixes) - { - IEnumerable expectedEntries = new DirectoryInfo(expectedPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); - IEnumerable actualEntries = new DirectoryInfo(actualPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); - - string dotGitFolder = System.IO.Path.DirectorySeparatorChar + TestConstants.DotGit.Root + System.IO.Path.DirectorySeparatorChar; - IEnumerator expectedEnumerator = expectedEntries - .Where(x => !x.FullName.Contains(dotGitFolder)) - .OrderBy(x => x.FullName) - .Where(x => IsMatchedPath(x, expectedPath, withinPrefixes)) - .GetEnumerator(); - IEnumerator actualEnumerator = actualEntries - .Where(x => !x.FullName.Contains(dotGitFolder)) - .OrderBy(x => x.FullName) - .GetEnumerator(); - - bool expectedMoved = expectedEnumerator.MoveNext(); - bool actualMoved = actualEnumerator.MoveNext(); - - while (expectedMoved && actualMoved) - { - bool nameIsEqual = false; - if (ignoreCase) - { - nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.OrdinalIgnoreCase); - } - else - { - nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.Ordinal); - } - - if (!nameIsEqual) - { - if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) - { - // ignoring directories that are empty in the control repo because Scalar does a better job at removing - // empty directories because it is tracking placeholder folders and removes them - // Only want to check for an empty directory if the names don't match. If the names match and - // both expected and actual directories are empty that is okay - if (Directory.GetFileSystemEntries(expectedEnumerator.Current.FullName, "*", SearchOption.TopDirectoryOnly).Length == 0) - { - expectedMoved = expectedEnumerator.MoveNext(); - - continue; - } - } - - Assert.Fail($"File names don't match: expected: {expectedEnumerator.Current.FullName} actual: {actualEnumerator.Current.FullName}"); - } - - if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) - { - (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldEqual(FileAttributes.Directory, $"expected directory path: {expectedEnumerator.Current.FullName} actual file path: {actualEnumerator.Current.FullName}"); - } - else - { - (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldNotEqual(FileAttributes.Directory, $"expected file path: {expectedEnumerator.Current.FullName} actual directory path: {actualEnumerator.Current.FullName}"); - - FileInfo expectedFileInfo = (expectedEnumerator.Current as FileInfo).ShouldNotBeNull(); - FileInfo actualFileInfo = (actualEnumerator.Current as FileInfo).ShouldNotBeNull(); - actualFileInfo.Length.ShouldEqual(expectedFileInfo.Length, $"File lengths do not agree expected: {expectedEnumerator.Current.FullName} = {expectedFileInfo.Length} actual: {actualEnumerator.Current.FullName} = {actualFileInfo.Length}"); - - if (compareContent) - { - actualFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents(expectedFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents()); - } - } - - expectedMoved = expectedEnumerator.MoveNext(); - actualMoved = actualEnumerator.MoveNext(); - } - - StringBuilder errorEntries = new StringBuilder(); - - if (expectedMoved) - { - do - { - errorEntries.AppendLine(string.Format("Missing entry {0}", expectedEnumerator.Current.FullName)); - } - while (expectedEnumerator.MoveNext()); - } - - while (actualEnumerator.MoveNext()) - { - errorEntries.AppendLine(string.Format("Extra entry {0}", actualEnumerator.Current.FullName)); - } - - if (errorEntries.Length > 0) - { - Assert.Fail(errorEntries.ToString()); - } - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.FunctionalTests.Should +{ + public static class FileSystemShouldExtensions + { + // This attribute only appears in directory enumeration classes (FILE_DIRECTORY_INFORMATION, + // FILE_BOTH_DIR_INFORMATION, etc.). When this attribute is set, it means that the file or + // directory has no physical representation on the local system; the item is virtual. Opening the + // item will be more expensive than normal, e.g. it will cause at least some of it to be fetched + // from a remote store. + // + // #define FILE_ATTRIBUTE_RECALL_ON_OPEN 0x00040000 // winnt + public const int FileAttributeRecallOnOpen = 0x00040000; + + // When this attribute is set, it means that the file or directory is not fully present locally. + // For a file that means that not all of its data is on local storage (e.g. it is sparse with some + // data still in remote storage). For a directory it means that some of the directory contents are + // being virtualized from another location. Reading the file / enumerating the directory will be + // more expensive than normal, e.g. it will cause at least some of the file/directory content to be + // fetched from a remote store. Only kernel-mode callers can set this bit. + // + // #define FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS 0x00400000 // winnt + public const int FileAttributeRecallOnDataAccess = 0x00400000; + + public static FileAdapter ShouldBeAFile(this string path, FileSystemRunner runner) + { + return new FileAdapter(path, runner); + } + + public static FileAdapter ShouldBeAFile(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) + { + return new FileAdapter(fileSystemInfo.FullName, runner); + } + + public static DirectoryAdapter ShouldBeADirectory(this string path, FileSystemRunner runner) + { + return new DirectoryAdapter(path, runner); + } + + public static DirectoryAdapter ShouldBeADirectory(this FileSystemInfo fileSystemInfo, FileSystemRunner runner) + { + return new DirectoryAdapter(fileSystemInfo.FullName, runner); + } + + public static string ShouldNotExistOnDisk(this string path, FileSystemRunner runner) + { + runner.FileExists(path).ShouldEqual(false, "File " + path + " exists when it should not"); + runner.DirectoryExists(path).ShouldEqual(false, "Directory " + path + " exists when it should not"); + return path; + } + + public class FileAdapter + { + private const int MaxWaitMS = 2000; + private const int ThreadSleepMS = 100; + + private FileSystemRunner runner; + + public FileAdapter(string path, FileSystemRunner runner) + { + this.runner = runner; + this.runner.FileExists(path).ShouldEqual(true, "Path does NOT exist: " + path); + this.Path = path; + } + + public string Path + { + get; private set; + } + + public string WithContents() + { + return this.runner.ReadAllText(this.Path); + } + + public FileAdapter WithContents(string expectedContents) + { + this.runner.ReadAllText(this.Path).ShouldEqual(expectedContents, "The contents of " + this.Path + " do not match what was expected"); + return this; + } + + public FileAdapter WithCaseMatchingName(string expectedName) + { + FileInfo fileInfo = new FileInfo(this.Path); + string parentPath = System.IO.Path.GetDirectoryName(this.Path); + DirectoryInfo parentInfo = new DirectoryInfo(parentPath); + expectedName.Equals(parentInfo.GetFileSystemInfos(fileInfo.Name)[0].Name, StringComparison.Ordinal) + .ShouldEqual(true, this.Path + " does not have the correct case"); + return this; + } + + public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) + { + FileInfo info = new FileInfo(this.Path); + info.CreationTime.ShouldEqual(creation, "Creation time does not match"); + info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); + info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); + + return info; + } + + public FileInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes) + { + FileInfo info = this.WithInfo(creation, lastWrite, lastAccess); + info.Attributes.ShouldEqual(attributes, "Attributes do not match"); + return info; + } + + public FileInfo WithAttribute(FileAttributes attribute) + { + FileInfo info = new FileInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); + return info; + } + + public FileInfo WithoutAttribute(FileAttributes attribute) + { + FileInfo info = new FileInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(false, "Attributes have incorrect flag: " + attribute); + return info; + } + } + + public class DirectoryAdapter + { + private FileSystemRunner runner; + + public DirectoryAdapter(string path, FileSystemRunner runner) + { + this.runner = runner; + this.runner.DirectoryExists(path).ShouldEqual(true, "Directory " + path + " does not exist"); + this.Path = path; + } + + public string Path + { + get; private set; + } + + public void WithNoItems() + { + Directory.EnumerateFileSystemEntries(this.Path).ShouldBeEmpty(this.Path + " is not empty"); + } + + public void WithNoItems(string searchPattern) + { + Directory.EnumerateFileSystemEntries(this.Path, searchPattern).ShouldBeEmpty(this.Path + " is not empty"); + } + + public FileSystemInfo WithOneItem() + { + return this.WithItems(1).Single(); + } + + public IEnumerable WithItems(int expectedCount) + { + IEnumerable items = this.WithItems(); + items.Count().ShouldEqual(expectedCount, this.Path + " has an invalid number of items"); + return items; + } + + public IEnumerable WithItems() + { + return this.WithItems("*"); + } + + public IEnumerable WithFiles() + { + IEnumerable items = this.WithItems(); + IEnumerable files = items.Where(info => info is FileInfo).Cast(); + files.Any().ShouldEqual(true, this.Path + " does not have any files. Contents: " + string.Join(",", items)); + return files; + } + + public IEnumerable WithDirectories() + { + IEnumerable items = this.WithItems(); + IEnumerable directories = items.Where(info => info is DirectoryInfo).Cast(); + directories.Any().ShouldEqual(true, this.Path + " does not have any directories. Contents: " + string.Join(",", items)); + return directories; + } + + public IEnumerable WithItems(string searchPattern) + { + DirectoryInfo directory = new DirectoryInfo(this.Path); + IEnumerable items = directory.GetFileSystemInfos(searchPattern); + items.Any().ShouldEqual(true, this.Path + " does not have any items"); + return items; + } + + public DirectoryAdapter WithDeepStructure( + FileSystemRunner fileSystem, + string otherPath, + bool ignoreCase = false, + bool compareContent = false, + string[] withinPrefixes = null) + { + otherPath.ShouldBeADirectory(this.runner); + CompareDirectories(fileSystem, otherPath, this.Path, ignoreCase, compareContent, withinPrefixes); + return this; + } + + public DirectoryAdapter WithCaseMatchingName(string expectedName) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + string parentPath = System.IO.Path.GetDirectoryName(this.Path); + DirectoryInfo parentInfo = new DirectoryInfo(parentPath); + expectedName.Equals(parentInfo.GetDirectories(info.Name)[0].Name, StringComparison.Ordinal) + .ShouldEqual(true, this.Path + " does not have the correct case"); + return this; + } + + public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + info.CreationTime.ShouldEqual(creation, "Creation time does not match"); + info.LastAccessTime.ShouldEqual(lastAccess, "Last access time does not match"); + info.LastWriteTime.ShouldEqual(lastWrite, "Last write time does not match"); + + return info; + } + + public DirectoryInfo WithInfo(DateTime creation, DateTime lastWrite, DateTime lastAccess, FileAttributes attributes, bool ignoreRecallAttributes) + { + DirectoryInfo info = this.WithInfo(creation, lastWrite, lastAccess); + if (ignoreRecallAttributes) + { + FileAttributes attributesWithoutRecall = info.Attributes & (FileAttributes)~(FileAttributeRecallOnOpen | FileAttributeRecallOnDataAccess); + attributesWithoutRecall.ShouldEqual(attributes, "Attributes do not match"); + } + else + { + info.Attributes.ShouldEqual(attributes, "Attributes do not match"); + } + + return info; + } + + public DirectoryInfo WithAttribute(FileAttributes attribute) + { + DirectoryInfo info = new DirectoryInfo(this.Path); + info.Attributes.HasFlag(attribute).ShouldEqual(true, "Attributes do not have correct flag: " + attribute); + return info; + } + + private static bool IsMatchedPath(FileSystemInfo info, string repoRoot, string[] prefixes) + { + if (prefixes == null || prefixes.Length == 0) + { + return true; + } + + string localPath = info.FullName.Substring(repoRoot.Length + 1); + + if (localPath.Equals(".git", StringComparison.OrdinalIgnoreCase)) + { + // Include _just_ the .git folder. + // All sub-items are not included in the enumerator. + return true; + } + + if (!localPath.Contains(System.IO.Path.DirectorySeparatorChar) && + (info.Attributes & FileAttributes.Directory) != FileAttributes.Directory) + { + // If it is a file in the root folder, then include it. + return true; + } + + foreach (string prefixDir in prefixes) + { + if (localPath.StartsWith(prefixDir, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + + if (prefixDir.StartsWith(localPath, StringComparison.OrdinalIgnoreCase) && + Directory.Exists(info.FullName)) + { + // For example: localPath = "Scalar" and prefix is "Scalar\\Scalar". + return true; + } + } + + return false; + } + + private static void CompareDirectories( + FileSystemRunner fileSystem, + string expectedPath, + string actualPath, + bool ignoreCase, + bool compareContent, + string[] withinPrefixes) + { + IEnumerable expectedEntries = new DirectoryInfo(expectedPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); + IEnumerable actualEntries = new DirectoryInfo(actualPath).EnumerateFileSystemInfos("*", SearchOption.AllDirectories); + + string dotGitFolder = System.IO.Path.DirectorySeparatorChar + TestConstants.DotGit.Root + System.IO.Path.DirectorySeparatorChar; + IEnumerator expectedEnumerator = expectedEntries + .Where(x => !x.FullName.Contains(dotGitFolder)) + .OrderBy(x => x.FullName) + .Where(x => IsMatchedPath(x, expectedPath, withinPrefixes)) + .GetEnumerator(); + IEnumerator actualEnumerator = actualEntries + .Where(x => !x.FullName.Contains(dotGitFolder)) + .OrderBy(x => x.FullName) + .GetEnumerator(); + + bool expectedMoved = expectedEnumerator.MoveNext(); + bool actualMoved = actualEnumerator.MoveNext(); + + while (expectedMoved && actualMoved) + { + bool nameIsEqual = false; + if (ignoreCase) + { + nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.OrdinalIgnoreCase); + } + else + { + nameIsEqual = actualEnumerator.Current.Name.Equals(expectedEnumerator.Current.Name, StringComparison.Ordinal); + } + + if (!nameIsEqual) + { + if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + // ignoring directories that are empty in the control repo because Scalar does a better job at removing + // empty directories because it is tracking placeholder folders and removes them + // Only want to check for an empty directory if the names don't match. If the names match and + // both expected and actual directories are empty that is okay + if (Directory.GetFileSystemEntries(expectedEnumerator.Current.FullName, "*", SearchOption.TopDirectoryOnly).Length == 0) + { + expectedMoved = expectedEnumerator.MoveNext(); + + continue; + } + } + + Assert.Fail($"File names don't match: expected: {expectedEnumerator.Current.FullName} actual: {actualEnumerator.Current.FullName}"); + } + + if ((expectedEnumerator.Current.Attributes & FileAttributes.Directory) == FileAttributes.Directory) + { + (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldEqual(FileAttributes.Directory, $"expected directory path: {expectedEnumerator.Current.FullName} actual file path: {actualEnumerator.Current.FullName}"); + } + else + { + (actualEnumerator.Current.Attributes & FileAttributes.Directory).ShouldNotEqual(FileAttributes.Directory, $"expected file path: {expectedEnumerator.Current.FullName} actual directory path: {actualEnumerator.Current.FullName}"); + + FileInfo expectedFileInfo = (expectedEnumerator.Current as FileInfo).ShouldNotBeNull(); + FileInfo actualFileInfo = (actualEnumerator.Current as FileInfo).ShouldNotBeNull(); + actualFileInfo.Length.ShouldEqual(expectedFileInfo.Length, $"File lengths do not agree expected: {expectedEnumerator.Current.FullName} = {expectedFileInfo.Length} actual: {actualEnumerator.Current.FullName} = {actualFileInfo.Length}"); + + if (compareContent) + { + actualFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents(expectedFileInfo.FullName.ShouldBeAFile(fileSystem).WithContents()); + } + } + + expectedMoved = expectedEnumerator.MoveNext(); + actualMoved = actualEnumerator.MoveNext(); + } + + StringBuilder errorEntries = new StringBuilder(); + + if (expectedMoved) + { + do + { + errorEntries.AppendLine(string.Format("Missing entry {0}", expectedEnumerator.Current.FullName)); + } + while (expectedEnumerator.MoveNext()); + } + + while (actualEnumerator.MoveNext()) + { + errorEntries.AppendLine(string.Format("Extra entry {0}", actualEnumerator.Current.FullName)); + } + + if (errorEntries.Length > 0) + { + Assert.Fail(errorEntries.ToString()); + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/DiskLayoutVersionTests.cs b/Scalar.FunctionalTests/Tests/DiskLayoutVersionTests.cs index e266f58df2..e3bacd1c56 100644 --- a/Scalar.FunctionalTests/Tests/DiskLayoutVersionTests.cs +++ b/Scalar.FunctionalTests/Tests/DiskLayoutVersionTests.cs @@ -1,71 +1,71 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tests.EnlistmentPerTestCase; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Tests -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class DiskLayoutVersionTests : TestsWithEnlistmentPerTestCase - { - private const int WindowsCurrentDiskLayoutMajorVersion = 0; - private const int MacCurrentDiskLayoutMajorVersion = 0; - private const int WindowsCurrentDiskLayoutMinimumMajorVersion = 0; - private const int MacCurrentDiskLayoutMinimumMajorVersion = 0; - private const int CurrentDiskLayoutMinorVersion = 0; - private int currentDiskMajorVersion; - private int currentDiskMinimumMajorVersion; - - [SetUp] - public override void CreateEnlistment() - { - base.CreateEnlistment(); - - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - this.currentDiskMajorVersion = MacCurrentDiskLayoutMajorVersion; - this.currentDiskMinimumMajorVersion = MacCurrentDiskLayoutMinimumMajorVersion; - } - else - { - this.currentDiskMajorVersion = WindowsCurrentDiskLayoutMajorVersion; - this.currentDiskMinimumMajorVersion = WindowsCurrentDiskLayoutMinimumMajorVersion; - } - } - - [TestCase] - public void MountSucceedsIfMinorVersionHasAdvancedButNotMajorVersion() - { - // Advance the minor version, mount should still work - this.Enlistment.UnmountScalar(); - ScalarHelpers.SaveDiskLayoutVersion( - this.Enlistment.DotScalarRoot, - this.currentDiskMajorVersion.ToString(), - (CurrentDiskLayoutMinorVersion + 1).ToString()); - this.Enlistment.TryMountScalar().ShouldBeTrue("Mount should succeed because only the minor version advanced"); - - // Advance the major version, mount should fail - this.Enlistment.UnmountScalar(); - ScalarHelpers.SaveDiskLayoutVersion( - this.Enlistment.DotScalarRoot, - (this.currentDiskMajorVersion + 1).ToString(), - CurrentDiskLayoutMinorVersion.ToString()); - this.Enlistment.TryMountScalar().ShouldBeFalse("Mount should fail because the major version has advanced"); - } - - [TestCase] - public void MountFailsIfBeforeMinimumVersion() - { - // Mount should fail if on disk version is below minimum supported version - this.Enlistment.UnmountScalar(); - ScalarHelpers.SaveDiskLayoutVersion( - this.Enlistment.DotScalarRoot, - (this.currentDiskMinimumMajorVersion - 1).ToString(), - CurrentDiskLayoutMinorVersion.ToString()); - this.Enlistment.TryMountScalar().ShouldBeFalse("Mount should fail because we are before minimum version"); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tests.EnlistmentPerTestCase; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Tests +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class DiskLayoutVersionTests : TestsWithEnlistmentPerTestCase + { + private const int WindowsCurrentDiskLayoutMajorVersion = 0; + private const int MacCurrentDiskLayoutMajorVersion = 0; + private const int WindowsCurrentDiskLayoutMinimumMajorVersion = 0; + private const int MacCurrentDiskLayoutMinimumMajorVersion = 0; + private const int CurrentDiskLayoutMinorVersion = 0; + private int currentDiskMajorVersion; + private int currentDiskMinimumMajorVersion; + + [SetUp] + public override void CreateEnlistment() + { + base.CreateEnlistment(); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + this.currentDiskMajorVersion = MacCurrentDiskLayoutMajorVersion; + this.currentDiskMinimumMajorVersion = MacCurrentDiskLayoutMinimumMajorVersion; + } + else + { + this.currentDiskMajorVersion = WindowsCurrentDiskLayoutMajorVersion; + this.currentDiskMinimumMajorVersion = WindowsCurrentDiskLayoutMinimumMajorVersion; + } + } + + [TestCase] + public void MountSucceedsIfMinorVersionHasAdvancedButNotMajorVersion() + { + // Advance the minor version, mount should still work + this.Enlistment.UnmountScalar(); + ScalarHelpers.SaveDiskLayoutVersion( + this.Enlistment.DotScalarRoot, + this.currentDiskMajorVersion.ToString(), + (CurrentDiskLayoutMinorVersion + 1).ToString()); + this.Enlistment.TryMountScalar().ShouldBeTrue("Mount should succeed because only the minor version advanced"); + + // Advance the major version, mount should fail + this.Enlistment.UnmountScalar(); + ScalarHelpers.SaveDiskLayoutVersion( + this.Enlistment.DotScalarRoot, + (this.currentDiskMajorVersion + 1).ToString(), + CurrentDiskLayoutMinorVersion.ToString()); + this.Enlistment.TryMountScalar().ShouldBeFalse("Mount should fail because the major version has advanced"); + } + + [TestCase] + public void MountFailsIfBeforeMinimumVersion() + { + // Mount should fail if on disk version is below minimum supported version + this.Enlistment.UnmountScalar(); + ScalarHelpers.SaveDiskLayoutVersion( + this.Enlistment.DotScalarRoot, + (this.currentDiskMinimumMajorVersion - 1).ToString(), + CurrentDiskLayoutMinorVersion.ToString()); + this.Enlistment.TryMountScalar().ShouldBeFalse("Mount should fail because we are before minimum version"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CacheServerTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CacheServerTests.cs index 8ec3b0e251..daf9dbef2c 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CacheServerTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CacheServerTests.cs @@ -1,40 +1,40 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class CacheServerTests : TestsWithEnlistmentPerFixture - { - private const string CustomUrl = "https://myCache"; - - [TestCase] - public void SettingGitConfigChangesCacheServer() - { - ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "config scalar.cache-server " + CustomUrl); - result.ExitCode.ShouldEqual(0, result.Errors); - - this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); - } - - [TestCase] - public void SetAndGetTests() - { - this.Enlistment.SetCacheServer("\"\"").ShouldContain("You must specify a value for the cache server"); - - string noneMessage = "Using cache server: None (" + this.Enlistment.RepoUrl + ")"; - - this.Enlistment.SetCacheServer("None").ShouldContain(noneMessage); - this.Enlistment.GetCacheServer().ShouldContain(noneMessage); - - this.Enlistment.SetCacheServer(this.Enlistment.RepoUrl).ShouldContain(noneMessage); - this.Enlistment.GetCacheServer().ShouldContain(noneMessage); - - this.Enlistment.SetCacheServer(CustomUrl).ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); - this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class CacheServerTests : TestsWithEnlistmentPerFixture + { + private const string CustomUrl = "https://myCache"; + + [TestCase] + public void SettingGitConfigChangesCacheServer() + { + ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "config scalar.cache-server " + CustomUrl); + result.ExitCode.ShouldEqual(0, result.Errors); + + this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); + } + + [TestCase] + public void SetAndGetTests() + { + this.Enlistment.SetCacheServer("\"\"").ShouldContain("You must specify a value for the cache server"); + + string noneMessage = "Using cache server: None (" + this.Enlistment.RepoUrl + ")"; + + this.Enlistment.SetCacheServer("None").ShouldContain(noneMessage); + this.Enlistment.GetCacheServer().ShouldContain(noneMessage); + + this.Enlistment.SetCacheServer(this.Enlistment.RepoUrl).ShouldContain(noneMessage); + this.Enlistment.GetCacheServer().ShouldContain(noneMessage); + + this.Enlistment.SetCacheServer(CustomUrl).ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); + this.Enlistment.GetCacheServer().ShouldContain("Using cache server: User Defined (" + CustomUrl + ")"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CloneTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CloneTests.cs index a82f4588b4..bb953104e9 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CloneTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/CloneTests.cs @@ -1,103 +1,103 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; using System; -using System.Diagnostics; -using System.IO; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - public class CloneTests : TestsWithEnlistmentPerFixture - { - private const int ScalarGenericError = 3; - - [TestCase] - public void CloneInsideMountedEnlistment() - { - this.SubfolderCloneShouldFail(); - } - - [TestCase] - public void CloneInsideUnmountedEnlistment() - { - this.Enlistment.UnmountScalar(); - this.SubfolderCloneShouldFail(); - this.Enlistment.MountScalar(); - } - - [TestCase] - public void CloneWithLocalCachePathWithinSrc() - { - string newEnlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); - - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = $"clone {Properties.Settings.Default.RepoToClone} {newEnlistmentRoot} --local-cache-path {Path.Combine(newEnlistmentRoot, "src", ".scalarCache")}"; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = true; - processInfo.WorkingDirectory = Path.GetDirectoryName(this.Enlistment.EnlistmentRoot); - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - ProcessResult result = ProcessHelper.Run(processInfo); - result.ExitCode.ShouldEqual(ScalarGenericError); - result.Output.ShouldContain("'--local-cache-path' cannot be inside the src folder"); - } - +using System.Diagnostics; +using System.IO; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public class CloneTests : TestsWithEnlistmentPerFixture + { + private const int ScalarGenericError = 3; + [TestCase] - [Category(Categories.MacOnly)] + public void CloneInsideMountedEnlistment() + { + this.SubfolderCloneShouldFail(); + } + + [TestCase] + public void CloneInsideUnmountedEnlistment() + { + this.Enlistment.UnmountScalar(); + this.SubfolderCloneShouldFail(); + this.Enlistment.MountScalar(); + } + + [TestCase] + public void CloneWithLocalCachePathWithinSrc() + { + string newEnlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); + + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = $"clone {Properties.Settings.Default.RepoToClone} {newEnlistmentRoot} --local-cache-path {Path.Combine(newEnlistmentRoot, "src", ".scalarCache")}"; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.CreateNoWindow = true; + processInfo.WorkingDirectory = Path.GetDirectoryName(this.Enlistment.EnlistmentRoot); + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + result.ExitCode.ShouldEqual(ScalarGenericError); + result.Output.ShouldContain("'--local-cache-path' cannot be inside the src folder"); + } + + [TestCase] + [Category(Categories.MacOnly)] public void CloneWithDefaultLocalCacheLocation() - { - FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; - string homeDirectory = Environment.GetEnvironmentVariable("HOME"); - homeDirectory.ShouldBeADirectory(fileSystem); - - string newEnlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); - - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = $"clone {Properties.Settings.Default.RepoToClone} {newEnlistmentRoot} --no-mount --no-prefetch"; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = true; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - ProcessResult result = ProcessHelper.Run(processInfo); + { + FileSystemRunner fileSystem = FileSystemRunner.DefaultRunner; + string homeDirectory = Environment.GetEnvironmentVariable("HOME"); + homeDirectory.ShouldBeADirectory(fileSystem); + + string newEnlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); + + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = $"clone {Properties.Settings.Default.RepoToClone} {newEnlistmentRoot} --no-mount --no-prefetch"; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.CreateNoWindow = true; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); result.ExitCode.ShouldEqual(0, result.Errors); - - string dotScalarRoot = Path.Combine(newEnlistmentRoot, ScalarTestConfig.DotScalarRoot); + + string dotScalarRoot = Path.Combine(newEnlistmentRoot, ScalarTestConfig.DotScalarRoot); dotScalarRoot.ShouldBeADirectory(fileSystem); - string localCacheRoot = ScalarHelpers.GetPersistedLocalCacheRoot(dotScalarRoot); - string gitObjectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(dotScalarRoot); - - string defaultScalarCacheRoot = Path.Combine(homeDirectory, ".scalarCache"); - localCacheRoot.StartsWith(defaultScalarCacheRoot, StringComparison.Ordinal).ShouldBeTrue($"Local cache root did not default to using {homeDirectory}"); - gitObjectsRoot.StartsWith(defaultScalarCacheRoot, StringComparison.Ordinal).ShouldBeTrue($"Git objects root did not default to using {homeDirectory}"); - + string localCacheRoot = ScalarHelpers.GetPersistedLocalCacheRoot(dotScalarRoot); + string gitObjectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(dotScalarRoot); + + string defaultScalarCacheRoot = Path.Combine(homeDirectory, ".scalarCache"); + localCacheRoot.StartsWith(defaultScalarCacheRoot, StringComparison.Ordinal).ShouldBeTrue($"Local cache root did not default to using {homeDirectory}"); + gitObjectsRoot.StartsWith(defaultScalarCacheRoot, StringComparison.Ordinal).ShouldBeTrue($"Git objects root did not default to using {homeDirectory}"); + RepositoryHelpers.DeleteTestDirectory(newEnlistmentRoot); - } - - [TestCase] - public void CloneToPathWithSpaces() - { - ScalarFunctionalTestEnlistment enlistment = ScalarFunctionalTestEnlistment.CloneAndMountEnlistmentWithSpacesInPath(ScalarTestConfig.PathToScalar); - enlistment.UnmountAndDeleteAll(); - } - - private void SubfolderCloneShouldFail() - { - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = "clone " + ScalarTestConfig.RepoToClone + " src\\scalar\\test1"; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.CreateNoWindow = true; - processInfo.WorkingDirectory = this.Enlistment.EnlistmentRoot; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - ProcessResult result = ProcessHelper.Run(processInfo); - result.ExitCode.ShouldEqual(ScalarGenericError); - result.Output.ShouldContain("You can't clone inside an existing Scalar repo"); - } - } -} + } + + [TestCase] + public void CloneToPathWithSpaces() + { + ScalarFunctionalTestEnlistment enlistment = ScalarFunctionalTestEnlistment.CloneAndMountEnlistmentWithSpacesInPath(ScalarTestConfig.PathToScalar); + enlistment.UnmountAndDeleteAll(); + } + + private void SubfolderCloneShouldFail() + { + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = "clone " + ScalarTestConfig.RepoToClone + " src\\scalar\\test1"; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.CreateNoWindow = true; + processInfo.WorkingDirectory = this.Enlistment.EnlistmentRoot; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + result.ExitCode.ShouldEqual(ScalarGenericError); + result.Output.ShouldContain("You can't clone inside an existing Scalar repo"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs index 8aa90ce867..c4bb8b2ee6 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/DiagnoseTests.cs @@ -1,38 +1,38 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [NonParallelizable] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class DiagnoseTests : TestsWithEnlistmentPerFixture - { - private FileSystemRunner fileSystem; - - public DiagnoseTests() - { - this.fileSystem = new SystemIORunner(); - } - - [TestCase] - public void DiagnoseProducesZipFile() - { - Directory.Exists(this.Enlistment.DiagnosticsRoot).ShouldEqual(false); - string output = this.Enlistment.Diagnose(); - output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "Failed"); - - IEnumerable files = Directory.EnumerateFiles(this.Enlistment.DiagnosticsRoot); - files.ShouldBeNonEmpty(); - string zipFilePath = files.First(); - - zipFilePath.EndsWith(".zip").ShouldEqual(true); - output.Contains(zipFilePath).ShouldEqual(true); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [NonParallelizable] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class DiagnoseTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem; + + public DiagnoseTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase] + public void DiagnoseProducesZipFile() + { + Directory.Exists(this.Enlistment.DiagnosticsRoot).ShouldEqual(false); + string output = this.Enlistment.Diagnose(); + output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "Failed"); + + IEnumerable files = Directory.EnumerateFiles(this.Enlistment.DiagnosticsRoot); + files.ShouldBeNonEmpty(); + string zipFilePath = files.First(); + + zipFilePath.EndsWith(".zip").ShouldEqual(true); + output.Contains(zipFilePath).ShouldEqual(true); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitCorruptObjectTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitCorruptObjectTests.cs index a7b4de9e3c..be872f3240 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitCorruptObjectTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitCorruptObjectTests.cs @@ -1,153 +1,153 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Linq; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class GitCorruptObjectTests : TestsWithEnlistmentPerFixture - { - private FileSystemRunner fileSystem; - - // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting - // the cache - public GitCorruptObjectTests() - : base(forcePerRepoObjectCache: true) - { - this.fileSystem = new SystemIORunner(); - } - - [TestCase] - public void GitRequestsReplacementForAllNullObject() - { - Action allNullObject = (string objectPath) => - { - FileInfo objectFileInfo = new FileInfo(objectPath); - File.WriteAllBytes(objectPath, Enumerable.Repeat(0, (int)objectFileInfo.Length).ToArray()); - }; - - this.RunGitDiffWithCorruptObject(allNullObject); - this.RunGitCatFileWithCorruptObject(allNullObject); - this.RunGitResetHardWithCorruptObject(allNullObject); - this.RunGitCheckoutOnFileWithCorruptObject(allNullObject); - } - - [TestCase] - public void GitRequestsReplacementForTruncatedObject() - { - Action truncateObject = (string objectPath) => - { - FileInfo objectFileInfo = new FileInfo(objectPath); - using (FileStream objectStream = new FileStream(objectPath, FileMode.Open)) - { - objectStream.SetLength(objectFileInfo.Length - 8); - } - }; - - this.RunGitDiffWithCorruptObject(truncateObject); - - // TODO 1114508: Update git cat-file to request object from Scalar when it finds a truncated object on disk. - ////this.RunGitCatFileWithCorruptObject(truncateObject); - - this.RunGitResetHardWithCorruptObject(truncateObject); - this.RunGitCheckoutOnFileWithCorruptObject(truncateObject); - } - - [TestCase] - public void GitRequestsReplacementForObjectCorruptedWithBadData() - { - Action fillObjectWithBadData = (string objectPath) => - { - this.fileSystem.WriteAllText(objectPath, "Not a valid git object"); - }; - - this.RunGitDiffWithCorruptObject(fillObjectWithBadData); - this.RunGitCatFileWithCorruptObject(fillObjectWithBadData); - this.RunGitResetHardWithCorruptObject(fillObjectWithBadData); - this.RunGitCheckoutOnFileWithCorruptObject(fillObjectWithBadData); - } - - private void RunGitDiffWithCorruptObject(Action corruptObject) - { - string fileName = "Protocol.md"; - string filePath = this.Enlistment.GetVirtualPathTo(fileName); - string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); - string newFileContents = "RunGitDiffWithCorruptObject"; - this.fileSystem.WriteAllText(filePath, newFileContents); - - string sha; - string objectPath = this.GetLooseObjectPath(fileName, out sha); - corruptObject(objectPath); - - ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"diff {fileName}"); - revParseResult.ExitCode.ShouldEqual(0); - revParseResult.Output.ShouldContain("The Scalar network protocol consists of three operations"); - revParseResult.Output.ShouldContain(newFileContents); - } - - private void RunGitCatFileWithCorruptObject(Action corruptObject) - { - string fileName = "Readme.md"; - string filePath = this.Enlistment.GetVirtualPathTo(fileName); - string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); - - string sha; - string objectPath = this.GetLooseObjectPath(fileName, out sha); - corruptObject(objectPath); - - ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"cat-file blob {sha}"); - revParseResult.ExitCode.ShouldEqual(0); - revParseResult.Output.ShouldEqual(fileContents); - } - - private void RunGitResetHardWithCorruptObject(Action corruptObject) - { - string fileName = "Readme.md"; - string filePath = this.Enlistment.GetVirtualPathTo(fileName); - string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); - string newFileContents = "RunGitDiffWithCorruptObject"; - this.fileSystem.WriteAllText(filePath, newFileContents); - - string sha; - string objectPath = this.GetLooseObjectPath(fileName, out sha); - corruptObject(objectPath); - - ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "reset --hard HEAD"); - revParseResult.ExitCode.ShouldEqual(0); - filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); - } - - private void RunGitCheckoutOnFileWithCorruptObject(Action corruptObject) - { - string fileName = "Readme.md"; - string filePath = this.Enlistment.GetVirtualPathTo(fileName); - string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); - string newFileContents = "RunGitDiffWithCorruptObject"; - this.fileSystem.WriteAllText(filePath, newFileContents); - - string sha; - string objectPath = this.GetLooseObjectPath(fileName, out sha); - corruptObject(objectPath); - - ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"checkout -- {fileName}"); - revParseResult.ExitCode.ShouldEqual(0); - filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); - } - - private string GetLooseObjectPath(string fileGitPath, out string sha) - { - ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"rev-parse :{fileGitPath}"); - sha = revParseResult.Output.Trim(); - sha.Length.ShouldEqual(40); - string objectPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), sha.Substring(0, 2), sha.Substring(2, 38)); - return objectPath; - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Linq; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class GitCorruptObjectTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem; + + // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting + // the cache + public GitCorruptObjectTests() + : base(forcePerRepoObjectCache: true) + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase] + public void GitRequestsReplacementForAllNullObject() + { + Action allNullObject = (string objectPath) => + { + FileInfo objectFileInfo = new FileInfo(objectPath); + File.WriteAllBytes(objectPath, Enumerable.Repeat(0, (int)objectFileInfo.Length).ToArray()); + }; + + this.RunGitDiffWithCorruptObject(allNullObject); + this.RunGitCatFileWithCorruptObject(allNullObject); + this.RunGitResetHardWithCorruptObject(allNullObject); + this.RunGitCheckoutOnFileWithCorruptObject(allNullObject); + } + + [TestCase] + public void GitRequestsReplacementForTruncatedObject() + { + Action truncateObject = (string objectPath) => + { + FileInfo objectFileInfo = new FileInfo(objectPath); + using (FileStream objectStream = new FileStream(objectPath, FileMode.Open)) + { + objectStream.SetLength(objectFileInfo.Length - 8); + } + }; + + this.RunGitDiffWithCorruptObject(truncateObject); + + // TODO 1114508: Update git cat-file to request object from Scalar when it finds a truncated object on disk. + ////this.RunGitCatFileWithCorruptObject(truncateObject); + + this.RunGitResetHardWithCorruptObject(truncateObject); + this.RunGitCheckoutOnFileWithCorruptObject(truncateObject); + } + + [TestCase] + public void GitRequestsReplacementForObjectCorruptedWithBadData() + { + Action fillObjectWithBadData = (string objectPath) => + { + this.fileSystem.WriteAllText(objectPath, "Not a valid git object"); + }; + + this.RunGitDiffWithCorruptObject(fillObjectWithBadData); + this.RunGitCatFileWithCorruptObject(fillObjectWithBadData); + this.RunGitResetHardWithCorruptObject(fillObjectWithBadData); + this.RunGitCheckoutOnFileWithCorruptObject(fillObjectWithBadData); + } + + private void RunGitDiffWithCorruptObject(Action corruptObject) + { + string fileName = "Protocol.md"; + string filePath = this.Enlistment.GetVirtualPathTo(fileName); + string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); + string newFileContents = "RunGitDiffWithCorruptObject"; + this.fileSystem.WriteAllText(filePath, newFileContents); + + string sha; + string objectPath = this.GetLooseObjectPath(fileName, out sha); + corruptObject(objectPath); + + ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"diff {fileName}"); + revParseResult.ExitCode.ShouldEqual(0); + revParseResult.Output.ShouldContain("The Scalar network protocol consists of three operations"); + revParseResult.Output.ShouldContain(newFileContents); + } + + private void RunGitCatFileWithCorruptObject(Action corruptObject) + { + string fileName = "Readme.md"; + string filePath = this.Enlistment.GetVirtualPathTo(fileName); + string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); + + string sha; + string objectPath = this.GetLooseObjectPath(fileName, out sha); + corruptObject(objectPath); + + ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"cat-file blob {sha}"); + revParseResult.ExitCode.ShouldEqual(0); + revParseResult.Output.ShouldEqual(fileContents); + } + + private void RunGitResetHardWithCorruptObject(Action corruptObject) + { + string fileName = "Readme.md"; + string filePath = this.Enlistment.GetVirtualPathTo(fileName); + string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); + string newFileContents = "RunGitDiffWithCorruptObject"; + this.fileSystem.WriteAllText(filePath, newFileContents); + + string sha; + string objectPath = this.GetLooseObjectPath(fileName, out sha); + corruptObject(objectPath); + + ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "reset --hard HEAD"); + revParseResult.ExitCode.ShouldEqual(0); + filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); + } + + private void RunGitCheckoutOnFileWithCorruptObject(Action corruptObject) + { + string fileName = "Readme.md"; + string filePath = this.Enlistment.GetVirtualPathTo(fileName); + string fileContents = filePath.ShouldBeAFile(this.fileSystem).WithContents(); + string newFileContents = "RunGitDiffWithCorruptObject"; + this.fileSystem.WriteAllText(filePath, newFileContents); + + string sha; + string objectPath = this.GetLooseObjectPath(fileName, out sha); + corruptObject(objectPath); + + ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"checkout -- {fileName}"); + revParseResult.ExitCode.ShouldEqual(0); + filePath.ShouldBeAFile(this.fileSystem).WithContents(fileContents); + } + + private string GetLooseObjectPath(string fileGitPath, out string sha) + { + ProcessResult revParseResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, $"rev-parse :{fileGitPath}"); + sha = revParseResult.Output.Trim(); + sha.Length.ShouldEqual(40); + string objectPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), sha.Substring(0, 2), sha.Substring(2, 38)); + return objectPath; + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs index 54efb7f556..1a451e558e 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/GitMoveRenameTests.cs @@ -1,260 +1,260 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class GitMoveRenameTests : TestsWithEnlistmentPerFixture - { - private string testFileContents = "0123456789"; - private FileSystemRunner fileSystem; - public GitMoveRenameTests(FileSystemRunner fileSystem) - { - this.fileSystem = fileSystem; - } - - [TestCase, Order(1)] - public void GitStatus() - { - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "nothing to commit, working tree clean"); - } - - [TestCase, Order(2)] - public void GitStatusAfterNewFile() - { - string filename = "new.cs"; - string filePath = this.Enlistment.GetVirtualPathTo(filename); - - filePath.ShouldNotExistOnDisk(this.fileSystem); - this.fileSystem.WriteAllText(filePath, this.testFileContents); - - filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - filename); - - this.fileSystem.DeleteFile(filePath); - } - - [TestCase, Order(3)] - public void GitStatusAfterFileNameCaseChange() - { - string oldFilename = "new.cs"; - this.EnsureTestFileExists(oldFilename); - - string newFilename = "New.cs"; - string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - newFilename); - - this.fileSystem.DeleteFile(newFilePath); - } - - [TestCase, Order(4)] - public void GitStatusAfterFileRename() - { - string oldFilename = "New.cs"; - this.EnsureTestFileExists(oldFilename); - - string newFilename = "test.cs"; - string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - newFilename); - } - - [TestCase, Order(5)] - public void GitStatusAndObjectAfterGitAdd() - { - string existingFilename = "test.cs"; - this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "add " + existingFilename, - new string[] { }); - - // Status should be correct - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Changes to be committed:", - existingFilename); - - // Object file for the test file should have the correct contents - ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( - this.Enlistment.RepoRoot, - "hash-object " + existingFilename); - - string objectHash = result.Output.Trim(); - result.Errors.ShouldBeEmpty(); - - this.Enlistment.GetObjectPathTo(objectHash).ShouldBeAFile(this.fileSystem); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "cat-file -p " + objectHash, - this.testFileContents); - } - - [TestCase, Order(6)] - public void GitStatusAfterUnstage() - { - string existingFilename = "test.cs"; - this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "reset HEAD " + existingFilename, - new string[] { }); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - existingFilename); - } - - [TestCase, Order(7)] - public void GitStatusAfterFileDelete() - { - string existingFilename = "test.cs"; - this.EnsureTestFileExists(existingFilename); - this.fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(existingFilename)); - this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "nothing to commit, working tree clean"); - } - - [TestCase, Order(8)] - public void GitWithEnvironmentVariables() - { - // The trace info is an error, so we can't use CheckGitCommand(). - // We just want to make sure this doesn't throw an exception. - ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( - this.Enlistment.RepoRoot, - "branch", - new Dictionary - { - { "GIT_TRACE_PERFORMANCE", "1" }, - { "git_trace", "1" }, - }, - removeWaitingMessages: false); - result.Output.ShouldContain("* FunctionalTests"); - result.Errors.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "exception"); - result.Errors.ShouldContain("trace.c:", "git command:"); - } - - [TestCase, Order(9)] - public void GitStatusAfterRenameFileIntoRepo() - { - string filename = "GitStatusAfterRenameFileIntoRepo.cs"; - - // Create the test file in this.Enlistment.EnlistmentRoot as it's outside of src - // and is cleaned up when the functional tests run - string filePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); - - this.fileSystem.WriteAllText(filePath, this.testFileContents); - filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); - - string renamedFileName = Path.Combine("GVFlt_MoveFileTest", "GitStatusAfterRenameFileIntoRepo.cs"); - string renamedFilePath = this.Enlistment.GetVirtualPathTo(renamedFileName); - this.fileSystem.MoveFile(filePath, renamedFilePath); - filePath.ShouldNotExistOnDisk(this.fileSystem); - renamedFilePath.ShouldBeAFile(this.fileSystem); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - renamedFileName.Replace('\\', '/')); - } - - [TestCase, Order(10)] - public void GitStatusAfterRenameFileOutOfRepo() - { - string existingFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "Program.cs"); - - // Move the test file to this.Enlistment.EnlistmentRoot as it's outside of src - // and is cleaned up when the functional tests run - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(existingFilename), Path.Combine(this.Enlistment.EnlistmentRoot, "Program.cs")); - this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); - - ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "status"); - result.Output.ShouldContain("On branch " + Properties.Settings.Default.Commitish); - result.Output.ShouldContain("Changes not staged for commit"); - result.Output.ShouldContain("deleted: Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program.cs"); - } - - [TestCase, Order(11)] - public void GitStatusAfterRenameFolderIntoRepo() - { - string folderName = "GitStatusAfterRenameFolderIntoRepo"; - - // Create the test folder in this.Enlistment.EnlistmentRoot as it's outside of src - // and is cleaned up when the functional tests run - string folderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); - - this.fileSystem.CreateDirectory(folderPath); - - string fileName = "GitStatusAfterRenameFolderIntoRepo_file.txt"; - string filePath = Path.Combine(folderPath, fileName); - this.fileSystem.WriteAllText(filePath, this.testFileContents); - filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); - - this.fileSystem.MoveDirectory(folderPath, this.Enlistment.GetVirtualPathTo(folderName)); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status -uall", - "On branch " + Properties.Settings.Default.Commitish, - "Untracked files:", - folderName + "/", - folderName + "/" + fileName); - } - - private void EnsureTestFileExists(string relativePath) - { - string filePath = this.Enlistment.GetVirtualPathTo(relativePath); - if (!this.fileSystem.FileExists(filePath)) - { - this.fileSystem.WriteAllText(filePath, this.testFileContents); - } - - this.Enlistment.GetVirtualPathTo(relativePath).ShouldBeAFile(this.fileSystem); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixtureSource(typeof(FileSystemRunner), nameof(FileSystemRunner.Runners))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class GitMoveRenameTests : TestsWithEnlistmentPerFixture + { + private string testFileContents = "0123456789"; + private FileSystemRunner fileSystem; + public GitMoveRenameTests(FileSystemRunner fileSystem) + { + this.fileSystem = fileSystem; + } + + [TestCase, Order(1)] + public void GitStatus() + { + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "nothing to commit, working tree clean"); + } + + [TestCase, Order(2)] + public void GitStatusAfterNewFile() + { + string filename = "new.cs"; + string filePath = this.Enlistment.GetVirtualPathTo(filename); + + filePath.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.WriteAllText(filePath, this.testFileContents); + + filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + filename); + + this.fileSystem.DeleteFile(filePath); + } + + [TestCase, Order(3)] + public void GitStatusAfterFileNameCaseChange() + { + string oldFilename = "new.cs"; + this.EnsureTestFileExists(oldFilename); + + string newFilename = "New.cs"; + string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + newFilename); + + this.fileSystem.DeleteFile(newFilePath); + } + + [TestCase, Order(4)] + public void GitStatusAfterFileRename() + { + string oldFilename = "New.cs"; + this.EnsureTestFileExists(oldFilename); + + string newFilename = "test.cs"; + string newFilePath = this.Enlistment.GetVirtualPathTo(newFilename); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldFilename), newFilePath); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + newFilename); + } + + [TestCase, Order(5)] + public void GitStatusAndObjectAfterGitAdd() + { + string existingFilename = "test.cs"; + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "add " + existingFilename, + new string[] { }); + + // Status should be correct + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Changes to be committed:", + existingFilename); + + // Object file for the test file should have the correct contents + ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( + this.Enlistment.RepoRoot, + "hash-object " + existingFilename); + + string objectHash = result.Output.Trim(); + result.Errors.ShouldBeEmpty(); + + this.Enlistment.GetObjectPathTo(objectHash).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "cat-file -p " + objectHash, + this.testFileContents); + } + + [TestCase, Order(6)] + public void GitStatusAfterUnstage() + { + string existingFilename = "test.cs"; + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "reset HEAD " + existingFilename, + new string[] { }); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + existingFilename); + } + + [TestCase, Order(7)] + public void GitStatusAfterFileDelete() + { + string existingFilename = "test.cs"; + this.EnsureTestFileExists(existingFilename); + this.fileSystem.DeleteFile(this.Enlistment.GetVirtualPathTo(existingFilename)); + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "nothing to commit, working tree clean"); + } + + [TestCase, Order(8)] + public void GitWithEnvironmentVariables() + { + // The trace info is an error, so we can't use CheckGitCommand(). + // We just want to make sure this doesn't throw an exception. + ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( + this.Enlistment.RepoRoot, + "branch", + new Dictionary + { + { "GIT_TRACE_PERFORMANCE", "1" }, + { "git_trace", "1" }, + }, + removeWaitingMessages: false); + result.Output.ShouldContain("* FunctionalTests"); + result.Errors.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "exception"); + result.Errors.ShouldContain("trace.c:", "git command:"); + } + + [TestCase, Order(9)] + public void GitStatusAfterRenameFileIntoRepo() + { + string filename = "GitStatusAfterRenameFileIntoRepo.cs"; + + // Create the test file in this.Enlistment.EnlistmentRoot as it's outside of src + // and is cleaned up when the functional tests run + string filePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); + + this.fileSystem.WriteAllText(filePath, this.testFileContents); + filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); + + string renamedFileName = Path.Combine("GVFlt_MoveFileTest", "GitStatusAfterRenameFileIntoRepo.cs"); + string renamedFilePath = this.Enlistment.GetVirtualPathTo(renamedFileName); + this.fileSystem.MoveFile(filePath, renamedFilePath); + filePath.ShouldNotExistOnDisk(this.fileSystem); + renamedFilePath.ShouldBeAFile(this.fileSystem); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + renamedFileName.Replace('\\', '/')); + } + + [TestCase, Order(10)] + public void GitStatusAfterRenameFileOutOfRepo() + { + string existingFilename = Path.Combine("Test_EPF_MoveRenameFileTests", "ChangeUnhydratedFileName", "Program.cs"); + + // Move the test file to this.Enlistment.EnlistmentRoot as it's outside of src + // and is cleaned up when the functional tests run + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(existingFilename), Path.Combine(this.Enlistment.EnlistmentRoot, "Program.cs")); + this.Enlistment.GetVirtualPathTo(existingFilename).ShouldNotExistOnDisk(this.fileSystem); + + ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "status"); + result.Output.ShouldContain("On branch " + Properties.Settings.Default.Commitish); + result.Output.ShouldContain("Changes not staged for commit"); + result.Output.ShouldContain("deleted: Test_EPF_MoveRenameFileTests/ChangeUnhydratedFileName/Program.cs"); + } + + [TestCase, Order(11)] + public void GitStatusAfterRenameFolderIntoRepo() + { + string folderName = "GitStatusAfterRenameFolderIntoRepo"; + + // Create the test folder in this.Enlistment.EnlistmentRoot as it's outside of src + // and is cleaned up when the functional tests run + string folderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); + + this.fileSystem.CreateDirectory(folderPath); + + string fileName = "GitStatusAfterRenameFolderIntoRepo_file.txt"; + string filePath = Path.Combine(folderPath, fileName); + this.fileSystem.WriteAllText(filePath, this.testFileContents); + filePath.ShouldBeAFile(this.fileSystem).WithContents(this.testFileContents); + + this.fileSystem.MoveDirectory(folderPath, this.Enlistment.GetVirtualPathTo(folderName)); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status -uall", + "On branch " + Properties.Settings.Default.Commitish, + "Untracked files:", + folderName + "/", + folderName + "/" + fileName); + } + + private void EnsureTestFileExists(string relativePath) + { + string filePath = this.Enlistment.GetVirtualPathTo(relativePath); + if (!this.fileSystem.FileExists(filePath)) + { + this.fileSystem.WriteAllText(filePath, this.testFileContents); + } + + this.Enlistment.GetVirtualPathTo(relativePath).ShouldBeAFile(this.fileSystem); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs index 478d8356b4..78b3856516 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/MountTests.cs @@ -1,310 +1,310 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class MountTests : TestsWithEnlistmentPerFixture - { - private const int ScalarGenericError = 3; - private const uint GenericRead = 2147483648; - private const uint FileFlagBackupSemantics = 3355443; - - private FileSystemRunner fileSystem; - - public MountTests() - { - this.fileSystem = new SystemIORunner(); - } - - [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] - public void SecondMountAttemptFails(string mountSubfolder) - { - this.MountShouldFail(0, "already mounted", this.Enlistment.GetVirtualPathTo(mountSubfolder)); - } - - [TestCase] - public void MountFailsOutsideEnlistment() - { - this.MountShouldFail("is not a valid Scalar enlistment", Path.GetDirectoryName(this.Enlistment.EnlistmentRoot)); - } - - [TestCase] - public void MountCopiesMissingReadObjectHook() - { - this.Enlistment.UnmountScalar(); - - string readObjectPath = this.Enlistment.GetVirtualPathTo(".git", "hooks", "read-object" + Settings.Default.BinaryFileNameExtension); - readObjectPath.ShouldBeAFile(this.fileSystem); - this.fileSystem.DeleteFile(readObjectPath); - readObjectPath.ShouldNotExistOnDisk(this.fileSystem); - this.Enlistment.MountScalar(); - readObjectPath.ShouldBeAFile(this.fileSystem); - } - - [TestCase] - public void MountSetsCoreHooksPath() - { - this.Enlistment.UnmountScalar(); - - GitProcess.Invoke(this.Enlistment.RepoRoot, "config --unset core.hookspath"); - string.IsNullOrWhiteSpace( - GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.hookspath")) - .ShouldBeTrue(); - - this.Enlistment.MountScalar(); - string expectedHooksPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "hooks"); - expectedHooksPath = GitHelpers.ConvertPathToGitFormat(expectedHooksPath); - - GitProcess.Invoke( - this.Enlistment.RepoRoot, "config core.hookspath") - .Trim('\n') - .ShouldEqual(expectedHooksPath); - } - - [TestCase] - public void MountChangesMountId() - { - string mountId = GitProcess.Invoke(this.Enlistment.RepoRoot, "config scalar.mount-id") - .Trim('\n'); - this.Enlistment.UnmountScalar(); - this.Enlistment.MountScalar(); - GitProcess.Invoke(this.Enlistment.RepoRoot, "config scalar.mount-id") - .Trim('\n') - .ShouldNotEqual(mountId, "scalar.mount-id should change on every mount"); - } - - [TestCase] - public void MountFailsWhenNoOnDiskVersion() - { - this.Enlistment.UnmountScalar(); - - // Get the current disk layout version - string majorVersion; - string minorVersion; - ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); - - int majorVersionNum; - int minorVersionNum; - int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); - int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); - - // Move the RepoMetadata database to a temp file - string versionDatabasePath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); - versionDatabasePath.ShouldBeAFile(this.fileSystem); - - string tempDatabasePath = versionDatabasePath + "_MountFailsWhenNoOnDiskVersion"; - tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); - - this.fileSystem.MoveFile(versionDatabasePath, tempDatabasePath); - versionDatabasePath.ShouldNotExistOnDisk(this.fileSystem); - - this.MountShouldFail("Failed to upgrade repo disk layout"); - - // Move the RepoMetadata database back - this.fileSystem.DeleteFile(versionDatabasePath); - this.fileSystem.MoveFile(tempDatabasePath, versionDatabasePath); - tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); - versionDatabasePath.ShouldBeAFile(this.fileSystem); - - this.Enlistment.MountScalar(); - } - - [TestCase] - public void MountFailsWhenNoLocalCacheRootInRepoMetadata() - { - this.Enlistment.UnmountScalar(); - - string majorVersion; - string minorVersion; - ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); - majorVersion.ShouldNotBeNull(); - minorVersion.ShouldNotBeNull(); - - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); - - string metadataPath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); - string metadataBackupPath = metadataPath + ".backup"; - this.fileSystem.MoveFile(metadataPath, metadataBackupPath); - - this.fileSystem.CreateEmptyFile(metadataPath); - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersion, minorVersion); - ScalarHelpers.SaveGitObjectsRoot(this.Enlistment.DotScalarRoot, objectsRoot); - - this.MountShouldFail("Failed to determine local cache path from repo metadata"); - - this.fileSystem.DeleteFile(metadataPath); - this.fileSystem.MoveFile(metadataBackupPath, metadataPath); - - this.Enlistment.MountScalar(); - } - - [TestCase] - public void MountFailsWhenNoGitObjectsRootInRepoMetadata() - { - this.Enlistment.UnmountScalar(); - - string majorVersion; - string minorVersion; - ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); - majorVersion.ShouldNotBeNull(); - minorVersion.ShouldNotBeNull(); - - string localCacheRoot = ScalarHelpers.GetPersistedLocalCacheRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); - - string metadataPath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); - string metadataBackupPath = metadataPath + ".backup"; - this.fileSystem.MoveFile(metadataPath, metadataBackupPath); - - this.fileSystem.CreateEmptyFile(metadataPath); - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersion, minorVersion); - ScalarHelpers.SaveLocalCacheRoot(this.Enlistment.DotScalarRoot, localCacheRoot); - - this.MountShouldFail("Failed to determine git objects root from repo metadata"); - - this.fileSystem.DeleteFile(metadataPath); - this.fileSystem.MoveFile(metadataBackupPath, metadataPath); - - this.Enlistment.MountScalar(); - } - - [TestCase] - public void MountRegeneratesAlternatesFileWhenMissingGitObjectsRoot() - { - this.Enlistment.UnmountScalar(); - - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); - - string alternatesFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "objects", "info", "alternates"); - alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); - this.fileSystem.WriteAllText(alternatesFilePath, "Z:\\invalidPath"); - - this.Enlistment.MountScalar(); - - alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); - } - - [TestCase] - public void MountRegeneratesAlternatesFileWhenMissingFromDisk() - { - this.Enlistment.UnmountScalar(); - - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); - - string alternatesFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "objects", "info", "alternates"); - alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); - this.fileSystem.DeleteFile(alternatesFilePath); - - this.Enlistment.MountScalar(); - - alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); - } - - [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] - public void MountFailsAfterBreakingDowngrade(string mountSubfolder) - { - MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem); - this.Enlistment.UnmountScalar(); - - string majorVersion; - string minorVersion; - ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); - - int majorVersionNum; - int minorVersionNum; - int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); - int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); - - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, (majorVersionNum + 1).ToString(), "0"); - - this.MountShouldFail("do not allow mounting after downgrade", this.Enlistment.GetVirtualPathTo(mountSubfolder)); - - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); - this.Enlistment.MountScalar(); - } - - [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] - public void MountFailsUpgradingFromInvalidUpgradePath(string mountSubfolder) - { - MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem); - string headCommitId = GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse HEAD"); - - this.Enlistment.UnmountScalar(); - - string majorVersion; - string minorVersion; - ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); - - int majorVersionNum; - int minorVersionNum; - int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); - int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); - - // 1 will always be below the minumum support version number - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, "1", "0"); - this.MountShouldFail("Breaking change to Scalar disk layout has been made since cloning", this.Enlistment.GetVirtualPathTo(mountSubfolder)); - - ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); - this.Enlistment.MountScalar(); - } - - private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, string mountWorkingDirectory = null) - { - string enlistmentRoot = this.Enlistment.EnlistmentRoot; - - // TODO: 865304 Use app.config instead of --internal* arguments - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = "mount " + TestConstants.InternalUseOnlyFlag + " " + ScalarHelpers.GetInternalParameter(); - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.WorkingDirectory = string.IsNullOrEmpty(mountWorkingDirectory) ? enlistmentRoot : mountWorkingDirectory; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - ProcessResult result = ProcessHelper.Run(processInfo); - result.ExitCode.ShouldEqual(expectedExitCode, $"mount exit code was not {expectedExitCode}. Output: {result.Output}"); - result.Output.ShouldContain(expectedErrorMessage); - } - - private void MountShouldFail(string expectedErrorMessage, string mountWorkingDirectory = null) - { - this.MountShouldFail(ScalarGenericError, expectedErrorMessage, mountWorkingDirectory); - } - - private class MountSubfolders - { - public const string MountFolders = "Folders"; - private static object[] mountFolders = - { - new object[] { string.Empty }, - new object[] { "Scalar" }, - }; - - public static object[] Folders - { - get - { - return mountFolders; - } - } - - public static void EnsureSubfoldersOnDisk(ScalarFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem) - { - // Enumerate the directory to ensure that the folder is on disk after Scalar is unmounted - foreach (object[] folder in Folders) - { - string folderPath = enlistment.GetVirtualPathTo((string)folder[0]); - folderPath.ShouldBeADirectory(fileSystem).WithItems(); - } - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class MountTests : TestsWithEnlistmentPerFixture + { + private const int ScalarGenericError = 3; + private const uint GenericRead = 2147483648; + private const uint FileFlagBackupSemantics = 3355443; + + private FileSystemRunner fileSystem; + + public MountTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] + public void SecondMountAttemptFails(string mountSubfolder) + { + this.MountShouldFail(0, "already mounted", this.Enlistment.GetVirtualPathTo(mountSubfolder)); + } + + [TestCase] + public void MountFailsOutsideEnlistment() + { + this.MountShouldFail("is not a valid Scalar enlistment", Path.GetDirectoryName(this.Enlistment.EnlistmentRoot)); + } + + [TestCase] + public void MountCopiesMissingReadObjectHook() + { + this.Enlistment.UnmountScalar(); + + string readObjectPath = this.Enlistment.GetVirtualPathTo(".git", "hooks", "read-object" + Settings.Default.BinaryFileNameExtension); + readObjectPath.ShouldBeAFile(this.fileSystem); + this.fileSystem.DeleteFile(readObjectPath); + readObjectPath.ShouldNotExistOnDisk(this.fileSystem); + this.Enlistment.MountScalar(); + readObjectPath.ShouldBeAFile(this.fileSystem); + } + + [TestCase] + public void MountSetsCoreHooksPath() + { + this.Enlistment.UnmountScalar(); + + GitProcess.Invoke(this.Enlistment.RepoRoot, "config --unset core.hookspath"); + string.IsNullOrWhiteSpace( + GitProcess.Invoke(this.Enlistment.RepoRoot, "config core.hookspath")) + .ShouldBeTrue(); + + this.Enlistment.MountScalar(); + string expectedHooksPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "hooks"); + expectedHooksPath = GitHelpers.ConvertPathToGitFormat(expectedHooksPath); + + GitProcess.Invoke( + this.Enlistment.RepoRoot, "config core.hookspath") + .Trim('\n') + .ShouldEqual(expectedHooksPath); + } + + [TestCase] + public void MountChangesMountId() + { + string mountId = GitProcess.Invoke(this.Enlistment.RepoRoot, "config scalar.mount-id") + .Trim('\n'); + this.Enlistment.UnmountScalar(); + this.Enlistment.MountScalar(); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config scalar.mount-id") + .Trim('\n') + .ShouldNotEqual(mountId, "scalar.mount-id should change on every mount"); + } + + [TestCase] + public void MountFailsWhenNoOnDiskVersion() + { + this.Enlistment.UnmountScalar(); + + // Get the current disk layout version + string majorVersion; + string minorVersion; + ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); + + int majorVersionNum; + int minorVersionNum; + int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); + int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); + + // Move the RepoMetadata database to a temp file + string versionDatabasePath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); + versionDatabasePath.ShouldBeAFile(this.fileSystem); + + string tempDatabasePath = versionDatabasePath + "_MountFailsWhenNoOnDiskVersion"; + tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + + this.fileSystem.MoveFile(versionDatabasePath, tempDatabasePath); + versionDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + + this.MountShouldFail("Failed to upgrade repo disk layout"); + + // Move the RepoMetadata database back + this.fileSystem.DeleteFile(versionDatabasePath); + this.fileSystem.MoveFile(tempDatabasePath, versionDatabasePath); + tempDatabasePath.ShouldNotExistOnDisk(this.fileSystem); + versionDatabasePath.ShouldBeAFile(this.fileSystem); + + this.Enlistment.MountScalar(); + } + + [TestCase] + public void MountFailsWhenNoLocalCacheRootInRepoMetadata() + { + this.Enlistment.UnmountScalar(); + + string majorVersion; + string minorVersion; + ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); + majorVersion.ShouldNotBeNull(); + minorVersion.ShouldNotBeNull(); + + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); + + string metadataPath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); + string metadataBackupPath = metadataPath + ".backup"; + this.fileSystem.MoveFile(metadataPath, metadataBackupPath); + + this.fileSystem.CreateEmptyFile(metadataPath); + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersion, minorVersion); + ScalarHelpers.SaveGitObjectsRoot(this.Enlistment.DotScalarRoot, objectsRoot); + + this.MountShouldFail("Failed to determine local cache path from repo metadata"); + + this.fileSystem.DeleteFile(metadataPath); + this.fileSystem.MoveFile(metadataBackupPath, metadataPath); + + this.Enlistment.MountScalar(); + } + + [TestCase] + public void MountFailsWhenNoGitObjectsRootInRepoMetadata() + { + this.Enlistment.UnmountScalar(); + + string majorVersion; + string minorVersion; + ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); + majorVersion.ShouldNotBeNull(); + minorVersion.ShouldNotBeNull(); + + string localCacheRoot = ScalarHelpers.GetPersistedLocalCacheRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); + + string metadataPath = Path.Combine(this.Enlistment.DotScalarRoot, ScalarHelpers.RepoMetadataName); + string metadataBackupPath = metadataPath + ".backup"; + this.fileSystem.MoveFile(metadataPath, metadataBackupPath); + + this.fileSystem.CreateEmptyFile(metadataPath); + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersion, minorVersion); + ScalarHelpers.SaveLocalCacheRoot(this.Enlistment.DotScalarRoot, localCacheRoot); + + this.MountShouldFail("Failed to determine git objects root from repo metadata"); + + this.fileSystem.DeleteFile(metadataPath); + this.fileSystem.MoveFile(metadataBackupPath, metadataPath); + + this.Enlistment.MountScalar(); + } + + [TestCase] + public void MountRegeneratesAlternatesFileWhenMissingGitObjectsRoot() + { + this.Enlistment.UnmountScalar(); + + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); + + string alternatesFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "objects", "info", "alternates"); + alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); + this.fileSystem.WriteAllText(alternatesFilePath, "Z:\\invalidPath"); + + this.Enlistment.MountScalar(); + + alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); + } + + [TestCase] + public void MountRegeneratesAlternatesFileWhenMissingFromDisk() + { + this.Enlistment.UnmountScalar(); + + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(this.Enlistment.DotScalarRoot).ShouldNotBeNull(); + + string alternatesFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "objects", "info", "alternates"); + alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); + this.fileSystem.DeleteFile(alternatesFilePath); + + this.Enlistment.MountScalar(); + + alternatesFilePath.ShouldBeAFile(this.fileSystem).WithContents(objectsRoot); + } + + [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] + public void MountFailsAfterBreakingDowngrade(string mountSubfolder) + { + MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem); + this.Enlistment.UnmountScalar(); + + string majorVersion; + string minorVersion; + ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); + + int majorVersionNum; + int minorVersionNum; + int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); + int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); + + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, (majorVersionNum + 1).ToString(), "0"); + + this.MountShouldFail("do not allow mounting after downgrade", this.Enlistment.GetVirtualPathTo(mountSubfolder)); + + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); + this.Enlistment.MountScalar(); + } + + [TestCaseSource(typeof(MountSubfolders), MountSubfolders.MountFolders)] + public void MountFailsUpgradingFromInvalidUpgradePath(string mountSubfolder) + { + MountSubfolders.EnsureSubfoldersOnDisk(this.Enlistment, this.fileSystem); + string headCommitId = GitProcess.Invoke(this.Enlistment.RepoRoot, "rev-parse HEAD"); + + this.Enlistment.UnmountScalar(); + + string majorVersion; + string minorVersion; + ScalarHelpers.GetPersistedDiskLayoutVersion(this.Enlistment.DotScalarRoot, out majorVersion, out minorVersion); + + int majorVersionNum; + int minorVersionNum; + int.TryParse(majorVersion.ShouldNotBeNull(), out majorVersionNum).ShouldEqual(true); + int.TryParse(minorVersion.ShouldNotBeNull(), out minorVersionNum).ShouldEqual(true); + + // 1 will always be below the minumum support version number + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, "1", "0"); + this.MountShouldFail("Breaking change to Scalar disk layout has been made since cloning", this.Enlistment.GetVirtualPathTo(mountSubfolder)); + + ScalarHelpers.SaveDiskLayoutVersion(this.Enlistment.DotScalarRoot, majorVersionNum.ToString(), minorVersionNum.ToString()); + this.Enlistment.MountScalar(); + } + + private void MountShouldFail(int expectedExitCode, string expectedErrorMessage, string mountWorkingDirectory = null) + { + string enlistmentRoot = this.Enlistment.EnlistmentRoot; + + // TODO: 865304 Use app.config instead of --internal* arguments + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = "mount " + TestConstants.InternalUseOnlyFlag + " " + ScalarHelpers.GetInternalParameter(); + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.WorkingDirectory = string.IsNullOrEmpty(mountWorkingDirectory) ? enlistmentRoot : mountWorkingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + result.ExitCode.ShouldEqual(expectedExitCode, $"mount exit code was not {expectedExitCode}. Output: {result.Output}"); + result.Output.ShouldContain(expectedErrorMessage); + } + + private void MountShouldFail(string expectedErrorMessage, string mountWorkingDirectory = null) + { + this.MountShouldFail(ScalarGenericError, expectedErrorMessage, mountWorkingDirectory); + } + + private class MountSubfolders + { + public const string MountFolders = "Folders"; + private static object[] mountFolders = + { + new object[] { string.Empty }, + new object[] { "Scalar" }, + }; + + public static object[] Folders + { + get + { + return mountFolders; + } + } + + public static void EnsureSubfoldersOnDisk(ScalarFunctionalTestEnlistment enlistment, FileSystemRunner fileSystem) + { + // Enumerate the directory to ensure that the folder is on disk after Scalar is unmounted + foreach (object[] folder in Folders) + { + string folderPath = enlistment.GetVirtualPathTo((string)folder[0]); + folderPath.ShouldBeADirectory(fileSystem).WithItems(); + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PackfileMaintenanceStepTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PackfileMaintenanceStepTests.cs index 70f8198cfc..04bced0d8d 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PackfileMaintenanceStepTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PackfileMaintenanceStepTests.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Scalar.FunctionalTests.FileSystemRunners; using Scalar.FunctionalTests.Tools; using Scalar.Tests.Should; @@ -47,8 +47,8 @@ public void ExpireClonePack() // Ensure we have a multi-pack-index (not created on clone) GitProcess.InvokeProcess( this.Enlistment.RepoRoot, - $"multi-pack-index write --object-dir={this.GitObjectRoot}"); - + $"multi-pack-index write --object-dir={this.GitObjectRoot}"); + this.Enlistment.PackfileMaintenanceStep(); List packs = this.GetPackfiles(); @@ -57,7 +57,7 @@ public void ExpireClonePack() Path.GetFileName(packs[0]) .StartsWith("prefetch-") - .ShouldBeTrue($"packsBetween[0] should start with 'prefetch-': {packs[0]}"); + .ShouldBeTrue($"packsBetween[0] should start with 'prefetch-': {packs[0]}"); } [TestCase, Order(2)] @@ -85,9 +85,9 @@ public void RepackAllToOnePack() this.GetPackSizes(out int packCount, out maxSize, out totalSize); // We should not have expired any packs, but created a new one with repack - packCount.ShouldEqual(afterPrefetchPackCount + 1, $"incorrect number of packs after repack step: {packCount}"); - } - + packCount.ShouldEqual(afterPrefetchPackCount + 1, $"incorrect number of packs after repack step: {packCount}"); + } + [TestCase, Order(3)] public void ExpireAllButOneAndKeep() { diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbTests.cs index e3a93f6357..e89c173b18 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbTests.cs @@ -1,226 +1,226 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [NonParallelizable] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class PrefetchVerbTests : TestsWithEnlistmentPerFixture - { - private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; - private const string LsTreeTypeInPathBranchName = "FunctionalTests/20181105_LsTreeTypeInPath"; - - private FileSystemRunner fileSystem; - - public PrefetchVerbTests() - { - this.fileSystem = new SystemIORunner(); - } - - [TestCase, Order(1)] - public void PrefetchAllMustBeExplicit() - { - this.Enlistment.Prefetch(string.Empty, failOnError: false).ShouldContain("Did you mean to fetch all blobs?"); - } - - [TestCase, Order(2)] - public void PrefetchSpecificFiles() - { - this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("Scalar", "Scalar", "Program.cs")}"), 1); - this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("Scalar", "Scalar", "Program.cs")};{Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj")}"), 2); - } - - [TestCase, Order(3)] - public void PrefetchByFileExtension() - { - this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs"), 199); - this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs;*.csproj"), 208); - } - - [TestCase, Order(4)] - public void PrefetchByFileExtensionWithHydrate() - { - int expectedCount = 3; - string output = this.Enlistment.Prefetch("--files *.md --hydrate"); - this.ExpectBlobCount(output, expectedCount); - output.ShouldContain("Hydrated files: " + expectedCount); - } - - [TestCase, Order(5)] - public void PrefetchByFilesWithHydrateWhoseObjectsAreAlreadyDownloaded() - { - int expectedCount = 2; - string output = this.Enlistment.Prefetch( - $"--files {Path.Combine("Scalar", "Scalar", "Program.cs")};{Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj")} --hydrate"); - this.ExpectBlobCount(output, expectedCount); - output.ShouldContain("Hydrated files: " + expectedCount); - output.ShouldContain("Downloaded: 0"); - } - - [TestCase, Order(6)] - public void PrefetchFolders() - { - this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("Scalar", "Scalar")}"), 17); - this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("Scalar", "Scalar")};{Path.Combine("Scalar", "Scalar.FunctionalTests")}"), 65); - } - - [TestCase, Order(7)] - public void PrefetchIsAllowedToDoNothing() - { - this.ExpectBlobCount(this.Enlistment.Prefetch("--files nonexistent.txt"), 0); - this.ExpectBlobCount(this.Enlistment.Prefetch("--folders nonexistent_folder"), 0); - } - - [TestCase, Order(8)] - public void PrefetchFolderListFromFile() - { - string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file"); - File.WriteAllLines( - tempFilePath, - new[] - { - "# A comment", - " ", - "scalar/", - "scalar/scalar", - "scalar/" - }); - - this.ExpectBlobCount(this.Enlistment.Prefetch("--folders-list \"" + tempFilePath + "\""), 279); - File.Delete(tempFilePath); - } - - [TestCase, Order(9)] - public void PrefetchAll() - { - this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494); - this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.DirectorySeparatorChar}"), 494); - } - - [TestCase, Order(10)] - public void NoopPrefetch() - { - this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494); - - this.Enlistment.Prefetch("--files *").ShouldContain("Nothing new to prefetch."); - } - - // TODO(#1219): Handle that lock files are not deleted on Mac, they are simply unlocked - [TestCase, Order(11)] - [Category(Categories.MacTODO.TestNeedsToLockFile)] - public void PrefetchCleansUpStalePrefetchLock() - { - this.Enlistment.Prefetch("--commits"); - this.PostFetchStepShouldComplete(); - string prefetchCommitsLockFile = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "pack", PrefetchCommitsAndTreesLock); - prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem); - this.fileSystem.WriteAllText(prefetchCommitsLockFile, this.Enlistment.EnlistmentRoot); - prefetchCommitsLockFile.ShouldBeAFile(this.fileSystem); - - this.fileSystem - .EnumerateDirectory(this.Enlistment.GetPackRoot(this.fileSystem)) - .Split() - .Where(file => string.Equals(Path.GetExtension(file), ".keep", StringComparison.OrdinalIgnoreCase)) - .Count() - .ShouldEqual(1, "Incorrect number of .keep files in pack directory"); - - this.Enlistment.Prefetch("--commits"); - this.PostFetchStepShouldComplete(); - prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem); - } - - [TestCase, Order(12)] - public void PrefetchFilesFromFileListFile() - { - string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file"); - try - { - File.WriteAllLines( - tempFilePath, - new[] - { - Path.Combine("Scalar", "Scalar", "Program.cs"), - Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj") - }); - - this.ExpectBlobCount(this.Enlistment.Prefetch($"--files-list \"{tempFilePath}\""), 2); - } - finally - { - File.Delete(tempFilePath); - } - } - - [TestCase, Order(13)] - public void PrefetchFilesFromFileListStdIn() - { - string input = string.Join( - Environment.NewLine, - new[] - { - Path.Combine("Scalar", "Scalar", "packages.config"), - Path.Combine("Scalar", "Scalar.FunctionalTests", "App.config") - }); - - this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-files-list", standardInput: input), 2); - } - - [TestCase, Order(14)] - public void PrefetchFolderListFromStdin() - { - string input = string.Join( - Environment.NewLine, - new[] - { - "# A comment", - " ", - "scalar/", - "scalar/scalar", - "scalar/" - }); - - this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-folders-list", standardInput: input), 279); - } - - public void PrefetchPathsWithLsTreeTypeInPath() - { - ProcessResult checkoutResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + LsTreeTypeInPathBranchName); - - this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 496); - } - - private void ExpectBlobCount(string output, int expectedCount) - { - output.ShouldContain("Matched blobs: " + expectedCount); - } - - private void PostFetchStepShouldComplete() - { - string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem); - string objectCacheLock = Path.Combine(objectDir, "git-maintenance-step.lock"); - - // Wait first, to hopefully ensure the background thread has - // started before we check for the lock file. - do - { - Thread.Sleep(500); - } - while (this.fileSystem.FileExists(objectCacheLock)); - - // A commit graph is not always generated, but if it is, then we want to ensure it is in a good state - if (this.fileSystem.FileExists(Path.Combine(objectDir, "info", "commit-graphs", "commit-graph-chain"))) - { - ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\""); - graphResult.ExitCode.ShouldEqual(0); - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [NonParallelizable] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class PrefetchVerbTests : TestsWithEnlistmentPerFixture + { + private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; + private const string LsTreeTypeInPathBranchName = "FunctionalTests/20181105_LsTreeTypeInPath"; + + private FileSystemRunner fileSystem; + + public PrefetchVerbTests() + { + this.fileSystem = new SystemIORunner(); + } + + [TestCase, Order(1)] + public void PrefetchAllMustBeExplicit() + { + this.Enlistment.Prefetch(string.Empty, failOnError: false).ShouldContain("Did you mean to fetch all blobs?"); + } + + [TestCase, Order(2)] + public void PrefetchSpecificFiles() + { + this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("Scalar", "Scalar", "Program.cs")}"), 1); + this.ExpectBlobCount(this.Enlistment.Prefetch($"--files {Path.Combine("Scalar", "Scalar", "Program.cs")};{Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj")}"), 2); + } + + [TestCase, Order(3)] + public void PrefetchByFileExtension() + { + this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs"), 199); + this.ExpectBlobCount(this.Enlistment.Prefetch("--files *.cs;*.csproj"), 208); + } + + [TestCase, Order(4)] + public void PrefetchByFileExtensionWithHydrate() + { + int expectedCount = 3; + string output = this.Enlistment.Prefetch("--files *.md --hydrate"); + this.ExpectBlobCount(output, expectedCount); + output.ShouldContain("Hydrated files: " + expectedCount); + } + + [TestCase, Order(5)] + public void PrefetchByFilesWithHydrateWhoseObjectsAreAlreadyDownloaded() + { + int expectedCount = 2; + string output = this.Enlistment.Prefetch( + $"--files {Path.Combine("Scalar", "Scalar", "Program.cs")};{Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj")} --hydrate"); + this.ExpectBlobCount(output, expectedCount); + output.ShouldContain("Hydrated files: " + expectedCount); + output.ShouldContain("Downloaded: 0"); + } + + [TestCase, Order(6)] + public void PrefetchFolders() + { + this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("Scalar", "Scalar")}"), 17); + this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.Combine("Scalar", "Scalar")};{Path.Combine("Scalar", "Scalar.FunctionalTests")}"), 65); + } + + [TestCase, Order(7)] + public void PrefetchIsAllowedToDoNothing() + { + this.ExpectBlobCount(this.Enlistment.Prefetch("--files nonexistent.txt"), 0); + this.ExpectBlobCount(this.Enlistment.Prefetch("--folders nonexistent_folder"), 0); + } + + [TestCase, Order(8)] + public void PrefetchFolderListFromFile() + { + string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file"); + File.WriteAllLines( + tempFilePath, + new[] + { + "# A comment", + " ", + "scalar/", + "scalar/scalar", + "scalar/" + }); + + this.ExpectBlobCount(this.Enlistment.Prefetch("--folders-list \"" + tempFilePath + "\""), 279); + File.Delete(tempFilePath); + } + + [TestCase, Order(9)] + public void PrefetchAll() + { + this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494); + this.ExpectBlobCount(this.Enlistment.Prefetch($"--folders {Path.DirectorySeparatorChar}"), 494); + } + + [TestCase, Order(10)] + public void NoopPrefetch() + { + this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 494); + + this.Enlistment.Prefetch("--files *").ShouldContain("Nothing new to prefetch."); + } + + // TODO(#1219): Handle that lock files are not deleted on Mac, they are simply unlocked + [TestCase, Order(11)] + [Category(Categories.MacTODO.TestNeedsToLockFile)] + public void PrefetchCleansUpStalePrefetchLock() + { + this.Enlistment.Prefetch("--commits"); + this.PostFetchStepShouldComplete(); + string prefetchCommitsLockFile = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "pack", PrefetchCommitsAndTreesLock); + prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.WriteAllText(prefetchCommitsLockFile, this.Enlistment.EnlistmentRoot); + prefetchCommitsLockFile.ShouldBeAFile(this.fileSystem); + + this.fileSystem + .EnumerateDirectory(this.Enlistment.GetPackRoot(this.fileSystem)) + .Split() + .Where(file => string.Equals(Path.GetExtension(file), ".keep", StringComparison.OrdinalIgnoreCase)) + .Count() + .ShouldEqual(1, "Incorrect number of .keep files in pack directory"); + + this.Enlistment.Prefetch("--commits"); + this.PostFetchStepShouldComplete(); + prefetchCommitsLockFile.ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase, Order(12)] + public void PrefetchFilesFromFileListFile() + { + string tempFilePath = Path.Combine(Path.GetTempPath(), "temp.file"); + try + { + File.WriteAllLines( + tempFilePath, + new[] + { + Path.Combine("Scalar", "Scalar", "Program.cs"), + Path.Combine("Scalar", "Scalar.FunctionalTests", "Scalar.FunctionalTests.csproj") + }); + + this.ExpectBlobCount(this.Enlistment.Prefetch($"--files-list \"{tempFilePath}\""), 2); + } + finally + { + File.Delete(tempFilePath); + } + } + + [TestCase, Order(13)] + public void PrefetchFilesFromFileListStdIn() + { + string input = string.Join( + Environment.NewLine, + new[] + { + Path.Combine("Scalar", "Scalar", "packages.config"), + Path.Combine("Scalar", "Scalar.FunctionalTests", "App.config") + }); + + this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-files-list", standardInput: input), 2); + } + + [TestCase, Order(14)] + public void PrefetchFolderListFromStdin() + { + string input = string.Join( + Environment.NewLine, + new[] + { + "# A comment", + " ", + "scalar/", + "scalar/scalar", + "scalar/" + }); + + this.ExpectBlobCount(this.Enlistment.Prefetch("--stdin-folders-list", standardInput: input), 279); + } + + public void PrefetchPathsWithLsTreeTypeInPath() + { + ProcessResult checkoutResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "checkout " + LsTreeTypeInPathBranchName); + + this.ExpectBlobCount(this.Enlistment.Prefetch("--files *"), 496); + } + + private void ExpectBlobCount(string output, int expectedCount) + { + output.ShouldContain("Matched blobs: " + expectedCount); + } + + private void PostFetchStepShouldComplete() + { + string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem); + string objectCacheLock = Path.Combine(objectDir, "git-maintenance-step.lock"); + + // Wait first, to hopefully ensure the background thread has + // started before we check for the lock file. + do + { + Thread.Sleep(500); + } + while (this.fileSystem.FileExists(objectCacheLock)); + + // A commit graph is not always generated, but if it is, then we want to ensure it is in a good state + if (this.fileSystem.FileExists(Path.Combine(objectDir, "info", "commit-graphs", "commit-graph-chain"))) + { + ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\""); + graphResult.ExitCode.ShouldEqual(0); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbWithoutSharedCacheTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbWithoutSharedCacheTests.cs index 9411ea027b..2f55ef3ee4 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbWithoutSharedCacheTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/PrefetchVerbWithoutSharedCacheTests.cs @@ -1,402 +1,402 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - // TODO(#1219): Before these tests can be enabled PostFetchJobShouldComplete needs - // to work on Mac (where post-fetch.lock is not removed from disk) - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.MacTODO.TestNeedsToLockFile)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class PrefetchVerbWithoutSharedCacheTests : TestsWithEnlistmentPerFixture - { - private const string PrefetchPackPrefix = "prefetch"; - private const string TempPackFolder = "tempPacks"; - - private FileSystemRunner fileSystem; - - // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting - // the cache - public PrefetchVerbWithoutSharedCacheTests() - : base(forcePerRepoObjectCache: true, skipPrefetchDuringClone: true) - { - this.fileSystem = new SystemIORunner(); - } - - private string PackRoot - { - get - { - return this.Enlistment.GetPackRoot(this.fileSystem); - } - } - - private string TempPackRoot - { - get - { - return Path.Combine(this.PackRoot, TempPackFolder); - } - } - - [TestCase, Order(1)] - public void PrefetchCommitsToEmptyCache() - { - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - // Verify prefetch pack(s) are in packs folder and have matching idx file - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - this.AllPrefetchPacksShouldHaveIdx(prefetchPacks); - - // Verify tempPacks is empty - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(2)] - public void PrefetchBuildsIdxWhenMissingFromPrefetchPack() - { - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack"); - - string idxPath = Path.ChangeExtension(prefetchPacks[0], ".idx"); - idxPath.ShouldBeAFile(this.fileSystem); - File.SetAttributes(idxPath, FileAttributes.Normal); - this.fileSystem.DeleteFile(idxPath); - idxPath.ShouldNotExistOnDisk(this.fileSystem); - - // Prefetch should rebuild the missing idx - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - idxPath.ShouldBeAFile(this.fileSystem); - - // All of the original prefetch packs should still be present - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(3)] - public void PrefetchCleansUpBadPrefetchPack() - { - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks); - - // Create a bad pack that is newer than the most recent pack - string badContents = "BADPACK"; - string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(badPackPath, badContents); - badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); - - // Prefetch should delete the bad pack - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - - // All of the original prefetch packs should still be present - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(4)] - public void PrefetchCleansUpOldPrefetchPack() - { - this.Enlistment.UnmountScalar(); - - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); - - // Create a bad pack that is older than the oldest pack - string badContents = "BADPACK"; - string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(badPackPath, badContents); - badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); - - // Prefetch should delete the bad pack and all packs after it - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - foreach (string packPath in prefetchPacks) - { - string idxPath = Path.ChangeExtension(packPath, ".idx"); - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - idxPath.ShouldNotExistOnDisk(this.fileSystem); - } - - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(5)] - public void PrefetchFailsWhenItCannotRemoveABadPrefetchPack() - { - this.Enlistment.UnmountScalar(); - - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks); - - // Create a bad pack that is newer than the most recent pack - string badContents = "BADPACK"; - string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(badPackPath, badContents); - badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); - - // Open a handle to the bad pack that will prevent prefetch from being able to delete it - using (FileStream stream = new FileStream(badPackPath, FileMode.Open, FileAccess.Read, FileShare.None)) - { - string output = this.Enlistment.Prefetch("--commits", failOnError: false); - output.ShouldContain($"Unable to delete {badPackPath}"); - } - - // After handle is closed prefetch should succeed - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(6)] - public void PrefetchFailsWhenItCannotRemoveAPrefetchPackNewerThanBadPrefetchPack() - { - this.Enlistment.UnmountScalar(); - - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); - - // Create a bad pack that is older than the oldest pack - string badContents = "BADPACK"; - string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(badPackPath, badContents); - badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); - - // Open a handle to a good pack that is newer than the bad pack, which will prevent prefetch from being able to delete it - using (FileStream stream = new FileStream(prefetchPacks[0], FileMode.Open, FileAccess.Read, FileShare.None)) - { - string output = this.Enlistment.Prefetch("--commits", failOnError: false); - output.ShouldContain($"Unable to delete {prefetchPacks[0]}"); - } - - // After handle is closed prefetch should succeed - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - // The bad pack and all packs newer than it should not be on disk - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(7)] - public void PrefetchFailsWhenItCannotRemoveAPrefetchIdxNewerThanBadPrefetchPack() - { - this.Enlistment.UnmountScalar(); - - string[] prefetchPacks = this.ReadPrefetchPackFileNames(); - long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); - - // Create a bad pack that is older than the oldest pack - string badContents = "BADPACK"; - string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(badPackPath, badContents); - badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); - - string newerIdxPath = Path.ChangeExtension(prefetchPacks[0], ".idx"); - newerIdxPath.ShouldBeAFile(this.fileSystem); - - // Open a handle to a good idx that is newer than the bad pack, which will prevent prefetch from being able to delete it - using (FileStream stream = new FileStream(newerIdxPath, FileMode.Open, FileAccess.Read, FileShare.None)) - { - string output = this.Enlistment.Prefetch("--commits", failOnError: false); - output.ShouldContain($"Unable to delete {newerIdxPath}"); - } - - // After handle is closed prefetch should succeed - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - // The bad pack and all packs newer than it should not be on disk - badPackPath.ShouldNotExistOnDisk(this.fileSystem); - newerIdxPath.ShouldNotExistOnDisk(this.fileSystem); - - string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); - newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); - this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); - this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); - } - - [TestCase, Order(8)] - public void PrefetchCleansUpStaleTempPrefetchPacks() - { - this.Enlistment.UnmountScalar(); - - // Create stale packs and idxs in the temp folder - string stalePackContents = "StalePack"; - string stalePackPath = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123456-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(stalePackPath, stalePackContents); - stalePackPath.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents); - - string staleIdxContents = "StaleIdx"; - string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx"); - this.fileSystem.WriteAllText(staleIdxPath, staleIdxContents); - staleIdxPath.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents); - - string stalePackPath2 = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123457-{Guid.NewGuid().ToString("N")}.pack"); - this.fileSystem.WriteAllText(stalePackPath2, stalePackContents); - stalePackPath2.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents); - - string stalePack2TempIdx = Path.ChangeExtension(stalePackPath2, ".tempidx"); - this.fileSystem.WriteAllText(stalePack2TempIdx, staleIdxContents); - stalePack2TempIdx.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents); - - // Create other unrelated file in the temp folder - string otherFileContents = "Test file, don't delete me!"; - string otherFilePath = Path.Combine(this.TempPackRoot, "ReadmeAndDontDeleteMe.txt"); - this.fileSystem.WriteAllText(otherFilePath, otherFileContents); - otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents); - - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - // Validate stale prefetch packs are cleaned up - Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.pack").ShouldBeEmpty("There should be no .pack files in the tempPack folder"); - Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.idx").ShouldBeEmpty("There should be no .idx files in the tempPack folder"); - Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.tempidx").ShouldBeEmpty("There should be no .tempidx files in the tempPack folder"); - - // Validate other files are not impacted - otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents); - } - - [TestCase, Order(9)] - public void PrefetchCleansUpOphanedLockFiles() - { - // the commit-graph write happens only when the prefetch downloads at least one pack - - string graphPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "info", "commit-graphs", "commit-graph-chain"); - string graphLockPath = graphPath + ".lock"; - - this.fileSystem.CreateEmptyFile(graphLockPath); - - // Unmount so we can delete the files. - this.Enlistment.UnmountScalar(); - - // Force deleting the prefetch packs to make the prefetch non-trivial. - this.fileSystem.DeleteDirectory(this.PackRoot); - this.fileSystem.CreateDirectory(this.PackRoot); - - // Re-mount so the post-fetch job runs - this.Enlistment.MountScalar(); - - this.Enlistment.Prefetch("--commits"); - this.PostFetchJobShouldComplete(); - - this.fileSystem.FileExists(graphLockPath).ShouldBeFalse(nameof(graphLockPath)); - this.fileSystem.FileExists(graphPath).ShouldBeTrue(nameof(graphPath)); - } - - private void PackShouldHaveIdxFile(string pathPath) - { - string idxPath = Path.ChangeExtension(pathPath, ".idx"); - idxPath.ShouldBeAFile(this.fileSystem).WithContents().Length.ShouldBeAtLeast(1, $"{idxPath} is unexepectedly empty"); - } - - private void AllPrefetchPacksShouldHaveIdx(string[] prefetchPacks) - { - prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack"); - - foreach (string prefetchPack in prefetchPacks) - { - this.PackShouldHaveIdxFile(prefetchPack); - } - } - - private string[] ReadPrefetchPackFileNames() - { - return Directory.GetFiles(this.PackRoot, $"{PrefetchPackPrefix}*.pack"); - } - - private long GetTimestamp(string preFetchPackName) - { - string filename = Path.GetFileName(preFetchPackName); - filename.StartsWith(PrefetchPackPrefix).ShouldBeTrue($"'{preFetchPackName}' does not start with '{PrefetchPackPrefix}'"); - - string[] parts = filename.Split('-'); - long parsed; - - parts.Length.ShouldBeAtLeast(1, $"'{preFetchPackName}' has less parts ({parts.Length}) than expected (1)"); - long.TryParse(parts[1], out parsed).ShouldBeTrue($"Failed to parse long from '{parts[1]}'"); - return parsed; - } - - private long GetMostRecentPackTimestamp(string[] prefetchPacks) - { - prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item"); - - long mostRecentPackTimestamp = -1; - foreach (string prefetchPack in prefetchPacks) - { - long timestamp = this.GetTimestamp(prefetchPack); - if (timestamp > mostRecentPackTimestamp) - { - mostRecentPackTimestamp = timestamp; - } - } - - mostRecentPackTimestamp.ShouldBeAtLeast(1, "Failed to find the most recent pack"); - return mostRecentPackTimestamp; - } - - private long GetOldestPackTimestamp(string[] prefetchPacks) - { - prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item"); - - long oldestPackTimestamp = long.MaxValue; - foreach (string prefetchPack in prefetchPacks) - { - long timestamp = this.GetTimestamp(prefetchPack); - if (timestamp < oldestPackTimestamp) - { - oldestPackTimestamp = timestamp; - } - } - - oldestPackTimestamp.ShouldBeAtMost(long.MaxValue - 1, "Failed to find the oldest pack"); - return oldestPackTimestamp; - } - - private void PostFetchJobShouldComplete() - { - string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem); - string postFetchLock = Path.Combine(objectDir, "git-maintenance-step.lock"); - - while (this.fileSystem.FileExists(postFetchLock)) - { - Thread.Sleep(500); - } - - ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\""); - graphResult.ExitCode.ShouldEqual(0); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + // TODO(#1219): Before these tests can be enabled PostFetchJobShouldComplete needs + // to work on Mac (where post-fetch.lock is not removed from disk) + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.MacTODO.TestNeedsToLockFile)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class PrefetchVerbWithoutSharedCacheTests : TestsWithEnlistmentPerFixture + { + private const string PrefetchPackPrefix = "prefetch"; + private const string TempPackFolder = "tempPacks"; + + private FileSystemRunner fileSystem; + + // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting + // the cache + public PrefetchVerbWithoutSharedCacheTests() + : base(forcePerRepoObjectCache: true, skipPrefetchDuringClone: true) + { + this.fileSystem = new SystemIORunner(); + } + + private string PackRoot + { + get + { + return this.Enlistment.GetPackRoot(this.fileSystem); + } + } + + private string TempPackRoot + { + get + { + return Path.Combine(this.PackRoot, TempPackFolder); + } + } + + [TestCase, Order(1)] + public void PrefetchCommitsToEmptyCache() + { + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + // Verify prefetch pack(s) are in packs folder and have matching idx file + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + this.AllPrefetchPacksShouldHaveIdx(prefetchPacks); + + // Verify tempPacks is empty + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(2)] + public void PrefetchBuildsIdxWhenMissingFromPrefetchPack() + { + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack"); + + string idxPath = Path.ChangeExtension(prefetchPacks[0], ".idx"); + idxPath.ShouldBeAFile(this.fileSystem); + File.SetAttributes(idxPath, FileAttributes.Normal); + this.fileSystem.DeleteFile(idxPath); + idxPath.ShouldNotExistOnDisk(this.fileSystem); + + // Prefetch should rebuild the missing idx + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + idxPath.ShouldBeAFile(this.fileSystem); + + // All of the original prefetch packs should still be present + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(3)] + public void PrefetchCleansUpBadPrefetchPack() + { + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks); + + // Create a bad pack that is newer than the most recent pack + string badContents = "BADPACK"; + string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(badPackPath, badContents); + badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); + + // Prefetch should delete the bad pack + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + + // All of the original prefetch packs should still be present + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(4)] + public void PrefetchCleansUpOldPrefetchPack() + { + this.Enlistment.UnmountScalar(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); + + // Create a bad pack that is older than the oldest pack + string badContents = "BADPACK"; + string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(badPackPath, badContents); + badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); + + // Prefetch should delete the bad pack and all packs after it + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + foreach (string packPath in prefetchPacks) + { + string idxPath = Path.ChangeExtension(packPath, ".idx"); + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + idxPath.ShouldNotExistOnDisk(this.fileSystem); + } + + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(5)] + public void PrefetchFailsWhenItCannotRemoveABadPrefetchPack() + { + this.Enlistment.UnmountScalar(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + long mostRecentPackTimestamp = this.GetMostRecentPackTimestamp(prefetchPacks); + + // Create a bad pack that is newer than the most recent pack + string badContents = "BADPACK"; + string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{mostRecentPackTimestamp + 1}-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(badPackPath, badContents); + badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); + + // Open a handle to the bad pack that will prevent prefetch from being able to delete it + using (FileStream stream = new FileStream(badPackPath, FileMode.Open, FileAccess.Read, FileShare.None)) + { + string output = this.Enlistment.Prefetch("--commits", failOnError: false); + output.ShouldContain($"Unable to delete {badPackPath}"); + } + + // After handle is closed prefetch should succeed + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + newPrefetchPacks.ShouldContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(6)] + public void PrefetchFailsWhenItCannotRemoveAPrefetchPackNewerThanBadPrefetchPack() + { + this.Enlistment.UnmountScalar(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); + + // Create a bad pack that is older than the oldest pack + string badContents = "BADPACK"; + string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(badPackPath, badContents); + badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); + + // Open a handle to a good pack that is newer than the bad pack, which will prevent prefetch from being able to delete it + using (FileStream stream = new FileStream(prefetchPacks[0], FileMode.Open, FileAccess.Read, FileShare.None)) + { + string output = this.Enlistment.Prefetch("--commits", failOnError: false); + output.ShouldContain($"Unable to delete {prefetchPacks[0]}"); + } + + // After handle is closed prefetch should succeed + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + // The bad pack and all packs newer than it should not be on disk + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(7)] + public void PrefetchFailsWhenItCannotRemoveAPrefetchIdxNewerThanBadPrefetchPack() + { + this.Enlistment.UnmountScalar(); + + string[] prefetchPacks = this.ReadPrefetchPackFileNames(); + long oldestPackTimestamp = this.GetOldestPackTimestamp(prefetchPacks); + + // Create a bad pack that is older than the oldest pack + string badContents = "BADPACK"; + string badPackPath = Path.Combine(this.PackRoot, $"{PrefetchPackPrefix}-{oldestPackTimestamp - 1}-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(badPackPath, badContents); + badPackPath.ShouldBeAFile(this.fileSystem).WithContents(badContents); + + string newerIdxPath = Path.ChangeExtension(prefetchPacks[0], ".idx"); + newerIdxPath.ShouldBeAFile(this.fileSystem); + + // Open a handle to a good idx that is newer than the bad pack, which will prevent prefetch from being able to delete it + using (FileStream stream = new FileStream(newerIdxPath, FileMode.Open, FileAccess.Read, FileShare.None)) + { + string output = this.Enlistment.Prefetch("--commits", failOnError: false); + output.ShouldContain($"Unable to delete {newerIdxPath}"); + } + + // After handle is closed prefetch should succeed + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + // The bad pack and all packs newer than it should not be on disk + badPackPath.ShouldNotExistOnDisk(this.fileSystem); + newerIdxPath.ShouldNotExistOnDisk(this.fileSystem); + + string[] newPrefetchPacks = this.ReadPrefetchPackFileNames(); + newPrefetchPacks.ShouldNotContain(prefetchPacks, (item, expectedValue) => { return string.Equals(item, expectedValue); }); + this.AllPrefetchPacksShouldHaveIdx(newPrefetchPacks); + this.TempPackRoot.ShouldBeADirectory(this.fileSystem).WithNoItems(); + } + + [TestCase, Order(8)] + public void PrefetchCleansUpStaleTempPrefetchPacks() + { + this.Enlistment.UnmountScalar(); + + // Create stale packs and idxs in the temp folder + string stalePackContents = "StalePack"; + string stalePackPath = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123456-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(stalePackPath, stalePackContents); + stalePackPath.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents); + + string staleIdxContents = "StaleIdx"; + string staleIdxPath = Path.ChangeExtension(stalePackPath, ".idx"); + this.fileSystem.WriteAllText(staleIdxPath, staleIdxContents); + staleIdxPath.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents); + + string stalePackPath2 = Path.Combine(this.TempPackRoot, $"{PrefetchPackPrefix}-123457-{Guid.NewGuid().ToString("N")}.pack"); + this.fileSystem.WriteAllText(stalePackPath2, stalePackContents); + stalePackPath2.ShouldBeAFile(this.fileSystem).WithContents(stalePackContents); + + string stalePack2TempIdx = Path.ChangeExtension(stalePackPath2, ".tempidx"); + this.fileSystem.WriteAllText(stalePack2TempIdx, staleIdxContents); + stalePack2TempIdx.ShouldBeAFile(this.fileSystem).WithContents(staleIdxContents); + + // Create other unrelated file in the temp folder + string otherFileContents = "Test file, don't delete me!"; + string otherFilePath = Path.Combine(this.TempPackRoot, "ReadmeAndDontDeleteMe.txt"); + this.fileSystem.WriteAllText(otherFilePath, otherFileContents); + otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents); + + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + // Validate stale prefetch packs are cleaned up + Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.pack").ShouldBeEmpty("There should be no .pack files in the tempPack folder"); + Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.idx").ShouldBeEmpty("There should be no .idx files in the tempPack folder"); + Directory.GetFiles(this.TempPackRoot, $"{PrefetchPackPrefix}*.tempidx").ShouldBeEmpty("There should be no .tempidx files in the tempPack folder"); + + // Validate other files are not impacted + otherFilePath.ShouldBeAFile(this.fileSystem).WithContents(otherFileContents); + } + + [TestCase, Order(9)] + public void PrefetchCleansUpOphanedLockFiles() + { + // the commit-graph write happens only when the prefetch downloads at least one pack + + string graphPath = Path.Combine(this.Enlistment.GetObjectRoot(this.fileSystem), "info", "commit-graphs", "commit-graph-chain"); + string graphLockPath = graphPath + ".lock"; + + this.fileSystem.CreateEmptyFile(graphLockPath); + + // Unmount so we can delete the files. + this.Enlistment.UnmountScalar(); + + // Force deleting the prefetch packs to make the prefetch non-trivial. + this.fileSystem.DeleteDirectory(this.PackRoot); + this.fileSystem.CreateDirectory(this.PackRoot); + + // Re-mount so the post-fetch job runs + this.Enlistment.MountScalar(); + + this.Enlistment.Prefetch("--commits"); + this.PostFetchJobShouldComplete(); + + this.fileSystem.FileExists(graphLockPath).ShouldBeFalse(nameof(graphLockPath)); + this.fileSystem.FileExists(graphPath).ShouldBeTrue(nameof(graphPath)); + } + + private void PackShouldHaveIdxFile(string pathPath) + { + string idxPath = Path.ChangeExtension(pathPath, ".idx"); + idxPath.ShouldBeAFile(this.fileSystem).WithContents().Length.ShouldBeAtLeast(1, $"{idxPath} is unexepectedly empty"); + } + + private void AllPrefetchPacksShouldHaveIdx(string[] prefetchPacks) + { + prefetchPacks.Length.ShouldBeAtLeast(1, "There should be at least one prefetch pack"); + + foreach (string prefetchPack in prefetchPacks) + { + this.PackShouldHaveIdxFile(prefetchPack); + } + } + + private string[] ReadPrefetchPackFileNames() + { + return Directory.GetFiles(this.PackRoot, $"{PrefetchPackPrefix}*.pack"); + } + + private long GetTimestamp(string preFetchPackName) + { + string filename = Path.GetFileName(preFetchPackName); + filename.StartsWith(PrefetchPackPrefix).ShouldBeTrue($"'{preFetchPackName}' does not start with '{PrefetchPackPrefix}'"); + + string[] parts = filename.Split('-'); + long parsed; + + parts.Length.ShouldBeAtLeast(1, $"'{preFetchPackName}' has less parts ({parts.Length}) than expected (1)"); + long.TryParse(parts[1], out parsed).ShouldBeTrue($"Failed to parse long from '{parts[1]}'"); + return parsed; + } + + private long GetMostRecentPackTimestamp(string[] prefetchPacks) + { + prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item"); + + long mostRecentPackTimestamp = -1; + foreach (string prefetchPack in prefetchPacks) + { + long timestamp = this.GetTimestamp(prefetchPack); + if (timestamp > mostRecentPackTimestamp) + { + mostRecentPackTimestamp = timestamp; + } + } + + mostRecentPackTimestamp.ShouldBeAtLeast(1, "Failed to find the most recent pack"); + return mostRecentPackTimestamp; + } + + private long GetOldestPackTimestamp(string[] prefetchPacks) + { + prefetchPacks.Length.ShouldBeAtLeast(1, "prefetchPacks should have at least one item"); + + long oldestPackTimestamp = long.MaxValue; + foreach (string prefetchPack in prefetchPacks) + { + long timestamp = this.GetTimestamp(prefetchPack); + if (timestamp < oldestPackTimestamp) + { + oldestPackTimestamp = timestamp; + } + } + + oldestPackTimestamp.ShouldBeAtMost(long.MaxValue - 1, "Failed to find the oldest pack"); + return oldestPackTimestamp; + } + + private void PostFetchJobShouldComplete() + { + string objectDir = this.Enlistment.GetObjectRoot(this.fileSystem); + string postFetchLock = Path.Combine(objectDir, "git-maintenance-step.lock"); + + while (this.fileSystem.FileExists(postFetchLock)) + { + Thread.Sleep(500); + } + + ProcessResult graphResult = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "commit-graph verify --shallow --object-dir=\"" + objectDir + "\""); + graphResult.ExitCode.ShouldEqual(0); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/ScalarUpgradeReminderTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/ScalarUpgradeReminderTests.cs index 1dedb0b5fb..db62b54e4b 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/ScalarUpgradeReminderTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/ScalarUpgradeReminderTests.cs @@ -1,258 +1,258 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [NonParallelizable] - [Category(Categories.ExtraCoverage)] - [Category(Categories.WindowsOnly)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class UpgradeReminderTests : TestsWithEnlistmentPerFixture - { - private const string HighestAvailableVersionFileName = "HighestAvailableVersion"; - private const string UpgradeRingKey = "upgrade.ring"; - private const string NugetFeedURLKey = "upgrade.feedurl"; - private const string NugetFeedPackageNameKey = "upgrade.feedpackagename"; - private const string AlwaysUpToDateRing = "None"; - - private string upgradeDownloadsDirectory; - private FileSystemRunner fileSystem; - - public UpgradeReminderTests() - { - this.fileSystem = new SystemIORunner(); - this.upgradeDownloadsDirectory = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), - "Scalar", - "Scalar.Upgrade", - "Downloads"); - } - - [TestCase] - public void NoReminderWhenUpgradeNotAvailable() - { - this.EmptyDownloadDirectory(); - - for (int count = 0; count < 50; count++) - { - ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status"); - - string.IsNullOrEmpty(result.Errors).ShouldBeTrue(); - } - } - - [TestCase] - public void RemindWhenUpgradeAvailable() - { - this.CreateUpgradeAvailableMarkerFile(); - this.ReminderMessagingEnabled().ShouldBeTrue(); - this.EmptyDownloadDirectory(); - } - - [TestCase] - public void NoReminderForLeftOverDownloads() - { - this.VerifyServiceRestartStopsReminder(); - - // This test should not use Nuget upgrader because it will usually find an upgrade - // to download. The "None" ring config doesn't stop the Nuget upgrader from checking - // its feed for updates, and the Scalar binaries installed during functional test - // runs typically have a 0.X version number (meaning there will always be a newer - // version of Scalar available to download from the feed). - this.ReadNugetConfig(out string feedUrl, out string feedName); - this.DeleteNugetConfig(); - this.VerifyUpgradeVerbStopsReminder(); - this.WriteNugetConfig(feedUrl, feedName); - } - - [TestCase] - public void UpgradeTimerScheduledOnServiceStart() - { - this.RestartService(); - - bool timerScheduled = false; - for (int trialCount = 0; trialCount < 15; trialCount++) - { - Thread.Sleep(TimeSpan.FromSeconds(1)); - if (this.ServiceLogContainsUpgradeMessaging()) - { - timerScheduled = true; - break; - } - } - - timerScheduled.ShouldBeTrue(); - } - - private void ReadNugetConfig(out string feedUrl, out string feedName) - { - ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); - - // failOnError is set to false because scalar config read can exit with - // GenericError when the key-value is not available in config file. That - // is normal. - feedUrl = scalar.ReadConfig(NugetFeedURLKey, failOnError: false); - feedName = scalar.ReadConfig(NugetFeedPackageNameKey, failOnError: false); - } - - private void DeleteNugetConfig() - { - ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); - scalar.DeleteConfig(NugetFeedURLKey); - scalar.DeleteConfig(NugetFeedPackageNameKey); - } - - private void WriteNugetConfig(string feedUrl, string feedName) - { - ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); - if (!string.IsNullOrEmpty(feedUrl)) - { - scalar.WriteConfig(NugetFeedURLKey, feedUrl); - } - - if (!string.IsNullOrEmpty(feedName)) - { - scalar.WriteConfig(NugetFeedPackageNameKey, feedName); - } - } - - private bool ServiceLogContainsUpgradeMessaging() - { - // This test checks for the upgrade timer start message in the Service log - // file. Scalar.Service should schedule the timer as it starts. - string expectedTimerMessage = "Checking for product upgrades. (Start)"; - string serviceLogFolder = Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "Scalar", - ScalarServiceProcess.TestServiceName, - "Logs"); - DirectoryInfo logsDirectory = new DirectoryInfo(serviceLogFolder); - FileInfo logFile = logsDirectory.GetFiles() - .OrderByDescending(f => f.LastWriteTime) - .FirstOrDefault(); - - if (logFile != null) - { - using (StreamReader fileStream = new StreamReader(File.Open(logFile.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - string nextLine = null; - while ((nextLine = fileStream.ReadLine()) != null) - { - if (nextLine.Contains(expectedTimerMessage)) - { - return true; - } - } - } - } - - return false; - } - - private void EmptyDownloadDirectory() - { - if (Directory.Exists(this.upgradeDownloadsDirectory)) - { - Directory.Delete(this.upgradeDownloadsDirectory, recursive: true); - } - - Directory.CreateDirectory(this.upgradeDownloadsDirectory); - Directory.Exists(this.upgradeDownloadsDirectory).ShouldBeTrue(); - Directory.EnumerateFiles(this.upgradeDownloadsDirectory).Any().ShouldBeFalse(); - } - - private void CreateUpgradeAvailableMarkerFile() - { - string scalarUpgradeAvailableFilePath = Path.Combine( - Path.GetDirectoryName(this.upgradeDownloadsDirectory), - HighestAvailableVersionFileName); - - this.EmptyDownloadDirectory(); - - this.fileSystem.CreateEmptyFile(scalarUpgradeAvailableFilePath); - this.fileSystem.FileExists(scalarUpgradeAvailableFilePath).ShouldBeTrue(); - } - - private void SetUpgradeRing(string value) - { - this.RunScalar($"config {UpgradeRingKey} {value}"); - } - - private string RunUpgradeCommand() - { - return this.RunScalar("upgrade"); - } - - private string RunScalar(string argument) - { - ProcessResult result = ProcessHelper.Run(ScalarTestConfig.PathToScalar, argument); - result.ExitCode.ShouldEqual(0, result.Errors); - - return result.Output; - } - - private void RestartService() - { - ScalarServiceProcess.StopService(); - ScalarServiceProcess.StartService(); - } - - private bool ReminderMessagingEnabled() - { - Dictionary environmentVariables = new Dictionary(); - environmentVariables["Scalar_UPGRADE_DETERMINISTIC"] = "true"; - ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - environmentVariables, - removeWaitingMessages: true, - removeUpgradeMessages: false); - - if (!string.IsNullOrEmpty(result.Errors) && - result.Errors.Contains("A new version of Scalar is available.")) - { - return true; - } - - return false; - } - - private void VerifyServiceRestartStopsReminder() - { - this.CreateUpgradeAvailableMarkerFile(); - this.ReminderMessagingEnabled().ShouldBeTrue("Upgrade marker file did not trigger reminder messaging"); - this.SetUpgradeRing(AlwaysUpToDateRing); - this.RestartService(); - - // Wait for sometime so service can detect product is up-to-date and delete left over downloads - TimeSpan timeToWait = TimeSpan.FromMinutes(1); - bool reminderMessagingEnabled = true; - while ((reminderMessagingEnabled = this.ReminderMessagingEnabled()) && timeToWait > TimeSpan.Zero) - { - Thread.Sleep(TimeSpan.FromSeconds(5)); - timeToWait = timeToWait.Subtract(TimeSpan.FromSeconds(5)); - } - - reminderMessagingEnabled.ShouldBeFalse("Service restart did not stop Upgrade reminder messaging"); - } - - private void VerifyUpgradeVerbStopsReminder() - { - this.SetUpgradeRing(AlwaysUpToDateRing); - this.CreateUpgradeAvailableMarkerFile(); - this.ReminderMessagingEnabled().ShouldBeTrue("Marker file did not trigger Upgrade reminder messaging"); - this.RunUpgradeCommand(); - this.ReminderMessagingEnabled().ShouldBeFalse("Upgrade verb did not stop Upgrade reminder messaging"); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [NonParallelizable] + [Category(Categories.ExtraCoverage)] + [Category(Categories.WindowsOnly)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class UpgradeReminderTests : TestsWithEnlistmentPerFixture + { + private const string HighestAvailableVersionFileName = "HighestAvailableVersion"; + private const string UpgradeRingKey = "upgrade.ring"; + private const string NugetFeedURLKey = "upgrade.feedurl"; + private const string NugetFeedPackageNameKey = "upgrade.feedpackagename"; + private const string AlwaysUpToDateRing = "None"; + + private string upgradeDownloadsDirectory; + private FileSystemRunner fileSystem; + + public UpgradeReminderTests() + { + this.fileSystem = new SystemIORunner(); + this.upgradeDownloadsDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), + "Scalar", + "Scalar.Upgrade", + "Downloads"); + } + + [TestCase] + public void NoReminderWhenUpgradeNotAvailable() + { + this.EmptyDownloadDirectory(); + + for (int count = 0; count < 50; count++) + { + ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status"); + + string.IsNullOrEmpty(result.Errors).ShouldBeTrue(); + } + } + + [TestCase] + public void RemindWhenUpgradeAvailable() + { + this.CreateUpgradeAvailableMarkerFile(); + this.ReminderMessagingEnabled().ShouldBeTrue(); + this.EmptyDownloadDirectory(); + } + + [TestCase] + public void NoReminderForLeftOverDownloads() + { + this.VerifyServiceRestartStopsReminder(); + + // This test should not use Nuget upgrader because it will usually find an upgrade + // to download. The "None" ring config doesn't stop the Nuget upgrader from checking + // its feed for updates, and the Scalar binaries installed during functional test + // runs typically have a 0.X version number (meaning there will always be a newer + // version of Scalar available to download from the feed). + this.ReadNugetConfig(out string feedUrl, out string feedName); + this.DeleteNugetConfig(); + this.VerifyUpgradeVerbStopsReminder(); + this.WriteNugetConfig(feedUrl, feedName); + } + + [TestCase] + public void UpgradeTimerScheduledOnServiceStart() + { + this.RestartService(); + + bool timerScheduled = false; + for (int trialCount = 0; trialCount < 15; trialCount++) + { + Thread.Sleep(TimeSpan.FromSeconds(1)); + if (this.ServiceLogContainsUpgradeMessaging()) + { + timerScheduled = true; + break; + } + } + + timerScheduled.ShouldBeTrue(); + } + + private void ReadNugetConfig(out string feedUrl, out string feedName) + { + ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); + + // failOnError is set to false because scalar config read can exit with + // GenericError when the key-value is not available in config file. That + // is normal. + feedUrl = scalar.ReadConfig(NugetFeedURLKey, failOnError: false); + feedName = scalar.ReadConfig(NugetFeedPackageNameKey, failOnError: false); + } + + private void DeleteNugetConfig() + { + ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); + scalar.DeleteConfig(NugetFeedURLKey); + scalar.DeleteConfig(NugetFeedPackageNameKey); + } + + private void WriteNugetConfig(string feedUrl, string feedName) + { + ScalarProcess scalar = new ScalarProcess(ScalarTestConfig.PathToScalar, enlistmentRoot: null, localCacheRoot: null); + if (!string.IsNullOrEmpty(feedUrl)) + { + scalar.WriteConfig(NugetFeedURLKey, feedUrl); + } + + if (!string.IsNullOrEmpty(feedName)) + { + scalar.WriteConfig(NugetFeedPackageNameKey, feedName); + } + } + + private bool ServiceLogContainsUpgradeMessaging() + { + // This test checks for the upgrade timer start message in the Service log + // file. Scalar.Service should schedule the timer as it starts. + string expectedTimerMessage = "Checking for product upgrades. (Start)"; + string serviceLogFolder = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Scalar", + ScalarServiceProcess.TestServiceName, + "Logs"); + DirectoryInfo logsDirectory = new DirectoryInfo(serviceLogFolder); + FileInfo logFile = logsDirectory.GetFiles() + .OrderByDescending(f => f.LastWriteTime) + .FirstOrDefault(); + + if (logFile != null) + { + using (StreamReader fileStream = new StreamReader(File.Open(logFile.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) + { + string nextLine = null; + while ((nextLine = fileStream.ReadLine()) != null) + { + if (nextLine.Contains(expectedTimerMessage)) + { + return true; + } + } + } + } + + return false; + } + + private void EmptyDownloadDirectory() + { + if (Directory.Exists(this.upgradeDownloadsDirectory)) + { + Directory.Delete(this.upgradeDownloadsDirectory, recursive: true); + } + + Directory.CreateDirectory(this.upgradeDownloadsDirectory); + Directory.Exists(this.upgradeDownloadsDirectory).ShouldBeTrue(); + Directory.EnumerateFiles(this.upgradeDownloadsDirectory).Any().ShouldBeFalse(); + } + + private void CreateUpgradeAvailableMarkerFile() + { + string scalarUpgradeAvailableFilePath = Path.Combine( + Path.GetDirectoryName(this.upgradeDownloadsDirectory), + HighestAvailableVersionFileName); + + this.EmptyDownloadDirectory(); + + this.fileSystem.CreateEmptyFile(scalarUpgradeAvailableFilePath); + this.fileSystem.FileExists(scalarUpgradeAvailableFilePath).ShouldBeTrue(); + } + + private void SetUpgradeRing(string value) + { + this.RunScalar($"config {UpgradeRingKey} {value}"); + } + + private string RunUpgradeCommand() + { + return this.RunScalar("upgrade"); + } + + private string RunScalar(string argument) + { + ProcessResult result = ProcessHelper.Run(ScalarTestConfig.PathToScalar, argument); + result.ExitCode.ShouldEqual(0, result.Errors); + + return result.Output; + } + + private void RestartService() + { + ScalarServiceProcess.StopService(); + ScalarServiceProcess.StartService(); + } + + private bool ReminderMessagingEnabled() + { + Dictionary environmentVariables = new Dictionary(); + environmentVariables["Scalar_UPGRADE_DETERMINISTIC"] = "true"; + ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + environmentVariables, + removeWaitingMessages: true, + removeUpgradeMessages: false); + + if (!string.IsNullOrEmpty(result.Errors) && + result.Errors.Contains("A new version of Scalar is available.")) + { + return true; + } + + return false; + } + + private void VerifyServiceRestartStopsReminder() + { + this.CreateUpgradeAvailableMarkerFile(); + this.ReminderMessagingEnabled().ShouldBeTrue("Upgrade marker file did not trigger reminder messaging"); + this.SetUpgradeRing(AlwaysUpToDateRing); + this.RestartService(); + + // Wait for sometime so service can detect product is up-to-date and delete left over downloads + TimeSpan timeToWait = TimeSpan.FromMinutes(1); + bool reminderMessagingEnabled = true; + while ((reminderMessagingEnabled = this.ReminderMessagingEnabled()) && timeToWait > TimeSpan.Zero) + { + Thread.Sleep(TimeSpan.FromSeconds(5)); + timeToWait = timeToWait.Subtract(TimeSpan.FromSeconds(5)); + } + + reminderMessagingEnabled.ShouldBeFalse("Service restart did not stop Upgrade reminder messaging"); + } + + private void VerifyUpgradeVerbStopsReminder() + { + this.SetUpgradeRing(AlwaysUpToDateRing); + this.CreateUpgradeAvailableMarkerFile(); + this.ReminderMessagingEnabled().ShouldBeTrue("Marker file did not trigger Upgrade reminder messaging"); + this.RunUpgradeCommand(); + this.ReminderMessagingEnabled().ShouldBeFalse("Upgrade verb did not stop Upgrade reminder messaging"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SparseTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SparseTests.cs index c6ba9e1d66..0dcc996f98 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SparseTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SparseTests.cs @@ -1,378 +1,378 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class SparseTests : TestsWithEnlistmentPerFixture - { - private FileSystemRunner fileSystem = new SystemIORunner(); - private ScalarProcess scalarProcess; - private string mainSparseFolder = Path.Combine("Scalar", "Scalar"); - private string[] allRootDirectories; - private string[] directoriesInMainFolder; - - [OneTimeSetUp] - public void Setup() - { - this.scalarProcess = new ScalarProcess(this.Enlistment); - this.allRootDirectories = Directory.GetDirectories(this.Enlistment.RepoRoot); - this.directoriesInMainFolder = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); - } - - [TearDown] - public void TearDown() - { - GitProcess.Invoke(this.Enlistment.RepoRoot, "clean -xdf"); - GitProcess.Invoke(this.Enlistment.RepoRoot, "reset --hard"); - - foreach (string sparseFolder in this.scalarProcess.GetSparseFolders()) - { - this.scalarProcess.RemoveSparseFolders(sparseFolder); - } - - // Remove all sparse folders should make all folders appear again - string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot); - directories.ShouldMatchInOrder(this.allRootDirectories); - this.ValidateFoldersInSparseList(new string[0]); - } - - [TestCase, Order(1)] - public void BasicTestsAddingSparseFolder() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot); - directories.Length.ShouldEqual(2); - directories[0].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, ".git")); - directories[1].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, "Scalar")); - - string folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder); - folder.ShouldBeADirectory(this.fileSystem); - folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine"); - folder.ShouldBeADirectory(this.fileSystem); - - string file = this.Enlistment.GetVirtualPathTo("Readme.md"); - file.ShouldBeAFile(this.fileSystem); - - folder = this.Enlistment.GetVirtualPathTo("Scripts"); - folder.ShouldNotExistOnDisk(this.fileSystem); - folder = this.Enlistment.GetVirtualPathTo("Scalar", "Scalar.Mount"); - folder.ShouldNotExistOnDisk(this.fileSystem); - - string secondPath = Path.Combine("Scalar", "Scalar.Common", "Physical"); - this.scalarProcess.AddSparseFolders(secondPath); - folder = this.Enlistment.GetVirtualPathTo(secondPath); - folder.ShouldBeADirectory(this.fileSystem); - file = this.Enlistment.GetVirtualPathTo("Scalar", "Scalar.Common", "Enlistment.cs"); - file.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(2)] - public void AddAndRemoveVariousPathsTests() - { - // Paths to validate [0] = path to pass to sparse [1] = expected path saved - string[][] paths = new[] - { - // AltDirectorySeparatorChar should get converted to DirectorySeparatorChar - new[] { string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Scalar"), this.mainSparseFolder }, - - // AltDirectorySeparatorChar should get trimmed - new[] { $"{Path.AltDirectorySeparatorChar}{string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Test")}{Path.AltDirectorySeparatorChar}", Path.Combine("Scalar", "Test") }, - - // DirectorySeparatorChar should get trimmed - new[] { $"{Path.DirectorySeparatorChar}{Path.Combine("Scalar", "More")}{Path.DirectorySeparatorChar}", Path.Combine("Scalar", "More") }, - - // spaces should get trimmed - new[] { $" {string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Other")} ", Path.Combine("Scalar", "Other") }, - }; - - foreach (string[] pathToValidate in paths) - { - this.ValidatePathAddsAndRemoves(pathToValidate[0], pathToValidate[1]); - } - } - - [TestCase, Order(3)] - public void AddingParentDirectoryShouldMakeItRecursive() - { - string childPath = Path.Combine(this.mainSparseFolder, "CommandLine"); - this.scalarProcess.AddSparseFolders(childPath); - string[] directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); - directories.Length.ShouldEqual(1); - directories[0].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, childPath)); - this.ValidateFoldersInSparseList(childPath); - - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); - directories.Length.ShouldBeAtLeast(2); - directories.ShouldMatchInOrder(this.directoriesInMainFolder); - this.ValidateFoldersInSparseList(childPath, this.mainSparseFolder); - } - - [TestCase, Order(4)] - public void AddingSiblingFolderShouldNotMakeParentRecursive() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - // Add and remove sibling folder to main folder - string siblingPath = Path.Combine("Scalar", "FastFetch"); - this.scalarProcess.AddSparseFolders(siblingPath); - string folder = this.Enlistment.GetVirtualPathTo(siblingPath); - folder.ShouldBeADirectory(this.fileSystem); - this.ValidateFoldersInSparseList(this.mainSparseFolder, siblingPath); - - this.scalarProcess.RemoveSparseFolders(siblingPath); - folder.ShouldNotExistOnDisk(this.fileSystem); - folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder); - folder.ShouldBeADirectory(this.fileSystem); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - } - - [TestCase, Order(5)] - public void AddingSubfolderShouldKeepParentRecursive() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - // Add subfolder of main folder and make sure it stays recursive - string subFolder = Path.Combine(this.mainSparseFolder, "Properties"); - this.scalarProcess.AddSparseFolders(subFolder); - string folder = this.Enlistment.GetVirtualPathTo(subFolder); - folder.ShouldBeADirectory(this.fileSystem); - this.ValidateFoldersInSparseList(this.mainSparseFolder, subFolder); - - folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine"); - folder.ShouldBeADirectory(this.fileSystem); - } - - [TestCase, Order(6)] - [Category(Categories.WindowsOnly)] - public void CreatingFolderShouldAddToSparseListAndStartProjecting() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Common"); - newFolderPath.ShouldNotExistOnDisk(this.fileSystem); - Directory.CreateDirectory(newFolderPath); - newFolderPath.ShouldBeADirectory(this.fileSystem); - string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); - fileSystemEntries.Length.ShouldEqual(32); - this.ValidateFoldersInSparseList(this.mainSparseFolder, Path.Combine("Scalar", "Scalar.Common")); - - string projectedFolder = Path.Combine(newFolderPath, "Git"); - projectedFolder.ShouldBeADirectory(this.fileSystem); - fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); - fileSystemEntries.Length.ShouldEqual(13); - - string projectedFile = Path.Combine(newFolderPath, "ReturnCode.cs"); - projectedFile.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(7)] - [Category(Categories.MacOnly)] - public void CreateFolderThenFileShouldAddToSparseListAndStartProjecting() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Common"); - newFolderPath.ShouldNotExistOnDisk(this.fileSystem); - Directory.CreateDirectory(newFolderPath); - string newFilePath = Path.Combine(newFolderPath, "test.txt"); - File.WriteAllText(newFilePath, "New file content"); - newFolderPath.ShouldBeADirectory(this.fileSystem); - newFilePath.ShouldBeAFile(this.fileSystem); - string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); - fileSystemEntries.Length.ShouldEqual(33); - this.ValidateFoldersInSparseList(this.mainSparseFolder, Path.Combine("Scalar", "Scalar.Common")); - - string projectedFolder = Path.Combine(newFolderPath, "Git"); - projectedFolder.ShouldBeADirectory(this.fileSystem); - fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); - fileSystemEntries.Length.ShouldEqual(13); - - string projectedFile = Path.Combine(newFolderPath, "ReturnCode.cs"); - projectedFile.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(7)] - public void ReadFileThenChangingSparseFoldersShouldRemoveFileAndFolder() - { - string fileToRead = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); - this.fileSystem.ReadAllText(fileToRead); - - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); - folderPath.ShouldNotExistOnDisk(this.fileSystem); - fileToRead.ShouldNotExistOnDisk(this.fileSystem); - } - - [TestCase, Order(8)] - public void CreateNewFileWillPreventRemoveSparseFolder() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder, "Scripts"); - this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts"); - - string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "newfile.txt"); - this.fileSystem.WriteAllText(fileToCreate, "New Contents"); - - string output = this.scalarProcess.RemoveSparseFolders(shouldSucceed: false, folders: "Scripts"); - output.ShouldContain("sparse was aborted"); - this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts"); - - string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); - folderPath.ShouldBeADirectory(this.fileSystem); - string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath); - fileSystemEntries.Length.ShouldEqual(6); - fileToCreate.ShouldBeAFile(this.fileSystem); - - this.fileSystem.DeleteFile(fileToCreate); - } - - [TestCase, Order(9)] - public void ModifiedFileShouldNotAllowSparseFolderChange() - { - string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); - this.fileSystem.WriteAllText(modifiedPath, "New Contents"); - - string output = this.scalarProcess.AddSparseFolders(shouldSucceed: false, folders: this.mainSparseFolder); - output.ShouldContain("sparse was aborted"); - this.ValidateFoldersInSparseList(new string[0]); - } - - [TestCase, Order(10)] - public void ModifiedFileAndCommitThenChangingSparseFoldersShouldKeepFileAndFolder() - { - string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); - this.fileSystem.WriteAllText(modifiedPath, "New Contents"); - GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); - GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); - - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); - folderPath.ShouldBeADirectory(this.fileSystem); - modifiedPath.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(11)] - public void DeleteFileAndCommitThenChangingSparseFoldersShouldKeepFolderAndFile() - { - string deletePath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Tests", "packages.config"); - this.fileSystem.DeleteFile(deletePath); - GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); - GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); - - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - // File and folder should no longer be on disk because the file was deleted and the folder deleted becase it was empty - string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Tests"); - folderPath.ShouldNotExistOnDisk(this.fileSystem); - deletePath.ShouldNotExistOnDisk(this.fileSystem); - - // Folder and file should be on disk even though they are outside the sparse scope because the file is in the modified paths - GitProcess.Invoke(this.Enlistment.RepoRoot, "checkout HEAD~1"); - folderPath.ShouldBeADirectory(this.fileSystem); - deletePath.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(12)] - public void CreateNewFileAndCommitThenRemoveSparseFolderShouldKeepFileAndFolder() - { - string folderToCreateFileIn = Path.Combine("Scalar", "Scalar.Hooks"); - this.scalarProcess.AddSparseFolders(this.mainSparseFolder, folderToCreateFileIn); - this.ValidateFoldersInSparseList(this.mainSparseFolder, folderToCreateFileIn); - - string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt"); - this.fileSystem.WriteAllText(fileToCreate, "New Contents"); - GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); - GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); - - this.scalarProcess.RemoveSparseFolders(folderToCreateFileIn); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn); - folderPath.ShouldBeADirectory(this.fileSystem); - string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath); - fileSystemEntries.Length.ShouldEqual(1); - fileToCreate.ShouldBeAFile(this.fileSystem); - } - - [TestCase, Order(13)] - [Category(Categories.MacOnly)] - public void CreateFolderAndFileThatAreExcluded() - { - this.scalarProcess.AddSparseFolders(this.mainSparseFolder); - this.ValidateFoldersInSparseList(this.mainSparseFolder); - - // Create a file that should already be in the projection but excluded - string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Mount"); - newFolderPath.ShouldNotExistOnDisk(this.fileSystem); - Directory.CreateDirectory(newFolderPath); - string newFilePath = Path.Combine(newFolderPath, "Program.cs"); - File.WriteAllText(newFilePath, "New file content"); - newFolderPath.ShouldBeADirectory(this.fileSystem); - newFilePath.ShouldBeAFile(this.fileSystem); - string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); - fileSystemEntries.Length.ShouldEqual(7); - - string projectedFolder = Path.Combine(newFolderPath, "Properties"); - projectedFolder.ShouldBeADirectory(this.fileSystem); - fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); - fileSystemEntries.Length.ShouldEqual(1); - - string projectedFile = Path.Combine(newFolderPath, "MountVerb.cs"); - projectedFile.ShouldBeAFile(this.fileSystem); - } - - private void ValidatePathAddsAndRemoves(string path, string expectedSparsePath) - { - this.scalarProcess.AddSparseFolders(path); - this.ValidateFoldersInSparseList(expectedSparsePath); - this.scalarProcess.RemoveSparseFolders(path); - this.ValidateFoldersInSparseList(new string[0]); - this.scalarProcess.AddSparseFolders(path); - this.ValidateFoldersInSparseList(expectedSparsePath); - this.scalarProcess.RemoveSparseFolders(expectedSparsePath); - this.ValidateFoldersInSparseList(new string[0]); - } - - private void ValidateFoldersInSparseList(params string[] folders) - { - StringBuilder folderErrors = new StringBuilder(); - HashSet actualSparseFolders = new HashSet(this.scalarProcess.GetSparseFolders()); - - foreach (string expectedFolder in folders) - { - if (!actualSparseFolders.Contains(expectedFolder)) - { - folderErrors.AppendLine($"{expectedFolder} not found in actual folder list"); - } - - actualSparseFolders.Remove(expectedFolder); - } - - foreach (string extraFolder in actualSparseFolders) - { - folderErrors.AppendLine($"{extraFolder} unexpected in folder list"); - } - - folderErrors.Length.ShouldEqual(0, folderErrors.ToString()); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class SparseTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem = new SystemIORunner(); + private ScalarProcess scalarProcess; + private string mainSparseFolder = Path.Combine("Scalar", "Scalar"); + private string[] allRootDirectories; + private string[] directoriesInMainFolder; + + [OneTimeSetUp] + public void Setup() + { + this.scalarProcess = new ScalarProcess(this.Enlistment); + this.allRootDirectories = Directory.GetDirectories(this.Enlistment.RepoRoot); + this.directoriesInMainFolder = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); + } + + [TearDown] + public void TearDown() + { + GitProcess.Invoke(this.Enlistment.RepoRoot, "clean -xdf"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "reset --hard"); + + foreach (string sparseFolder in this.scalarProcess.GetSparseFolders()) + { + this.scalarProcess.RemoveSparseFolders(sparseFolder); + } + + // Remove all sparse folders should make all folders appear again + string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot); + directories.ShouldMatchInOrder(this.allRootDirectories); + this.ValidateFoldersInSparseList(new string[0]); + } + + [TestCase, Order(1)] + public void BasicTestsAddingSparseFolder() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string[] directories = Directory.GetDirectories(this.Enlistment.RepoRoot); + directories.Length.ShouldEqual(2); + directories[0].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, ".git")); + directories[1].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, "Scalar")); + + string folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder); + folder.ShouldBeADirectory(this.fileSystem); + folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine"); + folder.ShouldBeADirectory(this.fileSystem); + + string file = this.Enlistment.GetVirtualPathTo("Readme.md"); + file.ShouldBeAFile(this.fileSystem); + + folder = this.Enlistment.GetVirtualPathTo("Scripts"); + folder.ShouldNotExistOnDisk(this.fileSystem); + folder = this.Enlistment.GetVirtualPathTo("Scalar", "Scalar.Mount"); + folder.ShouldNotExistOnDisk(this.fileSystem); + + string secondPath = Path.Combine("Scalar", "Scalar.Common", "Physical"); + this.scalarProcess.AddSparseFolders(secondPath); + folder = this.Enlistment.GetVirtualPathTo(secondPath); + folder.ShouldBeADirectory(this.fileSystem); + file = this.Enlistment.GetVirtualPathTo("Scalar", "Scalar.Common", "Enlistment.cs"); + file.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(2)] + public void AddAndRemoveVariousPathsTests() + { + // Paths to validate [0] = path to pass to sparse [1] = expected path saved + string[][] paths = new[] + { + // AltDirectorySeparatorChar should get converted to DirectorySeparatorChar + new[] { string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Scalar"), this.mainSparseFolder }, + + // AltDirectorySeparatorChar should get trimmed + new[] { $"{Path.AltDirectorySeparatorChar}{string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Test")}{Path.AltDirectorySeparatorChar}", Path.Combine("Scalar", "Test") }, + + // DirectorySeparatorChar should get trimmed + new[] { $"{Path.DirectorySeparatorChar}{Path.Combine("Scalar", "More")}{Path.DirectorySeparatorChar}", Path.Combine("Scalar", "More") }, + + // spaces should get trimmed + new[] { $" {string.Join(Path.AltDirectorySeparatorChar.ToString(), "Scalar", "Other")} ", Path.Combine("Scalar", "Other") }, + }; + + foreach (string[] pathToValidate in paths) + { + this.ValidatePathAddsAndRemoves(pathToValidate[0], pathToValidate[1]); + } + } + + [TestCase, Order(3)] + public void AddingParentDirectoryShouldMakeItRecursive() + { + string childPath = Path.Combine(this.mainSparseFolder, "CommandLine"); + this.scalarProcess.AddSparseFolders(childPath); + string[] directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); + directories.Length.ShouldEqual(1); + directories[0].ShouldEqual(Path.Combine(this.Enlistment.RepoRoot, childPath)); + this.ValidateFoldersInSparseList(childPath); + + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + directories = Directory.GetDirectories(Path.Combine(this.Enlistment.RepoRoot, this.mainSparseFolder)); + directories.Length.ShouldBeAtLeast(2); + directories.ShouldMatchInOrder(this.directoriesInMainFolder); + this.ValidateFoldersInSparseList(childPath, this.mainSparseFolder); + } + + [TestCase, Order(4)] + public void AddingSiblingFolderShouldNotMakeParentRecursive() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + // Add and remove sibling folder to main folder + string siblingPath = Path.Combine("Scalar", "FastFetch"); + this.scalarProcess.AddSparseFolders(siblingPath); + string folder = this.Enlistment.GetVirtualPathTo(siblingPath); + folder.ShouldBeADirectory(this.fileSystem); + this.ValidateFoldersInSparseList(this.mainSparseFolder, siblingPath); + + this.scalarProcess.RemoveSparseFolders(siblingPath); + folder.ShouldNotExistOnDisk(this.fileSystem); + folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder); + folder.ShouldBeADirectory(this.fileSystem); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + } + + [TestCase, Order(5)] + public void AddingSubfolderShouldKeepParentRecursive() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + // Add subfolder of main folder and make sure it stays recursive + string subFolder = Path.Combine(this.mainSparseFolder, "Properties"); + this.scalarProcess.AddSparseFolders(subFolder); + string folder = this.Enlistment.GetVirtualPathTo(subFolder); + folder.ShouldBeADirectory(this.fileSystem); + this.ValidateFoldersInSparseList(this.mainSparseFolder, subFolder); + + folder = this.Enlistment.GetVirtualPathTo(this.mainSparseFolder, "CommandLine"); + folder.ShouldBeADirectory(this.fileSystem); + } + + [TestCase, Order(6)] + [Category(Categories.WindowsOnly)] + public void CreatingFolderShouldAddToSparseListAndStartProjecting() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Common"); + newFolderPath.ShouldNotExistOnDisk(this.fileSystem); + Directory.CreateDirectory(newFolderPath); + newFolderPath.ShouldBeADirectory(this.fileSystem); + string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); + fileSystemEntries.Length.ShouldEqual(32); + this.ValidateFoldersInSparseList(this.mainSparseFolder, Path.Combine("Scalar", "Scalar.Common")); + + string projectedFolder = Path.Combine(newFolderPath, "Git"); + projectedFolder.ShouldBeADirectory(this.fileSystem); + fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); + fileSystemEntries.Length.ShouldEqual(13); + + string projectedFile = Path.Combine(newFolderPath, "ReturnCode.cs"); + projectedFile.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(7)] + [Category(Categories.MacOnly)] + public void CreateFolderThenFileShouldAddToSparseListAndStartProjecting() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Common"); + newFolderPath.ShouldNotExistOnDisk(this.fileSystem); + Directory.CreateDirectory(newFolderPath); + string newFilePath = Path.Combine(newFolderPath, "test.txt"); + File.WriteAllText(newFilePath, "New file content"); + newFolderPath.ShouldBeADirectory(this.fileSystem); + newFilePath.ShouldBeAFile(this.fileSystem); + string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); + fileSystemEntries.Length.ShouldEqual(33); + this.ValidateFoldersInSparseList(this.mainSparseFolder, Path.Combine("Scalar", "Scalar.Common")); + + string projectedFolder = Path.Combine(newFolderPath, "Git"); + projectedFolder.ShouldBeADirectory(this.fileSystem); + fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); + fileSystemEntries.Length.ShouldEqual(13); + + string projectedFile = Path.Combine(newFolderPath, "ReturnCode.cs"); + projectedFile.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(7)] + public void ReadFileThenChangingSparseFoldersShouldRemoveFileAndFolder() + { + string fileToRead = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); + this.fileSystem.ReadAllText(fileToRead); + + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); + folderPath.ShouldNotExistOnDisk(this.fileSystem); + fileToRead.ShouldNotExistOnDisk(this.fileSystem); + } + + [TestCase, Order(8)] + public void CreateNewFileWillPreventRemoveSparseFolder() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder, "Scripts"); + this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts"); + + string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "newfile.txt"); + this.fileSystem.WriteAllText(fileToCreate, "New Contents"); + + string output = this.scalarProcess.RemoveSparseFolders(shouldSucceed: false, folders: "Scripts"); + output.ShouldContain("sparse was aborted"); + this.ValidateFoldersInSparseList(this.mainSparseFolder, "Scripts"); + + string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); + folderPath.ShouldBeADirectory(this.fileSystem); + string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath); + fileSystemEntries.Length.ShouldEqual(6); + fileToCreate.ShouldBeAFile(this.fileSystem); + + this.fileSystem.DeleteFile(fileToCreate); + } + + [TestCase, Order(9)] + public void ModifiedFileShouldNotAllowSparseFolderChange() + { + string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); + this.fileSystem.WriteAllText(modifiedPath, "New Contents"); + + string output = this.scalarProcess.AddSparseFolders(shouldSucceed: false, folders: this.mainSparseFolder); + output.ShouldContain("sparse was aborted"); + this.ValidateFoldersInSparseList(new string[0]); + } + + [TestCase, Order(10)] + public void ModifiedFileAndCommitThenChangingSparseFoldersShouldKeepFileAndFolder() + { + string modifiedPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts", "RunFunctionalTests.bat"); + this.fileSystem.WriteAllText(modifiedPath, "New Contents"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); + GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); + + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scripts"); + folderPath.ShouldBeADirectory(this.fileSystem); + modifiedPath.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(11)] + public void DeleteFileAndCommitThenChangingSparseFoldersShouldKeepFolderAndFile() + { + string deletePath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Tests", "packages.config"); + this.fileSystem.DeleteFile(deletePath); + GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); + GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); + + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + // File and folder should no longer be on disk because the file was deleted and the folder deleted becase it was empty + string folderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Tests"); + folderPath.ShouldNotExistOnDisk(this.fileSystem); + deletePath.ShouldNotExistOnDisk(this.fileSystem); + + // Folder and file should be on disk even though they are outside the sparse scope because the file is in the modified paths + GitProcess.Invoke(this.Enlistment.RepoRoot, "checkout HEAD~1"); + folderPath.ShouldBeADirectory(this.fileSystem); + deletePath.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(12)] + public void CreateNewFileAndCommitThenRemoveSparseFolderShouldKeepFileAndFolder() + { + string folderToCreateFileIn = Path.Combine("Scalar", "Scalar.Hooks"); + this.scalarProcess.AddSparseFolders(this.mainSparseFolder, folderToCreateFileIn); + this.ValidateFoldersInSparseList(this.mainSparseFolder, folderToCreateFileIn); + + string fileToCreate = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn, "newfile.txt"); + this.fileSystem.WriteAllText(fileToCreate, "New Contents"); + GitProcess.Invoke(this.Enlistment.RepoRoot, "add ."); + GitProcess.Invoke(this.Enlistment.RepoRoot, "commit -m Test"); + + this.scalarProcess.RemoveSparseFolders(folderToCreateFileIn); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + string folderPath = Path.Combine(this.Enlistment.RepoRoot, folderToCreateFileIn); + folderPath.ShouldBeADirectory(this.fileSystem); + string[] fileSystemEntries = Directory.GetFileSystemEntries(folderPath); + fileSystemEntries.Length.ShouldEqual(1); + fileToCreate.ShouldBeAFile(this.fileSystem); + } + + [TestCase, Order(13)] + [Category(Categories.MacOnly)] + public void CreateFolderAndFileThatAreExcluded() + { + this.scalarProcess.AddSparseFolders(this.mainSparseFolder); + this.ValidateFoldersInSparseList(this.mainSparseFolder); + + // Create a file that should already be in the projection but excluded + string newFolderPath = Path.Combine(this.Enlistment.RepoRoot, "Scalar", "Scalar.Mount"); + newFolderPath.ShouldNotExistOnDisk(this.fileSystem); + Directory.CreateDirectory(newFolderPath); + string newFilePath = Path.Combine(newFolderPath, "Program.cs"); + File.WriteAllText(newFilePath, "New file content"); + newFolderPath.ShouldBeADirectory(this.fileSystem); + newFilePath.ShouldBeAFile(this.fileSystem); + string[] fileSystemEntries = Directory.GetFileSystemEntries(newFolderPath); + fileSystemEntries.Length.ShouldEqual(7); + + string projectedFolder = Path.Combine(newFolderPath, "Properties"); + projectedFolder.ShouldBeADirectory(this.fileSystem); + fileSystemEntries = Directory.GetFileSystemEntries(projectedFolder); + fileSystemEntries.Length.ShouldEqual(1); + + string projectedFile = Path.Combine(newFolderPath, "MountVerb.cs"); + projectedFile.ShouldBeAFile(this.fileSystem); + } + + private void ValidatePathAddsAndRemoves(string path, string expectedSparsePath) + { + this.scalarProcess.AddSparseFolders(path); + this.ValidateFoldersInSparseList(expectedSparsePath); + this.scalarProcess.RemoveSparseFolders(path); + this.ValidateFoldersInSparseList(new string[0]); + this.scalarProcess.AddSparseFolders(path); + this.ValidateFoldersInSparseList(expectedSparsePath); + this.scalarProcess.RemoveSparseFolders(expectedSparsePath); + this.ValidateFoldersInSparseList(new string[0]); + } + + private void ValidateFoldersInSparseList(params string[] folders) + { + StringBuilder folderErrors = new StringBuilder(); + HashSet actualSparseFolders = new HashSet(this.scalarProcess.GetSparseFolders()); + + foreach (string expectedFolder in folders) + { + if (!actualSparseFolders.Contains(expectedFolder)) + { + folderErrors.AppendLine($"{expectedFolder} not found in actual folder list"); + } + + actualSparseFolders.Remove(expectedFolder); + } + + foreach (string extraFolder in actualSparseFolders) + { + folderErrors.AppendLine($"{extraFolder} unexpected in folder list"); + } + + folderErrors.Length.ShouldEqual(0, folderErrors.ToString()); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/StatusVerbTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/StatusVerbTests.cs index 6d4f21b039..68afcf11e7 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/StatusVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/StatusVerbTests.cs @@ -1,29 +1,29 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class StatusVerbTests : TestsWithEnlistmentPerFixture - { - [TestCase] - public void GitTrace() - { - Dictionary environmentVariables = new Dictionary(); - - this.Enlistment.Status(trace: "1"); - this.Enlistment.Status(trace: "2"); - - string logPath = Path.Combine(this.Enlistment.RepoRoot, "log-file.txt"); - this.Enlistment.Status(trace: logPath); - - FileSystemRunner fileSystem = new SystemIORunner(); - fileSystem.FileExists(logPath).ShouldBeTrue(); - string.IsNullOrWhiteSpace(fileSystem.ReadAllText(logPath)).ShouldBeFalse(); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class StatusVerbTests : TestsWithEnlistmentPerFixture + { + [TestCase] + public void GitTrace() + { + Dictionary environmentVariables = new Dictionary(); + + this.Enlistment.Status(trace: "1"); + this.Enlistment.Status(trace: "2"); + + string logPath = Path.Combine(this.Enlistment.RepoRoot, "log-file.txt"); + this.Enlistment.Status(trace: logPath); + + FileSystemRunner fileSystem = new SystemIORunner(); + fileSystem.FileExists(logPath).ShouldBeTrue(); + string.IsNullOrWhiteSpace(fileSystem.ReadAllText(logPath)).ShouldBeFalse(); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs index b65eb15a29..f2370e9a69 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/SymbolicLinkTests.cs @@ -1,194 +1,194 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; +using Scalar.FunctionalTests.Tools; using Scalar.Tests.Should; -using System.IO; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - // MacOnly until issue #297 (add SymLink support for Windows) is complete - [Category(Categories.MacOnly)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - [TestFixture] - public class SymbolicLinkTests : TestsWithEnlistmentPerFixture - { - private const string TestFolderName = "Test_EPF_SymbolicLinks"; - - // FunctionalTests/20180925_SymLinksPart1 files - private const string TestFileName = "TestFile.txt"; - private const string TestFileContents = "This is a real file"; - private const string TestFile2Name = "TestFile2.txt"; - private const string TestFile2Contents = "This is the second real file"; - private const string ChildFolderName = "ChildDir"; - private const string ChildLinkName = "LinkToFileInFolder"; - private const string GrandChildLinkName = "LinkToFileInParentFolder"; - - // FunctionalTests/20180925_SymLinksPart2 files - // Note: In this branch ChildLinkName has been changed to point to TestFile2Name - private const string GrandChildFileName = "TestFile3.txt"; - private const string GrandChildFileContents = "This is the third file"; - private const string GrandChildLinkNowAFileContents = "This was a link but is now a file"; - - // FunctionalTests/20180925_SymLinksPart3 files - private const string ChildFolder2Name = "ChildDir2"; - - // FunctionalTests/20180925_SymLinksPart4 files - // Note: In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName - - private BashRunner bashRunner; - public SymbolicLinkTests() - { - this.bashRunner = new BashRunner(); - } - - [TestCase, Order(1)] - public void CheckoutBranchWithSymLinks() - { - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart1"); - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart1", - "nothing to commit, working tree clean"); - - string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); - testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); - this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); - - string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); - testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); - - string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); - this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); - childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); - - string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); - this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeTrue($"{grandChildLinkPath} should be a symlink"); - grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - } - - [TestCase, Order(2)] - public void CheckoutBranchWhereSymLinksChangeContentsAndTransitionToFile() - { - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart2"); - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart2", - "nothing to commit, working tree clean"); - - // testFilePath and testFile2Path are unchanged from FunctionalTests/20180925_SymLinksPart2 - string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); - testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); - this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); - - string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); - testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); - - // In this branch childLinkPath has been changed to point to testFile2Path - string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); - this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); - childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - - // grandChildLinkPath should now be a file - string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); - this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); - grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); - - // There should also be a new file in the child folder - string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); - newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); - this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); - } - - [TestCase, Order(3)] - public void CheckoutBranchWhereFilesTransitionToSymLinks() - { - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart3"); - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart3", - "nothing to commit, working tree clean"); - - // In this branch testFilePath has been changed to point to newGrandChildFilePath - string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); - testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); - this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); - - // There should be a new ChildFolder2Name directory - string childFolder2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); - this.bashRunner.IsSymbolicLink(childFolder2Path).ShouldBeFalse($"{childFolder2Path} should not be a symlink"); - childFolder2Path.ShouldBeADirectory(this.bashRunner); - - // The rest of the files are unchanged from FunctionalTests/20180925_SymLinksPart2 - string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); - testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); - - string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); - this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); - childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - - string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); - this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); - grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); - - string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); - newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); - this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); - } - - [TestCase, Order(4)] - public void CheckoutBranchWhereSymLinkTransistionsToFolderAndFolderTransitionsToSymlink() - { - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart4"); - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart4", - "nothing to commit, working tree clean"); - - // In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName - string linkNowADirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); - this.bashRunner.IsSymbolicLink(linkNowADirectoryPath).ShouldBeFalse($"{linkNowADirectoryPath} should not be a symlink"); - linkNowADirectoryPath.ShouldBeADirectory(this.bashRunner); - - string directoryNowALinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); - this.bashRunner.IsSymbolicLink(directoryNowALinkPath).ShouldBeTrue($"{directoryNowALinkPath} should be a symlink"); - } - - [TestCase, Order(5)] - public void GitStatusReportsSymLinkChanges() - { - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart4", - "nothing to commit, working tree clean"); - - string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); - testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); - this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); - - string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); - testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); - - // Update testFilePath's symlink to point to testFile2Path - this.bashRunner.CreateSymbolicLink(testFilePath, testFile2Path); - - testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); - this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); - - GitHelpers.CheckGitCommandAgainstScalarRepo( - this.Enlistment.RepoRoot, - "status", - "On branch FunctionalTests/20180925_SymLinksPart4", - $"modified: {TestFolderName}/{TestFileName}"); - } - } -} +using System.IO; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + // MacOnly until issue #297 (add SymLink support for Windows) is complete + [Category(Categories.MacOnly)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + [TestFixture] + public class SymbolicLinkTests : TestsWithEnlistmentPerFixture + { + private const string TestFolderName = "Test_EPF_SymbolicLinks"; + + // FunctionalTests/20180925_SymLinksPart1 files + private const string TestFileName = "TestFile.txt"; + private const string TestFileContents = "This is a real file"; + private const string TestFile2Name = "TestFile2.txt"; + private const string TestFile2Contents = "This is the second real file"; + private const string ChildFolderName = "ChildDir"; + private const string ChildLinkName = "LinkToFileInFolder"; + private const string GrandChildLinkName = "LinkToFileInParentFolder"; + + // FunctionalTests/20180925_SymLinksPart2 files + // Note: In this branch ChildLinkName has been changed to point to TestFile2Name + private const string GrandChildFileName = "TestFile3.txt"; + private const string GrandChildFileContents = "This is the third file"; + private const string GrandChildLinkNowAFileContents = "This was a link but is now a file"; + + // FunctionalTests/20180925_SymLinksPart3 files + private const string ChildFolder2Name = "ChildDir2"; + + // FunctionalTests/20180925_SymLinksPart4 files + // Note: In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName + + private BashRunner bashRunner; + public SymbolicLinkTests() + { + this.bashRunner = new BashRunner(); + } + + [TestCase, Order(1)] + public void CheckoutBranchWithSymLinks() + { + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart1"); + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart1", + "nothing to commit, working tree clean"); + + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeTrue($"{grandChildLinkPath} should be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + } + + [TestCase, Order(2)] + public void CheckoutBranchWhereSymLinksChangeContentsAndTransitionToFile() + { + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart2"); + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart2", + "nothing to commit, working tree clean"); + + // testFilePath and testFile2Path are unchanged from FunctionalTests/20180925_SymLinksPart2 + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeFalse($"{testFilePath} should not be a symlink"); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + // In this branch childLinkPath has been changed to point to testFile2Path + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + + // grandChildLinkPath should now be a file + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); + + // There should also be a new file in the child folder + string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); + newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); + } + + [TestCase, Order(3)] + public void CheckoutBranchWhereFilesTransitionToSymLinks() + { + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart3"); + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart3", + "nothing to commit, working tree clean"); + + // In this branch testFilePath has been changed to point to newGrandChildFilePath + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + + // There should be a new ChildFolder2Name directory + string childFolder2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); + this.bashRunner.IsSymbolicLink(childFolder2Path).ShouldBeFalse($"{childFolder2Path} should not be a symlink"); + childFolder2Path.ShouldBeADirectory(this.bashRunner); + + // The rest of the files are unchanged from FunctionalTests/20180925_SymLinksPart2 + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + string childLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(childLinkPath).ShouldBeTrue($"{childLinkPath} should be a symlink"); + childLinkPath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + + string grandChildLinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildLinkName)); + this.bashRunner.IsSymbolicLink(grandChildLinkPath).ShouldBeFalse($"{grandChildLinkPath} should not be a symlink"); + grandChildLinkPath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildLinkNowAFileContents); + + string newGrandChildFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolderName, GrandChildFileName)); + newGrandChildFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(newGrandChildFilePath).ShouldBeFalse($"{newGrandChildFilePath} should not be a symlink"); + } + + [TestCase, Order(4)] + public void CheckoutBranchWhereSymLinkTransistionsToFolderAndFolderTransitionsToSymlink() + { + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "checkout FunctionalTests/20180925_SymLinksPart4"); + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + "nothing to commit, working tree clean"); + + // In this branch ChildLinkName has been changed to a directory and ChildFolder2Name has been changed to a link to ChildFolderName + string linkNowADirectoryPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildLinkName)); + this.bashRunner.IsSymbolicLink(linkNowADirectoryPath).ShouldBeFalse($"{linkNowADirectoryPath} should not be a symlink"); + linkNowADirectoryPath.ShouldBeADirectory(this.bashRunner); + + string directoryNowALinkPath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, ChildFolder2Name)); + this.bashRunner.IsSymbolicLink(directoryNowALinkPath).ShouldBeTrue($"{directoryNowALinkPath} should be a symlink"); + } + + [TestCase, Order(5)] + public void GitStatusReportsSymLinkChanges() + { + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + "nothing to commit, working tree clean"); + + string testFilePath = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFileName)); + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(GrandChildFileContents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + + string testFile2Path = this.Enlistment.GetVirtualPathTo(Path.Combine(TestFolderName, TestFile2Name)); + testFile2Path.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFile2Path).ShouldBeFalse($"{testFile2Path} should not be a symlink"); + + // Update testFilePath's symlink to point to testFile2Path + this.bashRunner.CreateSymbolicLink(testFilePath, testFile2Path); + + testFilePath.ShouldBeAFile(this.bashRunner).WithContents(TestFile2Contents); + this.bashRunner.IsSymbolicLink(testFilePath).ShouldBeTrue($"{testFilePath} should be a symlink"); + + GitHelpers.CheckGitCommandAgainstScalarRepo( + this.Enlistment.RepoRoot, + "status", + "On branch FunctionalTests/20180925_SymLinksPart4", + $"modified: {TestFolderName}/{TestFileName}"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs index 7e1f74b0aa..5c864eac41 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/TestsWithEnlistmentPerFixture.cs @@ -1,45 +1,45 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - public abstract class TestsWithEnlistmentPerFixture - { - private readonly bool forcePerRepoObjectCache; - private readonly bool skipPrefetchDuringClone; - - public TestsWithEnlistmentPerFixture(bool forcePerRepoObjectCache = false, bool skipPrefetchDuringClone = false) - { - this.forcePerRepoObjectCache = forcePerRepoObjectCache; - this.skipPrefetchDuringClone = skipPrefetchDuringClone; - } - - public ScalarFunctionalTestEnlistment Enlistment - { - get; private set; - } - - [OneTimeSetUp] - public virtual void CreateEnlistment() - { - if (this.forcePerRepoObjectCache) - { - this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(ScalarTestConfig.PathToScalar, this.skipPrefetchDuringClone); - } - else - { - this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar); - } - } - - [OneTimeTearDown] - public virtual void DeleteEnlistment() - { - if (this.Enlistment != null) - { - this.Enlistment.UnmountAndDeleteAll(); - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + public abstract class TestsWithEnlistmentPerFixture + { + private readonly bool forcePerRepoObjectCache; + private readonly bool skipPrefetchDuringClone; + + public TestsWithEnlistmentPerFixture(bool forcePerRepoObjectCache = false, bool skipPrefetchDuringClone = false) + { + this.forcePerRepoObjectCache = forcePerRepoObjectCache; + this.skipPrefetchDuringClone = skipPrefetchDuringClone; + } + + public ScalarFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [OneTimeSetUp] + public virtual void CreateEnlistment() + { + if (this.forcePerRepoObjectCache) + { + this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(ScalarTestConfig.PathToScalar, this.skipPrefetchDuringClone); + } + else + { + this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar); + } + } + + [OneTimeTearDown] + public virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.UnmountAndDeleteAll(); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/UnmountTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/UnmountTests.cs index ee9bb1b79a..d01dd6d5f9 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/UnmountTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerFixture/UnmountTests.cs @@ -1,85 +1,85 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class UnmountTests : TestsWithEnlistmentPerFixture - { - private FileSystemRunner fileSystem; - - public UnmountTests() - { - this.fileSystem = new SystemIORunner(); - } - - [SetUp] - public void SetupTest() - { - ScalarProcess scalarProcess = new ScalarProcess( - ScalarTestConfig.PathToScalar, - this.Enlistment.EnlistmentRoot, - Path.Combine(this.Enlistment.EnlistmentRoot, ScalarTestConfig.DotScalarRoot)); - - if (!scalarProcess.IsEnlistmentMounted()) - { - scalarProcess.Mount(); - } - } - - [TestCase] - public void UnmountWaitsForLock() - { - ManualResetEventSlim lockHolder = GitHelpers.AcquireScalarLock(this.Enlistment, out _); - - using (Process unmountingProcess = this.StartUnmount()) - { - unmountingProcess.WaitForExit(3000).ShouldEqual(false, "Unmount completed while lock was acquired."); - - // Release the lock. - lockHolder.Set(); - - unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected."); - } - } - - [TestCase] - public void UnmountSkipLock() - { - ManualResetEventSlim lockHolder = GitHelpers.AcquireScalarLock(this.Enlistment, out _, Timeout.Infinite, true); - - using (Process unmountingProcess = this.StartUnmount("--skip-wait-for-lock")) - { - unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected."); - } - - // Signal process holding lock to terminate and release lock. - lockHolder.Set(); - } - - private Process StartUnmount(string extraParams = "") - { - string enlistmentRoot = this.Enlistment.EnlistmentRoot; - - // TODO: 865304 Use app.config instead of --internal* arguments - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = "unmount " + extraParams + " " + TestConstants.InternalUseOnlyFlag + " " + ScalarHelpers.GetInternalParameter(); - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.WorkingDirectory = enlistmentRoot; - processInfo.UseShellExecute = false; - - Process executingProcess = new Process(); - executingProcess.StartInfo = processInfo; - executingProcess.Start(); - - return executingProcess; - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerFixture +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class UnmountTests : TestsWithEnlistmentPerFixture + { + private FileSystemRunner fileSystem; + + public UnmountTests() + { + this.fileSystem = new SystemIORunner(); + } + + [SetUp] + public void SetupTest() + { + ScalarProcess scalarProcess = new ScalarProcess( + ScalarTestConfig.PathToScalar, + this.Enlistment.EnlistmentRoot, + Path.Combine(this.Enlistment.EnlistmentRoot, ScalarTestConfig.DotScalarRoot)); + + if (!scalarProcess.IsEnlistmentMounted()) + { + scalarProcess.Mount(); + } + } + + [TestCase] + public void UnmountWaitsForLock() + { + ManualResetEventSlim lockHolder = GitHelpers.AcquireScalarLock(this.Enlistment, out _); + + using (Process unmountingProcess = this.StartUnmount()) + { + unmountingProcess.WaitForExit(3000).ShouldEqual(false, "Unmount completed while lock was acquired."); + + // Release the lock. + lockHolder.Set(); + + unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected."); + } + } + + [TestCase] + public void UnmountSkipLock() + { + ManualResetEventSlim lockHolder = GitHelpers.AcquireScalarLock(this.Enlistment, out _, Timeout.Infinite, true); + + using (Process unmountingProcess = this.StartUnmount("--skip-wait-for-lock")) + { + unmountingProcess.WaitForExit(10000).ShouldEqual(true, "Unmount didn't complete as expected."); + } + + // Signal process holding lock to terminate and release lock. + lockHolder.Set(); + } + + private Process StartUnmount(string extraParams = "") + { + string enlistmentRoot = this.Enlistment.EnlistmentRoot; + + // TODO: 865304 Use app.config instead of --internal* arguments + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = "unmount " + extraParams + " " + TestConstants.InternalUseOnlyFlag + " " + ScalarHelpers.GetInternalParameter(); + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.WorkingDirectory = enlistmentRoot; + processInfo.UseShellExecute = false; + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + executingProcess.Start(); + + return executingProcess; + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs index 5d5c276b67..76bd366497 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/CaseOnlyFolderRenameTests.cs @@ -1,76 +1,76 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.Tests.Should; -using System.IO; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase -{ - [TestFixture] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class CaseOnlyFolderRenameTests : TestsWithEnlistmentPerTestCase - { - private FileSystemRunner fileSystem; - - public CaseOnlyFolderRenameTests() - { - this.fileSystem = new BashRunner(); - } - - // MacOnly because renames of partial folders are blocked on Windows - [TestCase] - [Category(Categories.MacOnly)] - public void CaseRenameFoldersAndRemountAndRenameAgain() - { - // Projected folder without a physical folder - string parentFolderName = "Scalar"; - string oldScalarSubFolderName = "Scalar"; - string oldScalarSubFolderPath = Path.Combine(parentFolderName, oldScalarSubFolderName); - string newScalarSubFolderName = "scalar"; - string newScalarSubFolderPath = Path.Combine(parentFolderName, newScalarSubFolderName); - - this.Enlistment.GetVirtualPathTo(oldScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(oldScalarSubFolderName); - - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldScalarSubFolderPath), this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath)); - - this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newScalarSubFolderName); - - // Projected folder with a physical folder - string oldTestsSubFolderName = "Scalar.FunctionalTests"; - string oldTestsSubFolderPath = Path.Combine(parentFolderName, oldTestsSubFolderName); - string newTestsSubFolderName = "scalar.functionaltests"; - string newTestsSubFolderPath = Path.Combine(parentFolderName, newTestsSubFolderName); - - string fileToAdd = "NewFile.txt"; - string fileToAddContent = "This is new file text."; - string fileToAddPath = this.Enlistment.GetVirtualPathTo(Path.Combine(oldTestsSubFolderPath, fileToAdd)); - this.fileSystem.WriteAllText(fileToAddPath, fileToAddContent); - - this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(oldTestsSubFolderName); - - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath)); - - this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newTestsSubFolderName); - - // Remount - this.Enlistment.UnmountScalar(); - this.Enlistment.MountScalar(); - - this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newScalarSubFolderName); - this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newTestsSubFolderName); - this.Enlistment.GetVirtualPathTo(Path.Combine(newTestsSubFolderPath, fileToAdd)).ShouldBeAFile(this.fileSystem).WithContents().ShouldEqual(fileToAddContent); - - // Rename each folder again - string finalScalarSubFolderName = "gvFS"; - string finalScalarSubFolderPath = Path.Combine(parentFolderName, finalScalarSubFolderName); - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath), this.Enlistment.GetVirtualPathTo(finalScalarSubFolderPath)); - this.Enlistment.GetVirtualPathTo(finalScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(finalScalarSubFolderName); - - string finalTestsSubFolderName = "scalar.FunctionalTESTS"; - string finalTestsSubFolderPath = Path.Combine(parentFolderName, finalTestsSubFolderName); - this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath)); - this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(finalTestsSubFolderName); - this.Enlistment.GetVirtualPathTo(Path.Combine(finalTestsSubFolderPath, fileToAdd)).ShouldBeAFile(this.fileSystem).WithContents().ShouldEqual(fileToAddContent); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.Tests.Should; +using System.IO; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class CaseOnlyFolderRenameTests : TestsWithEnlistmentPerTestCase + { + private FileSystemRunner fileSystem; + + public CaseOnlyFolderRenameTests() + { + this.fileSystem = new BashRunner(); + } + + // MacOnly because renames of partial folders are blocked on Windows + [TestCase] + [Category(Categories.MacOnly)] + public void CaseRenameFoldersAndRemountAndRenameAgain() + { + // Projected folder without a physical folder + string parentFolderName = "Scalar"; + string oldScalarSubFolderName = "Scalar"; + string oldScalarSubFolderPath = Path.Combine(parentFolderName, oldScalarSubFolderName); + string newScalarSubFolderName = "scalar"; + string newScalarSubFolderPath = Path.Combine(parentFolderName, newScalarSubFolderName); + + this.Enlistment.GetVirtualPathTo(oldScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(oldScalarSubFolderName); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldScalarSubFolderPath), this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath)); + + this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newScalarSubFolderName); + + // Projected folder with a physical folder + string oldTestsSubFolderName = "Scalar.FunctionalTests"; + string oldTestsSubFolderPath = Path.Combine(parentFolderName, oldTestsSubFolderName); + string newTestsSubFolderName = "scalar.functionaltests"; + string newTestsSubFolderPath = Path.Combine(parentFolderName, newTestsSubFolderName); + + string fileToAdd = "NewFile.txt"; + string fileToAddContent = "This is new file text."; + string fileToAddPath = this.Enlistment.GetVirtualPathTo(Path.Combine(oldTestsSubFolderPath, fileToAdd)); + this.fileSystem.WriteAllText(fileToAddPath, fileToAddContent); + + this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(oldTestsSubFolderName); + + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(oldTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath)); + + this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newTestsSubFolderName); + + // Remount + this.Enlistment.UnmountScalar(); + this.Enlistment.MountScalar(); + + this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newScalarSubFolderName); + this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(newTestsSubFolderName); + this.Enlistment.GetVirtualPathTo(Path.Combine(newTestsSubFolderPath, fileToAdd)).ShouldBeAFile(this.fileSystem).WithContents().ShouldEqual(fileToAddContent); + + // Rename each folder again + string finalScalarSubFolderName = "gvFS"; + string finalScalarSubFolderPath = Path.Combine(parentFolderName, finalScalarSubFolderName); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newScalarSubFolderPath), this.Enlistment.GetVirtualPathTo(finalScalarSubFolderPath)); + this.Enlistment.GetVirtualPathTo(finalScalarSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(finalScalarSubFolderName); + + string finalTestsSubFolderName = "scalar.FunctionalTESTS"; + string finalTestsSubFolderPath = Path.Combine(parentFolderName, finalTestsSubFolderName); + this.fileSystem.MoveFile(this.Enlistment.GetVirtualPathTo(newTestsSubFolderPath), this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath)); + this.Enlistment.GetVirtualPathTo(finalTestsSubFolderPath).ShouldBeADirectory(this.fileSystem).WithCaseMatchingName(finalTestsSubFolderName); + this.Enlistment.GetVirtualPathTo(Path.Combine(finalTestsSubFolderPath, fileToAdd)).ShouldBeAFile(this.fileSystem).WithContents().ShouldEqual(fileToAddContent); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/LooseObjectStepTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/LooseObjectStepTests.cs index 44669ba878..e063e3b6c0 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/LooseObjectStepTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/LooseObjectStepTests.cs @@ -1,210 +1,210 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase -{ - [TestFixture] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class LooseObjectStepTests : TestsWithEnlistmentPerTestCase - { - private const string TempPackFolder = "tempPacks"; - private FileSystemRunner fileSystem; - - // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting - // the cache - public LooseObjectStepTests() - : base(forcePerRepoObjectCache: true) - { - this.fileSystem = new SystemIORunner(); - } - - private string GitObjectRoot => this.Enlistment.GetObjectRoot(this.fileSystem); - private string PackRoot => this.Enlistment.GetPackRoot(this.fileSystem); - private string TempPackRoot => Path.Combine(this.PackRoot, TempPackFolder); - - [TestCase] - public void RemoveLooseObjectsInPackFiles() - { - this.ClearAllObjects(); - - // Copy and expand one pack - this.ExpandOneTempPack(copyPackBackToPackDirectory: true); - this.GetLooseObjectFiles().Count.ShouldBeAtLeast(1); - this.CountPackFiles().ShouldEqual(1); - - // Cleanup should delete all loose objects, since they are in the packfile - this.Enlistment.LooseObjectStep(); - - this.GetLooseObjectFiles().Count.ShouldEqual(0); - this.CountPackFiles().ShouldEqual(1); - this.GetLooseObjectFiles().Count.ShouldEqual(0); - this.CountPackFiles().ShouldEqual(1); - } - - [TestCase] - public void PutLooseObjectsInPackFiles() - { - this.ClearAllObjects(); - - // Expand one pack, and verify we have loose objects - this.ExpandOneTempPack(copyPackBackToPackDirectory: false); - int looseObjectCount = this.GetLooseObjectFiles().Count(); - looseObjectCount.ShouldBeAtLeast(1); - - // This step should put the loose objects into a packfile - this.Enlistment.LooseObjectStep(); - - this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount); - this.CountPackFiles().ShouldEqual(1); - - // Running the step a second time should remove the loose obects and keep the pack file - this.Enlistment.LooseObjectStep(); - - this.GetLooseObjectFiles().Count.ShouldEqual(0); - this.CountPackFiles().ShouldEqual(1); - } - - [TestCase] - public void NoLooseObjectsDoesNothing() - { - this.DeleteFiles(this.GetLooseObjectFiles()); - this.GetLooseObjectFiles().Count.ShouldEqual(0); - int startingPackFileCount = this.CountPackFiles(); - - this.Enlistment.LooseObjectStep(); - - this.GetLooseObjectFiles().Count.ShouldEqual(0); - this.CountPackFiles().ShouldEqual(startingPackFileCount); - } - - [TestCase] - public void CorruptLooseObjectIsDeleted() - { - this.ClearAllObjects(); - - // Expand one pack, and verify we have loose objects - this.ExpandOneTempPack(copyPackBackToPackDirectory: false); - int looseObjectCount = this.GetLooseObjectFiles().Count(); - looseObjectCount.ShouldBeAtLeast(1, "Too few loose objects"); - - // Create an invalid loose object - string fakeBlobFolder = Path.Combine(this.GitObjectRoot, "00"); - string fakeBlob = Path.Combine( - fakeBlobFolder, - "01234567890123456789012345678901234567"); - this.fileSystem.CreateDirectory(fakeBlobFolder); - this.fileSystem.CreateEmptyFile(fakeBlob); - - // This step should fail to place the objects, but - // succeed in deleting the given file. - this.Enlistment.LooseObjectStep(); - - this.fileSystem.FileExists(fakeBlob).ShouldBeFalse( - "Step failed to delete corrupt blob"); - this.CountPackFiles().ShouldEqual(0, "Incorrect number of packs after first loose object step"); - this.GetLooseObjectFiles().Count.ShouldEqual( - looseObjectCount, - "unexpected number of loose objects after step"); - - // This step should create a pack. - this.Enlistment.LooseObjectStep(); - - this.CountPackFiles().ShouldEqual(1, "Incorrect number of packs after second loose object step"); - this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount); - - // This step should delete the loose objects - this.Enlistment.LooseObjectStep(); - - this.GetLooseObjectFiles().Count.ShouldEqual(0, "Incorrect number of loose objects after third loose object step"); - } - - private void ClearAllObjects() - { - this.Enlistment.UnmountScalar(); - - // Delete/Move any starting loose objects and packfiles - this.DeleteFiles(this.GetLooseObjectFiles()); - this.MovePackFilesToTemp(); - this.GetLooseObjectFiles().Count.ShouldEqual(0, "incorrect number of loose objects after setup"); - this.CountPackFiles().ShouldEqual(0, "incorrect number of packs after setup"); - } - - private List GetLooseObjectFiles() - { - List looseObjectFiles = new List(); - foreach (string directory in Directory.GetDirectories(this.GitObjectRoot)) - { - // Check if the directory is 2 letter HEX - if (Regex.IsMatch(directory, @"[/\\][0-9a-fA-F]{2}$")) - { - string[] files = Directory.GetFiles(directory); - looseObjectFiles.AddRange(files); - } - } - - return looseObjectFiles; - } - - private void DeleteFiles(List filePaths) - { - foreach (string filePath in filePaths) - { - File.Delete(filePath); - } - } - - private int CountPackFiles() - { - return Directory.GetFiles(this.PackRoot, "*.pack").Length; - } - - private void MovePackFilesToTemp() - { - string[] files = Directory.GetFiles(this.PackRoot); - foreach (string file in files) - { - string path2 = Path.Combine(this.TempPackRoot, Path.GetFileName(file)); - - File.Move(file, path2); - } - } - - private void ExpandOneTempPack(bool copyPackBackToPackDirectory) - { - // Find all pack files - string[] packFiles = Directory.GetFiles(this.TempPackRoot, "pack-*.pack"); - Assert.Greater(packFiles.Length, 0); - - // Pick the first one found - string packFile = packFiles[0]; - - // Send the contents of the packfile to unpack-objects to example the loose objects - // Note this won't work if the object exists in a pack file which is why we had to move them - using (FileStream packFileStream = File.OpenRead(packFile)) - { - string output = GitProcess.InvokeProcess( - this.Enlistment.RepoRoot, - "unpack-objects", - new Dictionary() { { "GIT_OBJECT_DIRECTORY", this.GitObjectRoot } }, - inputStream: packFileStream).Output; - } - - if (copyPackBackToPackDirectory) - { - // Copy the pack file back to packs - string packFileName = Path.GetFileName(packFile); - File.Copy(packFile, Path.Combine(this.PackRoot, packFileName)); - - // Replace the '.pack' with '.idx' to copy the index file - string packFileIndexName = packFileName.Replace(".pack", ".idx"); - File.Copy(Path.Combine(this.TempPackRoot, packFileIndexName), Path.Combine(this.PackRoot, packFileIndexName)); - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class LooseObjectStepTests : TestsWithEnlistmentPerTestCase + { + private const string TempPackFolder = "tempPacks"; + private FileSystemRunner fileSystem; + + // Set forcePerRepoObjectCache to true to avoid any of the tests inadvertently corrupting + // the cache + public LooseObjectStepTests() + : base(forcePerRepoObjectCache: true) + { + this.fileSystem = new SystemIORunner(); + } + + private string GitObjectRoot => this.Enlistment.GetObjectRoot(this.fileSystem); + private string PackRoot => this.Enlistment.GetPackRoot(this.fileSystem); + private string TempPackRoot => Path.Combine(this.PackRoot, TempPackFolder); + + [TestCase] + public void RemoveLooseObjectsInPackFiles() + { + this.ClearAllObjects(); + + // Copy and expand one pack + this.ExpandOneTempPack(copyPackBackToPackDirectory: true); + this.GetLooseObjectFiles().Count.ShouldBeAtLeast(1); + this.CountPackFiles().ShouldEqual(1); + + // Cleanup should delete all loose objects, since they are in the packfile + this.Enlistment.LooseObjectStep(); + + this.GetLooseObjectFiles().Count.ShouldEqual(0); + this.CountPackFiles().ShouldEqual(1); + this.GetLooseObjectFiles().Count.ShouldEqual(0); + this.CountPackFiles().ShouldEqual(1); + } + + [TestCase] + public void PutLooseObjectsInPackFiles() + { + this.ClearAllObjects(); + + // Expand one pack, and verify we have loose objects + this.ExpandOneTempPack(copyPackBackToPackDirectory: false); + int looseObjectCount = this.GetLooseObjectFiles().Count(); + looseObjectCount.ShouldBeAtLeast(1); + + // This step should put the loose objects into a packfile + this.Enlistment.LooseObjectStep(); + + this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount); + this.CountPackFiles().ShouldEqual(1); + + // Running the step a second time should remove the loose obects and keep the pack file + this.Enlistment.LooseObjectStep(); + + this.GetLooseObjectFiles().Count.ShouldEqual(0); + this.CountPackFiles().ShouldEqual(1); + } + + [TestCase] + public void NoLooseObjectsDoesNothing() + { + this.DeleteFiles(this.GetLooseObjectFiles()); + this.GetLooseObjectFiles().Count.ShouldEqual(0); + int startingPackFileCount = this.CountPackFiles(); + + this.Enlistment.LooseObjectStep(); + + this.GetLooseObjectFiles().Count.ShouldEqual(0); + this.CountPackFiles().ShouldEqual(startingPackFileCount); + } + + [TestCase] + public void CorruptLooseObjectIsDeleted() + { + this.ClearAllObjects(); + + // Expand one pack, and verify we have loose objects + this.ExpandOneTempPack(copyPackBackToPackDirectory: false); + int looseObjectCount = this.GetLooseObjectFiles().Count(); + looseObjectCount.ShouldBeAtLeast(1, "Too few loose objects"); + + // Create an invalid loose object + string fakeBlobFolder = Path.Combine(this.GitObjectRoot, "00"); + string fakeBlob = Path.Combine( + fakeBlobFolder, + "01234567890123456789012345678901234567"); + this.fileSystem.CreateDirectory(fakeBlobFolder); + this.fileSystem.CreateEmptyFile(fakeBlob); + + // This step should fail to place the objects, but + // succeed in deleting the given file. + this.Enlistment.LooseObjectStep(); + + this.fileSystem.FileExists(fakeBlob).ShouldBeFalse( + "Step failed to delete corrupt blob"); + this.CountPackFiles().ShouldEqual(0, "Incorrect number of packs after first loose object step"); + this.GetLooseObjectFiles().Count.ShouldEqual( + looseObjectCount, + "unexpected number of loose objects after step"); + + // This step should create a pack. + this.Enlistment.LooseObjectStep(); + + this.CountPackFiles().ShouldEqual(1, "Incorrect number of packs after second loose object step"); + this.GetLooseObjectFiles().Count.ShouldEqual(looseObjectCount); + + // This step should delete the loose objects + this.Enlistment.LooseObjectStep(); + + this.GetLooseObjectFiles().Count.ShouldEqual(0, "Incorrect number of loose objects after third loose object step"); + } + + private void ClearAllObjects() + { + this.Enlistment.UnmountScalar(); + + // Delete/Move any starting loose objects and packfiles + this.DeleteFiles(this.GetLooseObjectFiles()); + this.MovePackFilesToTemp(); + this.GetLooseObjectFiles().Count.ShouldEqual(0, "incorrect number of loose objects after setup"); + this.CountPackFiles().ShouldEqual(0, "incorrect number of packs after setup"); + } + + private List GetLooseObjectFiles() + { + List looseObjectFiles = new List(); + foreach (string directory in Directory.GetDirectories(this.GitObjectRoot)) + { + // Check if the directory is 2 letter HEX + if (Regex.IsMatch(directory, @"[/\\][0-9a-fA-F]{2}$")) + { + string[] files = Directory.GetFiles(directory); + looseObjectFiles.AddRange(files); + } + } + + return looseObjectFiles; + } + + private void DeleteFiles(List filePaths) + { + foreach (string filePath in filePaths) + { + File.Delete(filePath); + } + } + + private int CountPackFiles() + { + return Directory.GetFiles(this.PackRoot, "*.pack").Length; + } + + private void MovePackFilesToTemp() + { + string[] files = Directory.GetFiles(this.PackRoot); + foreach (string file in files) + { + string path2 = Path.Combine(this.TempPackRoot, Path.GetFileName(file)); + + File.Move(file, path2); + } + } + + private void ExpandOneTempPack(bool copyPackBackToPackDirectory) + { + // Find all pack files + string[] packFiles = Directory.GetFiles(this.TempPackRoot, "pack-*.pack"); + Assert.Greater(packFiles.Length, 0); + + // Pick the first one found + string packFile = packFiles[0]; + + // Send the contents of the packfile to unpack-objects to example the loose objects + // Note this won't work if the object exists in a pack file which is why we had to move them + using (FileStream packFileStream = File.OpenRead(packFile)) + { + string output = GitProcess.InvokeProcess( + this.Enlistment.RepoRoot, + "unpack-objects", + new Dictionary() { { "GIT_OBJECT_DIRECTORY", this.GitObjectRoot } }, + inputStream: packFileStream).Output; + } + + if (copyPackBackToPackDirectory) + { + // Copy the pack file back to packs + string packFileName = Path.GetFileName(packFile); + File.Copy(packFile, Path.Combine(this.PackRoot, packFileName)); + + // Replace the '.pack' with '.idx' to copy the index file + string packFileIndexName = packFileName.Replace(".pack", ".idx"); + File.Copy(Path.Combine(this.TempPackRoot, packFileIndexName), Path.Combine(this.PackRoot, packFileIndexName)); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs index 26787cf596..d04677d882 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/RepairTests.cs @@ -1,183 +1,183 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class RepairTests : TestsWithEnlistmentPerTestCase - { - [TestCase] - public void NoFixesNeeded() - { - this.Enlistment.UnmountScalar(); - this.Enlistment.Repair(confirm: false); - this.Enlistment.Repair(confirm: true); - } - - [TestCase] - public void FixesCorruptHeadSha() - { - this.Enlistment.UnmountScalar(); - - string headFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "HEAD"); - File.WriteAllText(headFilePath, "0000"); - this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when HEAD is corrupt"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesCorruptHeadSymRef() - { - this.Enlistment.UnmountScalar(); - - string headFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "HEAD"); - File.WriteAllText(headFilePath, "ref: refs"); - this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when HEAD is corrupt"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesMissingGitIndex() - { - this.Enlistment.UnmountScalar(); - - string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); - File.Delete(gitIndexPath); - this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when git index is missing"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesGitIndexCorruptedWithBadData() - { - this.Enlistment.UnmountScalar(); - - string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); - this.CreateCorruptIndexAndRename( - gitIndexPath, - (current, temp) => - { - byte[] badData = Encoding.ASCII.GetBytes("BAD_INDEX"); - temp.Write(badData, 0, badData.Length); - }); - - string output; - this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); - output.ShouldContain("Index validation failed"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesGitIndexContainingAllNulls() - { - this.Enlistment.UnmountScalar(); - - string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); - - // Set the contents of the index file to gitIndexPath NULL - this.CreateCorruptIndexAndRename( - gitIndexPath, - (current, temp) => - { - temp.Write(Enumerable.Repeat(0, (int)current.Length).ToArray(), 0, (int)current.Length); - }); - - string output; - this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); - output.ShouldContain("Index validation failed"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesGitIndexCorruptedByTruncation() - { - this.Enlistment.UnmountScalar(); - - string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); - - // Truncate the contents of the index - this.CreateCorruptIndexAndRename( - gitIndexPath, - (current, temp) => - { - // 20 will truncate the file in the middle of the first entry in the index - byte[] currentStartOfIndex = new byte[20]; - current.Read(currentStartOfIndex, 0, currentStartOfIndex.Length); - temp.Write(currentStartOfIndex, 0, currentStartOfIndex.Length); - }); - - string output; - this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); - output.ShouldContain("Index validation failed"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.RepairWithConfirmShouldFix(); - } - - [TestCase] - public void FixesCorruptGitConfig() - { - this.Enlistment.UnmountScalar(); - - string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "config"); - File.WriteAllText(gitIndexPath, "[cor"); - - this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when git config is missing"); - - this.RepairWithoutConfirmShouldNotFix(); - - this.Enlistment.Repair(confirm: true); - ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "remote add origin " + this.Enlistment.RepoUrl); - result.ExitCode.ShouldEqual(0, result.Errors); - this.Enlistment.MountScalar(); - } - - private void CreateCorruptIndexAndRename(string indexPath, Action corruptionAction) - { - string tempIndexPath = indexPath + ".lock"; - using (FileStream currentIndexStream = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) - using (FileStream tempIndexStream = new FileStream(tempIndexPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite)) - { - corruptionAction(currentIndexStream, tempIndexStream); - } - - File.Delete(indexPath); - File.Move(tempIndexPath, indexPath); - } - - private void RepairWithConfirmShouldFix() - { - this.Enlistment.Repair(confirm: true); - this.Enlistment.MountScalar(); - } - - private void RepairWithoutConfirmShouldNotFix() - { - this.Enlistment.Repair(confirm: false); - this.Enlistment.TryMountScalar().ShouldEqual(false, "Repair without confirm should not fix the enlistment"); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class RepairTests : TestsWithEnlistmentPerTestCase + { + [TestCase] + public void NoFixesNeeded() + { + this.Enlistment.UnmountScalar(); + this.Enlistment.Repair(confirm: false); + this.Enlistment.Repair(confirm: true); + } + + [TestCase] + public void FixesCorruptHeadSha() + { + this.Enlistment.UnmountScalar(); + + string headFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "HEAD"); + File.WriteAllText(headFilePath, "0000"); + this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when HEAD is corrupt"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesCorruptHeadSymRef() + { + this.Enlistment.UnmountScalar(); + + string headFilePath = Path.Combine(this.Enlistment.RepoRoot, ".git", "HEAD"); + File.WriteAllText(headFilePath, "ref: refs"); + this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when HEAD is corrupt"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesMissingGitIndex() + { + this.Enlistment.UnmountScalar(); + + string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); + File.Delete(gitIndexPath); + this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when git index is missing"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesGitIndexCorruptedWithBadData() + { + this.Enlistment.UnmountScalar(); + + string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); + this.CreateCorruptIndexAndRename( + gitIndexPath, + (current, temp) => + { + byte[] badData = Encoding.ASCII.GetBytes("BAD_INDEX"); + temp.Write(badData, 0, badData.Length); + }); + + string output; + this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); + output.ShouldContain("Index validation failed"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesGitIndexContainingAllNulls() + { + this.Enlistment.UnmountScalar(); + + string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); + + // Set the contents of the index file to gitIndexPath NULL + this.CreateCorruptIndexAndRename( + gitIndexPath, + (current, temp) => + { + temp.Write(Enumerable.Repeat(0, (int)current.Length).ToArray(), 0, (int)current.Length); + }); + + string output; + this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); + output.ShouldContain("Index validation failed"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesGitIndexCorruptedByTruncation() + { + this.Enlistment.UnmountScalar(); + + string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "index"); + + // Truncate the contents of the index + this.CreateCorruptIndexAndRename( + gitIndexPath, + (current, temp) => + { + // 20 will truncate the file in the middle of the first entry in the index + byte[] currentStartOfIndex = new byte[20]; + current.Read(currentStartOfIndex, 0, currentStartOfIndex.Length); + temp.Write(currentStartOfIndex, 0, currentStartOfIndex.Length); + }); + + string output; + this.Enlistment.TryMountScalar(out output).ShouldEqual(false, "Scalar shouldn't mount when index is corrupt"); + output.ShouldContain("Index validation failed"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.RepairWithConfirmShouldFix(); + } + + [TestCase] + public void FixesCorruptGitConfig() + { + this.Enlistment.UnmountScalar(); + + string gitIndexPath = Path.Combine(this.Enlistment.RepoRoot, ".git", "config"); + File.WriteAllText(gitIndexPath, "[cor"); + + this.Enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when git config is missing"); + + this.RepairWithoutConfirmShouldNotFix(); + + this.Enlistment.Repair(confirm: true); + ProcessResult result = GitProcess.InvokeProcess(this.Enlistment.RepoRoot, "remote add origin " + this.Enlistment.RepoUrl); + result.ExitCode.ShouldEqual(0, result.Errors); + this.Enlistment.MountScalar(); + } + + private void CreateCorruptIndexAndRename(string indexPath, Action corruptionAction) + { + string tempIndexPath = indexPath + ".lock"; + using (FileStream currentIndexStream = new FileStream(indexPath, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (FileStream tempIndexStream = new FileStream(tempIndexPath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.ReadWrite)) + { + corruptionAction(currentIndexStream, tempIndexStream); + } + + File.Delete(indexPath); + File.Move(tempIndexPath, indexPath); + } + + private void RepairWithConfirmShouldFix() + { + this.Enlistment.Repair(confirm: true); + this.Enlistment.MountScalar(); + } + + private void RepairWithoutConfirmShouldNotFix() + { + this.Enlistment.Repair(confirm: false); + this.Enlistment.TryMountScalar().ShouldEqual(false, "Repair without confirm should not fix the enlistment"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs index 93fb36a5d5..fd2a224d3f 100644 --- a/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs +++ b/Scalar.FunctionalTests/Tests/EnlistmentPerTestCase/TestsWithEnlistmentPerTestCase.cs @@ -1,43 +1,43 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; - -namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase -{ - [TestFixture] - public abstract class TestsWithEnlistmentPerTestCase - { - private readonly bool forcePerRepoObjectCache; - - public TestsWithEnlistmentPerTestCase(bool forcePerRepoObjectCache = false) - { - this.forcePerRepoObjectCache = forcePerRepoObjectCache; - } - - public ScalarFunctionalTestEnlistment Enlistment - { - get; private set; - } - - [SetUp] - public virtual void CreateEnlistment() - { - if (this.forcePerRepoObjectCache) - { - this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(ScalarTestConfig.PathToScalar, skipPrefetch: false); - } - else - { - this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar); - } - } - - [TearDown] - public virtual void DeleteEnlistment() - { - if (this.Enlistment != null) - { - this.Enlistment.UnmountAndDeleteAll(); - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.EnlistmentPerTestCase +{ + [TestFixture] + public abstract class TestsWithEnlistmentPerTestCase + { + private readonly bool forcePerRepoObjectCache; + + public TestsWithEnlistmentPerTestCase(bool forcePerRepoObjectCache = false) + { + this.forcePerRepoObjectCache = forcePerRepoObjectCache; + } + + public ScalarFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [SetUp] + public virtual void CreateEnlistment() + { + if (this.forcePerRepoObjectCache) + { + this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMountWithPerRepoCache(ScalarTestConfig.PathToScalar, skipPrefetch: false); + } + else + { + this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar); + } + } + + [TearDown] + public virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.UnmountAndDeleteAll(); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs index f8bc1a789a..ce32382cc9 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/AddStageTests.cs @@ -1,70 +1,70 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Tools; +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; using System.IO; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class AddStageTests : GitRepoTests - { - public AddStageTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase, Order(1)] - public void AddBasicTest() - { - this.EditFile("Some new content.", "Readme.md"); - this.ValidateGitCommand("add Readme.md"); - this.RunGitCommand("commit -m \"Changing the Readme.md\""); - } - - [TestCase, Order(2)] - public void StageBasicTest() - { - this.EditFile("Some new content.", "AuthoringTests.md"); - this.ValidateGitCommand("stage AuthoringTests.md"); - this.RunGitCommand("commit -m \"Changing the AuthoringTests.md\""); - } - - [TestCase, Order(3)] - public void AddAndStageHardLinksTest() - { - this.CreateHardLink("ReadmeLink.md", "Readme.md"); - this.ValidateGitCommand("add ReadmeLink.md"); - this.RunGitCommand("commit -m \"Created ReadmeLink.md\""); - - this.CreateHardLink("AuthoringTestsLink.md", "AuthoringTests.md"); - this.ValidateGitCommand("stage AuthoringTestsLink.md"); - this.RunGitCommand("commit -m \"Created AuthoringTestsLink.md\""); - } - - [TestCase, Order(4)] - public void AddAllowsPlaceholderCreation() - { - this.CommandAllowsPlaceholderCreation("add", "Scalar", "Scalar", "Program.cs"); - } - - [TestCase, Order(5)] - public void StageAllowsPlaceholderCreation() - { - this.CommandAllowsPlaceholderCreation("stage", "Scalar", "Scalar", "App.config"); - } - - private void CommandAllowsPlaceholderCreation(string command, params string[] fileToReadPathParts) - { - string fileToRead = Path.Combine(fileToReadPathParts); - this.EditFile($"Some new content for {command}.", "Protocol.md"); - ManualResetEventSlim resetEvent = GitHelpers.RunGitCommandWithWaitAndStdIn(this.Enlistment, resetTimeout: 3000, command: $"{command} -p", stdinToQuit: "q", processId: out _); - this.FileContentsShouldMatch(fileToRead); - this.ValidateGitCommand("--no-optional-locks status"); - resetEvent.Wait(); - this.RunGitCommand("reset --hard"); - } - } -} +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class AddStageTests : GitRepoTests + { + public AddStageTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase, Order(1)] + public void AddBasicTest() + { + this.EditFile("Some new content.", "Readme.md"); + this.ValidateGitCommand("add Readme.md"); + this.RunGitCommand("commit -m \"Changing the Readme.md\""); + } + + [TestCase, Order(2)] + public void StageBasicTest() + { + this.EditFile("Some new content.", "AuthoringTests.md"); + this.ValidateGitCommand("stage AuthoringTests.md"); + this.RunGitCommand("commit -m \"Changing the AuthoringTests.md\""); + } + + [TestCase, Order(3)] + public void AddAndStageHardLinksTest() + { + this.CreateHardLink("ReadmeLink.md", "Readme.md"); + this.ValidateGitCommand("add ReadmeLink.md"); + this.RunGitCommand("commit -m \"Created ReadmeLink.md\""); + + this.CreateHardLink("AuthoringTestsLink.md", "AuthoringTests.md"); + this.ValidateGitCommand("stage AuthoringTestsLink.md"); + this.RunGitCommand("commit -m \"Created AuthoringTestsLink.md\""); + } + + [TestCase, Order(4)] + public void AddAllowsPlaceholderCreation() + { + this.CommandAllowsPlaceholderCreation("add", "Scalar", "Scalar", "Program.cs"); + } + + [TestCase, Order(5)] + public void StageAllowsPlaceholderCreation() + { + this.CommandAllowsPlaceholderCreation("stage", "Scalar", "Scalar", "App.config"); + } + + private void CommandAllowsPlaceholderCreation(string command, params string[] fileToReadPathParts) + { + string fileToRead = Path.Combine(fileToReadPathParts); + this.EditFile($"Some new content for {command}.", "Protocol.md"); + ManualResetEventSlim resetEvent = GitHelpers.RunGitCommandWithWaitAndStdIn(this.Enlistment, resetTimeout: 3000, command: $"{command} -p", stdinToQuit: "q", processId: out _); + this.FileContentsShouldMatch(fileToRead); + this.ValidateGitCommand("--no-optional-locks status"); + resetEvent.Wait(); + this.RunGitCommand("reset --hard"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs index e7389570f3..96548c8ad2 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/CherryPickConflictTests.cs @@ -1,105 +1,105 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class CherryPickConflictTests : GitRepoTests - { - public CherryPickConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void CherryPickConflict() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void CherryPickConflictWithFileReads() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ReadConflictTargetFiles(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void CherryPickConflictWithFileReads2() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ReadConflictTargetFiles(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - this.ValidateGitCommand("cherry-pick --abort"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - } - - [TestCase] - public void CherryPickConflict_ThenAbort() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("cherry-pick --abort"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void CherryPickConflict_ThenSkip() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("cherry-pick --skip"); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void CherryPickConflict_UsingOurs() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("cherry-pick -Xours " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void CherryPickConflict_UsingTheirs() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("cherry-pick -Xtheirs " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void CherryPickNoCommit() - { - this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); - this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); - } - - [TestCase] - public void CherryPickNoCommitReset() - { - this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); - this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset"); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class CherryPickConflictTests : GitRepoTests + { + public CherryPickConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void CherryPickConflict() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflictWithFileReads() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflictWithFileReads2() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + this.ValidateGitCommand("cherry-pick --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + } + + [TestCase] + public void CherryPickConflict_ThenAbort() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("cherry-pick --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void CherryPickConflict_ThenSkip() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("cherry-pick --skip"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflict_UsingOurs() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick -Xours " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickConflict_UsingTheirs() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("cherry-pick -Xtheirs " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void CherryPickNoCommit() + { + this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); + this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); + } + + [TestCase] + public void CherryPickNoCommitReset() + { + this.ValidateGitCommand("checkout 170b13ce1990c53944403a70e93c257061598ae0"); + this.ValidateGitCommand("cherry-pick --no-commit " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs index 86fe016a64..caa3a13880 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/DeleteEmptyFolderTests.cs @@ -1,46 +1,46 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class DeleteEmptyFolderTests : GitRepoTests - { - public DeleteEmptyFolderTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void VerifyResetHardDeletesEmptyFolders() - { - this.SetupFolderDeleteTest(); - - this.RunGitCommand("reset --hard HEAD"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - } - - [TestCase] - public void VerifyCleanDeletesEmptyFolders() - { - this.SetupFolderDeleteTest(); - - this.RunGitCommand("clean -fd"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - } - - private void SetupFolderDeleteTest() - { - this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); - this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); - this.DeleteFile("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m\"Delete only file.\""); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class DeleteEmptyFolderTests : GitRepoTests + { + public DeleteEmptyFolderTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyResetHardDeletesEmptyFolders() + { + this.SetupFolderDeleteTest(); + + this.RunGitCommand("reset --hard HEAD"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + [TestCase] + public void VerifyCleanDeletesEmptyFolders() + { + this.SetupFolderDeleteTest(); + + this.RunGitCommand("clean -fd"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + private void SetupFolderDeleteTest() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.DeleteFile("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m\"Delete only file.\""); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs b/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs index 86b55feb08..f916d4e1b6 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/EnumerationMergeTest.cs @@ -1,32 +1,32 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class EnumerationMergeTest : GitRepoTests - { - // Commit that found GvFlt Bug 12258777: Entries are sometimes skipped during - // enumeration when they don't fit in a user's buffer - private const string EnumerationReproCommitish = "FunctionalTests/20170602"; - - public EnumerationMergeTest(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void ConfirmEnumerationMatches() - { - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - } - - protected override void CreateEnlistment() - { - this.CreateEnlistment(EnumerationReproCommitish); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class EnumerationMergeTest : GitRepoTests + { + // Commit that found GvFlt Bug 12258777: Entries are sometimes skipped during + // enumeration when they don't fit in a user's buffer + private const string EnumerationReproCommitish = "FunctionalTests/20170602"; + + public EnumerationMergeTest(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ConfirmEnumerationMatches() + { + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + } + + protected override void CreateEnlistment() + { + this.CreateEnlistment(EnumerationReproCommitish); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs index f730d5d49b..93b830ae60 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/GitCommandsTests.cs @@ -1,1116 +1,1116 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Runtime.CompilerServices; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class GitCommandsTests : GitRepoTests - { - public const string TopLevelFolderToCreate = "level1"; - private const string EncodingFileFolder = "FilenameEncoding"; - private const string EncodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt"; - private const string ContentWhenEditingFile = "// Adding a comment to the file"; - private const string UnknownTestName = "Unknown"; - private const string SubFolderToCreate = "level2"; - - private static readonly string EditFilePath = Path.Combine("Scalar", "Scalar.Common", "ScalarContext.cs"); - private static readonly string DeleteFilePath = Path.Combine("Scalar", "Scalar", "Program.cs"); - private static readonly string RenameFilePathFrom = Path.Combine("Scalar", "Scalar.Common", "Physical", "FileSystem", "FileProperties.cs"); - private static readonly string RenameFilePathTo = Path.Combine("Scalar", "Scalar.Common", "Physical", "FileSystem", "FileProperties2.cs"); - private static readonly string RenameFolderPathFrom = Path.Combine("Scalar", "Scalar.Common", "PrefetchPacks"); - private static readonly string RenameFolderPathTo = Path.Combine("Scalar", "Scalar.Common", "PrefetchPacksRenamed"); - - public GitCommandsTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void VerifyTestFilesExist() - { - // Sanity checks to ensure that the test files we expect to be in our test repo are present - Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); - Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); - Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.DeleteFilePath).ShouldBeAFile(this.FileSystem); - Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom).ShouldBeAFile(this.FileSystem); - Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFolderPathFrom).ShouldBeADirectory(this.FileSystem); - } - - [TestCase] - public void StatusTest() - { - this.ValidateGitCommand("status"); - } - - [TestCase] - public void StatusShortTest() - { - this.ValidateGitCommand("status -s"); - } - - [TestCase] - public void BranchTest() - { - this.ValidateGitCommand("branch"); - } - - [TestCase] - public void NewBranchTest() - { - this.ValidateGitCommand("branch tests/functional/NewBranchTest"); - this.ValidateGitCommand("branch"); - } - - [TestCase] - public void DeleteBranchTest() - { - this.ValidateGitCommand("branch tests/functional/DeleteBranchTest"); - this.ValidateGitCommand("branch"); - this.ValidateGitCommand("branch -d tests/functional/DeleteBranchTest"); - this.ValidateGitCommand("branch"); - } - - [TestCase] - public void RenameCurrentBranchTest() - { - this.ValidateGitCommand("checkout -b tests/functional/RenameBranchTest"); - this.ValidateGitCommand("branch -m tests/functional/RenameBranchTest2"); - this.ValidateGitCommand("branch"); - } - - [TestCase] - public void UntrackedFileTest() - { - this.BasicCommit(this.CreateFile, addCommand: "add ."); - } - - [TestCase] - public void UntrackedEmptyFileTest() - { - this.BasicCommit(this.CreateEmptyFile, addCommand: "add ."); - } - - [TestCase] - public void UntrackedFileAddAllTest() - { - this.BasicCommit(this.CreateFile, addCommand: "add --all"); - } - - [TestCase] - public void UntrackedEmptyFileAddAllTest() - { - this.BasicCommit(this.CreateEmptyFile, addCommand: "add --all"); - } - - [TestCase] - public void StageUntrackedFileTest() - { - this.BasicCommit(this.CreateFile, addCommand: "stage ."); - } - - [TestCase] - public void StageUntrackedEmptyFileTest() - { - this.BasicCommit(this.CreateEmptyFile, addCommand: "stage ."); - } - - [TestCase] - public void StageUntrackedFileAddAllTest() - { - this.BasicCommit(this.CreateFile, addCommand: "stage --all"); - } - - [TestCase] - public void StageUntrackedEmptyFileAddAllTest() - { - this.BasicCommit(this.CreateEmptyFile, addCommand: "stage --all"); - } - - [TestCase] - public void CheckoutNewBranchTest() - { - this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchTest"); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void CheckoutOrphanBranchTest() - { - this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrphanBranchTest"); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void CreateFileSwitchBranchTest() - { - this.SwitchBranch(fileSystemAction: this.CreateFile); - } - - [TestCase] - public void CreateFileStageChangesSwitchBranchTest() - { - this.StageChangesSwitchBranch(fileSystemAction: this.CreateFile); - } - - [TestCase] - public void CreateFileCommitChangesSwitchBranchTest() - { - this.CommitChangesSwitchBranch(fileSystemAction: this.CreateFile); - } - - [TestCase] - public void CreateFileCommitChangesSwitchBranchSwitchBranchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.CreateFile); - } - - [TestCase] - public void DeleteFileSwitchBranchTest() - { - this.SwitchBranch(fileSystemAction: this.DeleteFile); - } - - [TestCase] - public void DeleteFileStageChangesSwitchBranchTest() - { - this.StageChangesSwitchBranch(fileSystemAction: this.DeleteFile); - } - - [TestCase] - public void DeleteFileCommitChangesSwitchBranchTest() - { - this.CommitChangesSwitchBranch(fileSystemAction: this.DeleteFile); - } - - [TestCase] - public void DeleteFileCommitChangesSwitchBranchSwitchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.DeleteFile); - } - - [TestCase] - public void DeleteFileCommitChangesSwitchBranchSwitchBackDeleteFolderTest() - { - // 663045 - Confirm that folder can be deleted after deleting file then changing - // branches - string deleteFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); - string deleteFilePath = Path.Combine(deleteFolderPath, "bar.txt"); - - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFile(deleteFilePath)); - this.DeleteFolder(deleteFolderPath); - } - - [TestCase] - public void DeleteFolderSwitchBranchTest() - { - this.SwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_DeleteOnClose")); - } - - [TestCase] - public void DeleteFolderStageChangesSwitchBranchTest() - { - this.StageChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_SetDisposition")); - } - - [TestCase] - public void DeleteFolderCommitChangesSwitchBranchTest() - { - this.CommitChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose")); - } - - [TestCase] - public void DeleteFolderCommitChangesSwitchBranchSwitchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_SetDisposition")); - } - - [TestCase] - public void DeleteFilesWithNameAheadOfDot() - { - string folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "1"); - this.FolderShouldExistAndHaveFile(folder, "#test"); - this.DeleteFile(folder, "#test"); - this.FolderShouldExistAndBeEmpty(folder); - - folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "2"); - this.FolderShouldExistAndHaveFile(folder, "$test"); - this.DeleteFile(folder, "$test"); - this.FolderShouldExistAndBeEmpty(folder); - - folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "3"); - this.FolderShouldExistAndHaveFile(folder, ")"); - this.DeleteFile(folder, ")"); - this.FolderShouldExistAndBeEmpty(folder); - - folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "4"); - this.FolderShouldExistAndHaveFile(folder, "+.test"); - this.DeleteFile(folder, "+.test"); - this.FolderShouldExistAndBeEmpty(folder); - - folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "5"); - this.FolderShouldExistAndHaveFile(folder, "-.test"); - this.DeleteFile(folder, "-.test"); - this.FolderShouldExistAndBeEmpty(folder); - - this.ValidateGitCommand("status"); - } - - [TestCase] - public void RenameFilesWithNameAheadOfDot() - { - this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "1", "#test"); - this.MoveFile( - Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#test"), - Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#testRenamed")); - - this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "2", "$test"); - this.MoveFile( - Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$test"), - Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$testRenamed")); - - this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "3", ")"); - this.MoveFile( - Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")"), - Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")Renamed")); - - this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "4", "+.test"); - this.MoveFile( - Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.test"), - Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.testRenamed")); - - this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "5", "-.test"); - this.MoveFile( - Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.test"), - Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.testRenamed")); - - this.ValidateGitCommand("status"); - } - - [TestCase] - public void DeleteFileWithNameAheadOfDotAndSwitchCommits() - { - string fileRelativePath = Path.Combine("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(1).txt"); - this.DeleteFile(fileRelativePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(1).txt"); - this.DeleteFile(fileRelativePath); - this.ValidateGitCommand("status"); - - // 14cf226119766146b1fa5c5aa4cd0896d05f6b63 is the commit prior to creating (1).txt, it has two different files with - // names that start with '(': - // (a).txt - // (z).txt - this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63"); - this.DeleteFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt"); - this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(a).txt"); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() - { - // 663045 - Confirm that folder can be deleted after adding a file then changing branches - string newFileParentFolderPath = Path.Combine("Scalar", "Scalar", "CommandLine"); - string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); - string newFileContents = "test contents"; - - this.CommitChangesSwitchBranch( - fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), - test: "AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.DeleteFolder(newFileParentFolderPath); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout tests/functional/AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - this.FolderShouldExist(newFileParentFolderPath); - this.FileShouldHaveContents(newFileContents, newFilePath); - } - - [TestCase] - public void OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() - { - string overwrittenFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition"); - - // GVFlt_DeleteFolderTest\GVFlt_DeletePlaceholderNonEmptyFolder_SetDispositiontestfile.txt already exists in the repo as TestFile.txt - string fileToOverwritePath = Path.Combine(overwrittenFileParentFolderPath, "testfile.txt"); - string newFileContents = "test contents"; - - this.CommitChangesSwitchBranch( - fileSystemAction: () => this.CreateFile(newFileContents, fileToOverwritePath), - test: "OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.DeleteFolder(overwrittenFileParentFolderPath); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout tests/functional/OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - string subFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition", "NonEmptyFolder"); - this.ShouldNotExistOnDisk(subFolderPath); - this.FolderShouldExist(overwrittenFileParentFolderPath); - this.FileShouldHaveContents(newFileContents, fileToOverwritePath); - } - - [TestCase] - public void AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() - { - // 663045 - Confirm that grandparent folder can be deleted after adding a (granchild) file - // then changing branches - string newFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); - string newFileGrandParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose"); - string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); - string newFileContents = "test contents"; - - this.CommitChangesSwitchBranch( - fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), - test: "AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.DeleteFolder(newFileGrandParentFolderPath); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout tests/functional/AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); - - this.FolderShouldExist(newFileParentFolderPath); - this.FolderShouldExist(newFileGrandParentFolderPath); - this.FileShouldHaveContents(newFileContents, newFilePath); - } - - [TestCase] - public void CommitWithNewlinesInMessage() - { - this.ValidateGitCommand("checkout -b tests/functional/commit_with_uncommon_arguments"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Message that contains \na\nnew\nline\""); - } - - [TestCase] - public void CaseOnlyRenameFileAndChangeBranches() - { - // 693190 - Confirm that file does not disappear after case-only rename and branch - // changes - string newBranchName = "tests/functional/CaseOnlyRenameFileAndChangeBranches"; - string oldFileName = "Readme.md"; - string newFileName = "README.md"; - - this.ValidateGitCommand("checkout -b " + newBranchName); - this.ValidateGitCommand("mv {0} {1}", oldFileName, newFileName); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for CaseOnlyRenameFileAndChangeBranches\""); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.FileShouldHaveCaseMatchingName(oldFileName); - - this.ValidateGitCommand("checkout " + newBranchName); - this.FileShouldHaveCaseMatchingName(newFileName); - } - - [TestCase] - public void MoveFileFromOutsideRepoToInsideRepoAndAdd() - { - string testFileContents = "0123456789"; - string filename = "MoveFileFromOutsideRepoToInsideRepo.cs"; - - // Create the test files in this.Enlistment.EnlistmentRoot as it's outside of src and the control - // repo and is cleaned up when the functional tests run - string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); - string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filename); - string scalarFilePath = Path.Combine(this.Enlistment.RepoRoot, filename); - - string newBranchName = "tests/functional/MoveFileFromOutsideRepoToInsideRepoAndAdd"; - this.ValidateGitCommand("checkout -b " + newBranchName); - - // Move file to control repo - this.FileSystem.WriteAllText(oldFilePath, testFileContents); - this.FileSystem.MoveFile(oldFilePath, controlFilePath); - oldFilePath.ShouldNotExistOnDisk(this.FileSystem); - controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); - - // Move file to Scalar repo - this.FileSystem.WriteAllText(oldFilePath, testFileContents); - this.FileSystem.MoveFile(oldFilePath, scalarFilePath); - oldFilePath.ShouldNotExistOnDisk(this.FileSystem); - scalarFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); - - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for MoveFileFromOutsideRepoToInsideRepoAndAdd\""); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - } - - [TestCase] - public void MoveFolderFromOutsideRepoToInsideRepoAndAdd() - { - string testFileContents = "0123456789"; - string filename = "MoveFolderFromOutsideRepoToInsideRepoAndAdd.cs"; - string folderName = "GitCommand_MoveFolderFromOutsideRepoToInsideRepoAndAdd"; - - // Create the test folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control - // repo and is cleaned up when the functional tests run - string oldFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); - string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName, filename); - string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, folderName); - string scalarFolderPath = Path.Combine(this.Enlistment.RepoRoot, folderName); - - string newBranchName = "tests/functional/MoveFolderFromOutsideRepoToInsideRepoAndAdd"; - this.ValidateGitCommand("checkout -b " + newBranchName); - - // Move folder to control repo - this.FileSystem.CreateDirectory(oldFolderPath); - this.FileSystem.WriteAllText(oldFilePath, testFileContents); - this.FileSystem.MoveDirectory(oldFolderPath, controlFolderPath); - oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); - Path.Combine(controlFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); - - // Move folder to Scalar repo - this.FileSystem.CreateDirectory(oldFolderPath); - this.FileSystem.WriteAllText(oldFilePath, testFileContents); - this.FileSystem.MoveDirectory(oldFolderPath, scalarFolderPath); - oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); - Path.Combine(scalarFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); - - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for MoveFolderFromOutsideRepoToInsideRepoAndAdd\""); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - } - - [TestCase] - public void MoveFileFromInsideRepoToOutsideRepoAndCommit() - { - string newBranchName = "tests/functional/MoveFileFromInsideRepoToOutsideRepoAndCommit"; - this.ValidateGitCommand("checkout -b " + newBranchName); - - string fileName = "Protocol.md"; - string controlTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ControlTarget"; - string scalarTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ScalarTarget"; - - // Create the target folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control repo - // and is cleaned up when the functional tests run - string controlTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, controlTargetFolder); - string scalarTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, scalarTargetFolder); - string controlTargetFilePath = Path.Combine(controlTargetFolderPath, fileName); - string scalarTargetFilePath = Path.Combine(scalarTargetFolderPath, fileName); - - // Move control repo file - this.FileSystem.CreateDirectory(controlTargetFolderPath); - this.FileSystem.MoveFile(Path.Combine(this.ControlGitRepo.RootPath, fileName), controlTargetFilePath); - controlTargetFilePath.ShouldBeAFile(this.FileSystem); - - // Move Scalar repo file - this.FileSystem.CreateDirectory(scalarTargetFolderPath); - this.FileSystem.MoveFile(Path.Combine(this.Enlistment.RepoRoot, fileName), scalarTargetFilePath); - scalarTargetFilePath.ShouldBeAFile(this.FileSystem); - - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for MoveFileFromInsideRepoToOutsideRepoAndCommit\""); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - } - - [TestCase] - public void EditFileSwitchBranchTest() - { - this.SwitchBranch(fileSystemAction: this.EditFile); - } - - [TestCase] - public void EditFileStageChangesSwitchBranchTest() - { - this.StageChangesSwitchBranch(fileSystemAction: this.EditFile); - } - - [TestCase] - public void EditFileCommitChangesSwitchBranchTest() - { - this.CommitChangesSwitchBranch(fileSystemAction: this.EditFile); - } - - [TestCase] - public void EditFileCommitChangesSwitchBranchSwitchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.EditFile); - } - - [TestCase] - public void RenameFileCommitChangesSwitchBranchSwitchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.RenameFile); - } - - // MacOnly because renames of partial folders are blocked on Windows - [TestCase] - [Category(Categories.MacOnly)] - public void MoveFolderCommitChangesSwitchBranchSwitchBackTest() - { - this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.MoveFolder); - } +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Runtime.CompilerServices; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class GitCommandsTests : GitRepoTests + { + public const string TopLevelFolderToCreate = "level1"; + private const string EncodingFileFolder = "FilenameEncoding"; + private const string EncodingFilename = "ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك.txt"; + private const string ContentWhenEditingFile = "// Adding a comment to the file"; + private const string UnknownTestName = "Unknown"; + private const string SubFolderToCreate = "level2"; + + private static readonly string EditFilePath = Path.Combine("Scalar", "Scalar.Common", "ScalarContext.cs"); + private static readonly string DeleteFilePath = Path.Combine("Scalar", "Scalar", "Program.cs"); + private static readonly string RenameFilePathFrom = Path.Combine("Scalar", "Scalar.Common", "Physical", "FileSystem", "FileProperties.cs"); + private static readonly string RenameFilePathTo = Path.Combine("Scalar", "Scalar.Common", "Physical", "FileSystem", "FileProperties2.cs"); + private static readonly string RenameFolderPathFrom = Path.Combine("Scalar", "Scalar.Common", "PrefetchPacks"); + private static readonly string RenameFolderPathTo = Path.Combine("Scalar", "Scalar.Common", "PrefetchPacksRenamed"); + + public GitCommandsTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: false, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyTestFilesExist() + { + // Sanity checks to ensure that the test files we expect to be in our test repo are present + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.DeleteFilePath).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom).ShouldBeAFile(this.FileSystem); + Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFolderPathFrom).ShouldBeADirectory(this.FileSystem); + } + + [TestCase] + public void StatusTest() + { + this.ValidateGitCommand("status"); + } + + [TestCase] + public void StatusShortTest() + { + this.ValidateGitCommand("status -s"); + } + + [TestCase] + public void BranchTest() + { + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void NewBranchTest() + { + this.ValidateGitCommand("branch tests/functional/NewBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void DeleteBranchTest() + { + this.ValidateGitCommand("branch tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + this.ValidateGitCommand("branch -d tests/functional/DeleteBranchTest"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void RenameCurrentBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/RenameBranchTest"); + this.ValidateGitCommand("branch -m tests/functional/RenameBranchTest2"); + this.ValidateGitCommand("branch"); + } + + [TestCase] + public void UntrackedFileTest() + { + this.BasicCommit(this.CreateFile, addCommand: "add ."); + } + + [TestCase] + public void UntrackedEmptyFileTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "add ."); + } + + [TestCase] + public void UntrackedFileAddAllTest() + { + this.BasicCommit(this.CreateFile, addCommand: "add --all"); + } + + [TestCase] + public void UntrackedEmptyFileAddAllTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "add --all"); + } + + [TestCase] + public void StageUntrackedFileTest() + { + this.BasicCommit(this.CreateFile, addCommand: "stage ."); + } + + [TestCase] + public void StageUntrackedEmptyFileTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "stage ."); + } + + [TestCase] + public void StageUntrackedFileAddAllTest() + { + this.BasicCommit(this.CreateFile, addCommand: "stage --all"); + } + + [TestCase] + public void StageUntrackedEmptyFileAddAllTest() + { + this.BasicCommit(this.CreateEmptyFile, addCommand: "stage --all"); + } + + [TestCase] + public void CheckoutNewBranchTest() + { + this.ValidateGitCommand("checkout -b tests/functional/CheckoutNewBranchTest"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CheckoutOrphanBranchTest() + { + this.ValidateGitCommand("checkout --orphan tests/functional/CheckoutOrphanBranchTest"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CreateFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void CreateFileCommitChangesSwitchBranchSwitchBranchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.CreateFile); + } + + [TestCase] + public void DeleteFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.DeleteFile); + } + + [TestCase] + public void DeleteFileCommitChangesSwitchBranchSwitchBackDeleteFolderTest() + { + // 663045 - Confirm that folder can be deleted after deleting file then changing + // branches + string deleteFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); + string deleteFilePath = Path.Combine(deleteFolderPath, "bar.txt"); + + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFile(deleteFilePath)); + this.DeleteFolder(deleteFolderPath); + } + + [TestCase] + public void DeleteFolderSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_DeleteOnClose")); + } + + [TestCase] + public void DeleteFolderStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteLocalEmptyFolder_SetDisposition")); + } + + [TestCase] + public void DeleteFolderCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_DeleteOnClose")); + } + + [TestCase] + public void DeleteFolderCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: () => this.DeleteFolder("GVFlt_DeleteFolderTest", "GVFlt_DeleteNonRootVirtualFolder_SetDisposition")); + } + + [TestCase] + public void DeleteFilesWithNameAheadOfDot() + { + string folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "1"); + this.FolderShouldExistAndHaveFile(folder, "#test"); + this.DeleteFile(folder, "#test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "2"); + this.FolderShouldExistAndHaveFile(folder, "$test"); + this.DeleteFile(folder, "$test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "3"); + this.FolderShouldExistAndHaveFile(folder, ")"); + this.DeleteFile(folder, ")"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "4"); + this.FolderShouldExistAndHaveFile(folder, "+.test"); + this.DeleteFile(folder, "+.test"); + this.FolderShouldExistAndBeEmpty(folder); + + folder = Path.Combine("GitCommandsTests", "DeleteFileTests", "5"); + this.FolderShouldExistAndHaveFile(folder, "-.test"); + this.DeleteFile(folder, "-.test"); + this.FolderShouldExistAndBeEmpty(folder); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void RenameFilesWithNameAheadOfDot() + { + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "1", "#test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "1", "#testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "2", "$test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "2", "$testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "3", ")"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")"), + Path.Combine("GitCommandsTests", "RenameFileTests", "3", ")Renamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "4", "+.test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "4", "+.testRenamed")); + + this.FolderShouldExistAndHaveFile("GitCommandsTests", "RenameFileTests", "5", "-.test"); + this.MoveFile( + Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.test"), + Path.Combine("GitCommandsTests", "RenameFileTests", "5", "-.testRenamed")); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void DeleteFileWithNameAheadOfDotAndSwitchCommits() + { + string fileRelativePath = Path.Combine("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(1).txt"); + this.DeleteFile(fileRelativePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(1).txt"); + this.DeleteFile(fileRelativePath); + this.ValidateGitCommand("status"); + + // 14cf226119766146b1fa5c5aa4cd0896d05f6b63 is the commit prior to creating (1).txt, it has two different files with + // names that start with '(': + // (a).txt + // (z).txt + this.ValidateGitCommand("checkout 14cf226119766146b1fa5c5aa4cd0896d05f6b63"); + this.DeleteFile("DeleteFileWithNameAheadOfDotAndSwitchCommits", "(a).txt"); + this.ValidateGitCommand("checkout -- DeleteFileWithNameAheadOfDotAndSwitchCommits/(a).txt"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that folder can be deleted after adding a file then changing branches + string newFileParentFolderPath = Path.Combine("Scalar", "Scalar", "CommandLine"); + string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), + test: "AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileParentFolderPath); + this.FileShouldHaveContents(newFileContents, newFilePath); + } + + [TestCase] + public void OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + string overwrittenFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition"); + + // GVFlt_DeleteFolderTest\GVFlt_DeletePlaceholderNonEmptyFolder_SetDispositiontestfile.txt already exists in the repo as TestFile.txt + string fileToOverwritePath = Path.Combine(overwrittenFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, fileToOverwritePath), + test: "OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(overwrittenFileParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/OverwriteFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + string subFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeletePlaceholderNonEmptyFolder_SetDisposition", "NonEmptyFolder"); + this.ShouldNotExistOnDisk(subFolderPath); + this.FolderShouldExist(overwrittenFileParentFolderPath); + this.FileShouldHaveContents(newFileContents, fileToOverwritePath); + } + + [TestCase] + public void AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack() + { + // 663045 - Confirm that grandparent folder can be deleted after adding a (granchild) file + // then changing branches + string newFileParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose", "NonEmptyFolder"); + string newFileGrandParentFolderPath = Path.Combine("GVFlt_DeleteFolderTest", "GVFlt_DeleteVirtualNonEmptyFolder_DeleteOnClose"); + string newFilePath = Path.Combine(newFileParentFolderPath, "testfile.txt"); + string newFileContents = "test contents"; + + this.CommitChangesSwitchBranch( + fileSystemAction: () => this.CreateFile(newFileContents, newFilePath), + test: "AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.DeleteFolder(newFileGrandParentFolderPath); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout tests/functional/AddFileInSubfolderAndCommitOnNewBranchSwitchDeleteFolderAndSwitchBack"); + + this.FolderShouldExist(newFileParentFolderPath); + this.FolderShouldExist(newFileGrandParentFolderPath); + this.FileShouldHaveContents(newFileContents, newFilePath); + } + + [TestCase] + public void CommitWithNewlinesInMessage() + { + this.ValidateGitCommand("checkout -b tests/functional/commit_with_uncommon_arguments"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Message that contains \na\nnew\nline\""); + } + + [TestCase] + public void CaseOnlyRenameFileAndChangeBranches() + { + // 693190 - Confirm that file does not disappear after case-only rename and branch + // changes + string newBranchName = "tests/functional/CaseOnlyRenameFileAndChangeBranches"; + string oldFileName = "Readme.md"; + string newFileName = "README.md"; + + this.ValidateGitCommand("checkout -b " + newBranchName); + this.ValidateGitCommand("mv {0} {1}", oldFileName, newFileName); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for CaseOnlyRenameFileAndChangeBranches\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.FileShouldHaveCaseMatchingName(oldFileName); + + this.ValidateGitCommand("checkout " + newBranchName); + this.FileShouldHaveCaseMatchingName(newFileName); + } + + [TestCase] + public void MoveFileFromOutsideRepoToInsideRepoAndAdd() + { + string testFileContents = "0123456789"; + string filename = "MoveFileFromOutsideRepoToInsideRepo.cs"; + + // Create the test files in this.Enlistment.EnlistmentRoot as it's outside of src and the control + // repo and is cleaned up when the functional tests run + string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, filename); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filename); + string scalarFilePath = Path.Combine(this.Enlistment.RepoRoot, filename); + + string newBranchName = "tests/functional/MoveFileFromOutsideRepoToInsideRepoAndAdd"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + // Move file to control repo + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveFile(oldFilePath, controlFilePath); + oldFilePath.ShouldNotExistOnDisk(this.FileSystem); + controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + // Move file to Scalar repo + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveFile(oldFilePath, scalarFilePath); + oldFilePath.ShouldNotExistOnDisk(this.FileSystem); + scalarFilePath.ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for MoveFileFromOutsideRepoToInsideRepoAndAdd\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void MoveFolderFromOutsideRepoToInsideRepoAndAdd() + { + string testFileContents = "0123456789"; + string filename = "MoveFolderFromOutsideRepoToInsideRepoAndAdd.cs"; + string folderName = "GitCommand_MoveFolderFromOutsideRepoToInsideRepoAndAdd"; + + // Create the test folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control + // repo and is cleaned up when the functional tests run + string oldFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName); + string oldFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, folderName, filename); + string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, folderName); + string scalarFolderPath = Path.Combine(this.Enlistment.RepoRoot, folderName); + + string newBranchName = "tests/functional/MoveFolderFromOutsideRepoToInsideRepoAndAdd"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + // Move folder to control repo + this.FileSystem.CreateDirectory(oldFolderPath); + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveDirectory(oldFolderPath, controlFolderPath); + oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); + Path.Combine(controlFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + // Move folder to Scalar repo + this.FileSystem.CreateDirectory(oldFolderPath); + this.FileSystem.WriteAllText(oldFilePath, testFileContents); + this.FileSystem.MoveDirectory(oldFolderPath, scalarFolderPath); + oldFolderPath.ShouldNotExistOnDisk(this.FileSystem); + Path.Combine(scalarFolderPath, filename).ShouldBeAFile(this.FileSystem).WithContents(testFileContents); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for MoveFolderFromOutsideRepoToInsideRepoAndAdd\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void MoveFileFromInsideRepoToOutsideRepoAndCommit() + { + string newBranchName = "tests/functional/MoveFileFromInsideRepoToOutsideRepoAndCommit"; + this.ValidateGitCommand("checkout -b " + newBranchName); + + string fileName = "Protocol.md"; + string controlTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ControlTarget"; + string scalarTargetFolder = "MoveFileFromInsideRepoToOutsideRepoAndCommit_ScalarTarget"; + + // Create the target folders in this.Enlistment.EnlistmentRoot as it's outside of src and the control repo + // and is cleaned up when the functional tests run + string controlTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, controlTargetFolder); + string scalarTargetFolderPath = Path.Combine(this.Enlistment.EnlistmentRoot, scalarTargetFolder); + string controlTargetFilePath = Path.Combine(controlTargetFolderPath, fileName); + string scalarTargetFilePath = Path.Combine(scalarTargetFolderPath, fileName); + + // Move control repo file + this.FileSystem.CreateDirectory(controlTargetFolderPath); + this.FileSystem.MoveFile(Path.Combine(this.ControlGitRepo.RootPath, fileName), controlTargetFilePath); + controlTargetFilePath.ShouldBeAFile(this.FileSystem); + + // Move Scalar repo file + this.FileSystem.CreateDirectory(scalarTargetFolderPath); + this.FileSystem.MoveFile(Path.Combine(this.Enlistment.RepoRoot, fileName), scalarTargetFilePath); + scalarTargetFilePath.ShouldBeAFile(this.FileSystem); + + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for MoveFileFromInsideRepoToOutsideRepoAndCommit\""); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + } + + [TestCase] + public void EditFileSwitchBranchTest() + { + this.SwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileStageChangesSwitchBranchTest() + { + this.StageChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchTest() + { + this.CommitChangesSwitchBranch(fileSystemAction: this.EditFile); + } + + [TestCase] + public void EditFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.EditFile); + } + + [TestCase] + public void RenameFileCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.RenameFile); + } + + // MacOnly because renames of partial folders are blocked on Windows + [TestCase] + [Category(Categories.MacOnly)] + public void MoveFolderCommitChangesSwitchBranchSwitchBackTest() + { + this.CommitChangesSwitchBranchSwitchBack(fileSystemAction: this.MoveFolder); + } // MacOnly because Windows does not support file mode - [TestCase] + [TestCase] [Category(Categories.MacOnly)] - public void UpdateFileModeOnly() - { - const string TestFileName = "test-file-mode"; - this.CreateFile("#!/bin/bash\n", TestFileName); - this.ChangeMode(TestFileName, Convert.ToUInt16("755", 8)); - this.ValidateGitCommand($"add {TestFileName}"); - this.ValidateGitCommand($"ls-files --stage {TestFileName}"); - } - - [TestCase] - public void AddFileCommitThenDeleteAndCommit() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_before"); - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_after"); - string filePath = Path.Combine("Scalar", "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.DeleteFile(filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Delete file for AddFileCommitThenDeleteAndCommit\""); - this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_before"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_after"); - } - - [TestCase] - public void AddFileCommitThenDeleteAndResetSoft() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string filePath = Path.Combine("Scalar", "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.DeleteFile(filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --soft HEAD~1"); - } - - [TestCase] - public void AddFileCommitThenDeleteAndResetMixed() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string filePath = Path.Combine("Scalar", "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.DeleteFile(filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --soft HEAD~1"); - } - - [TestCase] - public void AddFolderAndFileCommitThenDeleteAndResetSoft() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string folderPath = "test_folder"; - this.CreateFolder(folderPath); - string filePath = Path.Combine(folderPath, "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.DeleteFile(filePath); - this.DeleteFolder(folderPath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --soft HEAD~1"); - } - - [TestCase] - public void AddFolderAndFileCommitThenDeleteAndResetMixed() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string folderPath = "test_folder"; - this.CreateFolder(folderPath); - string filePath = Path.Combine(folderPath, "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.DeleteFile(filePath); - this.DeleteFolder(folderPath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --mixed HEAD~1"); - } - - [TestCase] - public void AddFolderAndFileCommitThenResetSoftAndResetHard() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string folderPath = "test_folder"; - this.CreateFolder(folderPath); - string filePath = Path.Combine(folderPath, "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void AddFolderAndFileCommitThenResetSoftAndResetMixed() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); - string folderPath = "test_folder"; - this.CreateFolder(folderPath); - string filePath = Path.Combine(folderPath, "testfile.txt"); - this.CreateFile("Some new content for the file", filePath); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.ValidateGitCommand("reset --mixed HEAD"); - } - - [TestCase] - public void AddFoldersAndFilesAndRenameFolder() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFoldersAndFilesAndRenameFolder"); - - string topMostNewFolder = "AddFoldersAndFilesAndRenameFolder_Test"; - this.CreateFolder(topMostNewFolder); - this.CreateFile("test contents", topMostNewFolder, "top_level_test_file.txt"); - - string testFolderLevel1 = Path.Combine(topMostNewFolder, "TestFolderLevel1"); - this.CreateFolder(testFolderLevel1); - this.CreateFile("test contents", testFolderLevel1, "level_1_test_file.txt"); - - string testFolderLevel2 = Path.Combine(testFolderLevel1, "TestFolderLevel2"); - this.CreateFolder(testFolderLevel2); - this.CreateFile("test contents", testFolderLevel2, "level_2_test_file.txt"); - - string testFolderLevel3 = Path.Combine(testFolderLevel2, "TestFolderLevel3"); - this.CreateFolder(testFolderLevel3); - this.CreateFile("test contents", testFolderLevel3, "level_3_test_file.txt"); - this.ValidateGitCommand("status"); - - this.MoveFolder(testFolderLevel3, Path.Combine(testFolderLevel2, "TestFolderLevel3Renamed")); - this.ValidateGitCommand("status"); - - this.MoveFolder(testFolderLevel2, Path.Combine(testFolderLevel1, "TestFolderLevel2Renamed")); - this.ValidateGitCommand("status"); - - this.MoveFolder(testFolderLevel1, Path.Combine(topMostNewFolder, "TestFolderLevel1Renamed")); - this.ValidateGitCommand("status"); - - this.MoveFolder(topMostNewFolder, "AddFoldersAndFilesAndRenameFolder_TestRenamed"); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void AddFileAfterFolderRename() - { - this.ValidateGitCommand("checkout -b tests/functional/AddFileAfterFolderRename"); - - string folder = "AddFileAfterFolderRename_Test"; - string renamedFolder = "AddFileAfterFolderRename_TestRenamed"; - this.CreateFolder(folder); - this.MoveFolder(folder, renamedFolder); - this.CreateFile("test contents", renamedFolder, "test_file.txt"); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void ResetSoft() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetSoft"); - this.ValidateGitCommand("reset --soft HEAD~1"); - } - - [TestCase] - public void ResetMixed() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetMixed"); - this.ValidateGitCommand("reset --mixed HEAD~1"); - } - - [TestCase] - public void ResetMixed2() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2"); - this.ValidateGitCommand("reset HEAD~1"); - } - - [TestCase] - public void ManuallyModifyHead() - { - this.ValidateGitCommand("status"); - this.ReplaceText("f1bce402a7a980a8320f3f235cf8c8fdade4b17a", TestConstants.DotGit.Head); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void ResetSoftTwice() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetSoftTwice"); - - // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and - // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 - this.ValidateGitCommand("reset --soft 60d19c87328120d11618ad563c396044a50985b2"); - this.ValidateGitCommand("reset --soft 99fc72275f950b0052c8548bbcf83a851f2b4467"); - } - - [TestCase] - public void ResetMixedTwice() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice"); - - // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and - // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 - this.ValidateGitCommand("reset --mixed 60d19c87328120d11618ad563c396044a50985b2"); - this.ValidateGitCommand("reset --mixed 99fc72275f950b0052c8548bbcf83a851f2b4467"); - } - - [TestCase] - public void ResetMixed2Twice() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2Twice"); - - // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and - // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 - this.ValidateGitCommand("reset 60d19c87328120d11618ad563c396044a50985b2"); - this.ValidateGitCommand("reset 99fc72275f950b0052c8548bbcf83a851f2b4467"); - } - - [TestCase] - public void ResetHardAfterCreate() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreate"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ResetHardAfterEdit() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEdit"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ResetHardAfterDelete() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDelete"); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ResetHardAfterCreateAndAdd() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreateAndAdd"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ResetHardAfterEditAndAdd() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEditAndAdd"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ResetHardAfterDeleteAndAdd() - { - this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDeleteAndAdd"); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.ValidateGitCommand("reset --hard HEAD"); - } - - [TestCase] - public void ChangeTwoBranchesAndMerge() - { - this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_1"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge first branch\""); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_2"); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge second branch\""); - this.ValidateGitCommand("merge tests/functional/ChangeTwoBranchesAndMerge_1"); - } - - [TestCase] - public void ChangeBranchAndCherryPickIntoAnotherBranch() - { - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_1"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Create for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Delete for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); - this.ValidateGitCommand("tag DeleteForCherryPick"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Edit for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_2"); - this.RunGitCommand("cherry-pick DeleteForCherryPick"); - } - - [TestCase] - public void ChangeBranchAndMergeRebaseOnAnotherBranch() - { - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Create for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Delete for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_2"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Edit for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); - - this.RunGitCommand("rebase --merge tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); - } - - [TestCase] - public void ChangeBranchAndRebaseOnAnotherBranch() - { - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); - this.CreateFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Create for ChangeBranchAndRebaseOnAnotherBranch first branch\""); - this.DeleteFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Delete for ChangeBranchAndRebaseOnAnotherBranch first branch\""); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_2"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Edit for ChangeBranchAndRebaseOnAnotherBranch first branch\""); - - this.ValidateGitCommand("rebase tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); - } - - [TestCase] - public void StashChanges() - { - this.ValidateGitCommand("checkout -b tests/functional/StashChanges"); - this.EditFile(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.ValidateGitCommand("stash"); - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.ValidateGitCommand("checkout -b tests/functional/StashChanges_2"); - this.RunGitCommand("stash pop"); - } - - [TestCase] - public void OpenFileThenCheckout() - { - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.EditFilePath); - - // Open files with ReadWrite sharing because depending on the state of the index (and the mtimes), git might need to read the file - // as part of status (while we have the handle open). - using (FileStream virtualFS = File.Open(virtualFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) - using (StreamWriter virtualWriter = new StreamWriter(virtualFS)) - using (FileStream controlFS = File.Open(controlFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) - using (StreamWriter controlWriter = new StreamWriter(controlFS)) - { - this.ValidateGitCommand("checkout -b tests/functional/OpenFileThenCheckout"); - virtualWriter.WriteLine("// Adding a line for testing purposes"); - controlWriter.WriteLine("// Adding a line for testing purposes"); - this.ValidateGitCommand("status"); - } - - // NOTE: Due to optimizations in checkout -b, the modified files will not be included as part of the - // success message. Validate that the succcess messages match, and the call to validate "status" below - // will ensure that Scalar is still reporting the edited file as modified. - - string controlRepoRoot = this.ControlGitRepo.RootPath; - string scalarRepoRoot = this.Enlistment.RepoRoot; - string command = "checkout -b tests/functional/OpenFileThenCheckout_2"; - ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); - ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); - GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult); - actualResult.Errors.ShouldContain("Switched to a new branch"); - - this.ValidateGitCommand("status"); - } - - [TestCase] - public void EditFileNeedingUtf8Encoding() - { - this.ValidateGitCommand("checkout -b tests/functional/EditFileNeedingUtf8Encoding"); - this.ValidateGitCommand("status"); - - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, EncodingFileFolder, EncodingFilename); - - string contents = virtualFile.ShouldBeAFile(this.FileSystem).WithContents(); - string expectedContents = controlFile.ShouldBeAFile(this.FileSystem).WithContents(); - contents.ShouldEqual(expectedContents); - - this.ValidateGitCommand("status"); - - this.AppendAllText(ContentWhenEditingFile, virtualFile); - this.AppendAllText(ContentWhenEditingFile, controlFile); - - this.ValidateGitCommand("status"); - } - - [TestCase] - public void UseAlias() - { - this.ValidateGitCommand("config --local alias.potato status"); - this.ValidateGitCommand("potato"); - } - - [TestCase] - public void RenameOnlyFileInFolder() - { - this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); - this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeSource"); - - this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); - this.FileSystem.ReadAllText(this.Enlistment.GetVirtualPathTo("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt")); - this.ValidateGitCommand("merge origin/FunctionalTests/20170202_RenameTestMergeSource"); - } - - [TestCase] - public void BlameTest() - { - this.ValidateGitCommand("blame Readme.md"); - } - - private void BasicCommit(Action fileSystemAction, string addCommand, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) - { - this.ValidateGitCommand($"checkout -b tests/functional/{test}"); - fileSystemAction(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand(addCommand); - this.RunGitCommand($"commit -m \"BasicCommit for {test}\""); - } - - private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) - { - this.ValidateGitCommand("checkout -b tests/functional/{0}", test); - fileSystemAction(); - this.ValidateGitCommand("status"); - } - - private void StageChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) - { - this.ValidateGitCommand("checkout -b tests/functional/{0}", test); - fileSystemAction(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - } - - private void CommitChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) - { - this.ValidateGitCommand("checkout -b tests/functional/{0}", test); - fileSystemAction(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for {0}\"", test); - } - - private void CommitChangesSwitchBranchSwitchBack(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) - { - string branch = string.Format("tests/functional/{0}", test); - this.ValidateGitCommand("checkout -b {0}", branch); - fileSystemAction(); - this.ValidateGitCommand("status"); - this.ValidateGitCommand("add ."); - this.RunGitCommand("commit -m \"Change for {0}\"", branch); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - - this.ValidateGitCommand("checkout {0}", branch); - } - - private void CreateFile() - { - this.CreateFile("Some content here", Path.GetRandomFileName() + "tempFile.txt"); - this.CreateFolder(TopLevelFolderToCreate); - this.CreateFolder(Path.Combine(TopLevelFolderToCreate, SubFolderToCreate)); - this.CreateFile("File in new folder", Path.Combine(TopLevelFolderToCreate, SubFolderToCreate, Path.GetRandomFileName() + "folderFile.txt")); - } - - private void EditFile() - { - this.AppendAllText(ContentWhenEditingFile, GitCommandsTests.EditFilePath); - } - - private void DeleteFile() - { - this.DeleteFile(GitCommandsTests.DeleteFilePath); - } - - private void RenameFile() - { - string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom); - string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathTo); - string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathFrom); - string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathTo); - this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); - this.FileSystem.MoveFile(controlFileFrom, controlFileTo); - virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); - controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); - } - - private void MoveFolder() - { - this.MoveFolder(GitCommandsTests.RenameFolderPathFrom, GitCommandsTests.RenameFolderPathTo); - } - } -} + public void UpdateFileModeOnly() + { + const string TestFileName = "test-file-mode"; + this.CreateFile("#!/bin/bash\n", TestFileName); + this.ChangeMode(TestFileName, Convert.ToUInt16("755", 8)); + this.ValidateGitCommand($"add {TestFileName}"); + this.ValidateGitCommand($"ls-files --stage {TestFileName}"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndCommit() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndCommit_after"); + string filePath = Path.Combine("Scalar", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete file for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_before"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + this.ValidateGitCommand("checkout tests/functional/AddFileCommitThenDeleteAndCommit_after"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = Path.Combine("Scalar", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string filePath = Path.Combine("Scalar", "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenDeleteAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.DeleteFile(filePath); + this.DeleteFolder(folderPath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetHard() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void AddFolderAndFileCommitThenResetSoftAndResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileCommitThenDeleteAndResetSoft"); + string folderPath = "test_folder"; + this.CreateFolder(folderPath); + string filePath = Path.Combine(folderPath, "testfile.txt"); + this.CreateFile("Some new content for the file", filePath); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for AddFileCommitThenDeleteAndCommit\""); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset --mixed HEAD"); + } + + [TestCase] + public void AddFoldersAndFilesAndRenameFolder() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFoldersAndFilesAndRenameFolder"); + + string topMostNewFolder = "AddFoldersAndFilesAndRenameFolder_Test"; + this.CreateFolder(topMostNewFolder); + this.CreateFile("test contents", topMostNewFolder, "top_level_test_file.txt"); + + string testFolderLevel1 = Path.Combine(topMostNewFolder, "TestFolderLevel1"); + this.CreateFolder(testFolderLevel1); + this.CreateFile("test contents", testFolderLevel1, "level_1_test_file.txt"); + + string testFolderLevel2 = Path.Combine(testFolderLevel1, "TestFolderLevel2"); + this.CreateFolder(testFolderLevel2); + this.CreateFile("test contents", testFolderLevel2, "level_2_test_file.txt"); + + string testFolderLevel3 = Path.Combine(testFolderLevel2, "TestFolderLevel3"); + this.CreateFolder(testFolderLevel3); + this.CreateFile("test contents", testFolderLevel3, "level_3_test_file.txt"); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel3, Path.Combine(testFolderLevel2, "TestFolderLevel3Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel2, Path.Combine(testFolderLevel1, "TestFolderLevel2Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(testFolderLevel1, Path.Combine(topMostNewFolder, "TestFolderLevel1Renamed")); + this.ValidateGitCommand("status"); + + this.MoveFolder(topMostNewFolder, "AddFoldersAndFilesAndRenameFolder_TestRenamed"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void AddFileAfterFolderRename() + { + this.ValidateGitCommand("checkout -b tests/functional/AddFileAfterFolderRename"); + + string folder = "AddFileAfterFolderRename_Test"; + string renamedFolder = "AddFileAfterFolderRename_TestRenamed"; + this.CreateFolder(folder); + this.MoveFolder(folder, renamedFolder); + this.CreateFile("test contents", renamedFolder, "test_file.txt"); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetSoft() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoft"); + this.ValidateGitCommand("reset --soft HEAD~1"); + } + + [TestCase] + public void ResetMixed() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + } + + [TestCase] + public void ResetMixed2() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2"); + this.ValidateGitCommand("reset HEAD~1"); + } + + [TestCase] + public void ManuallyModifyHead() + { + this.ValidateGitCommand("status"); + this.ReplaceText("f1bce402a7a980a8320f3f235cf8c8fdade4b17a", TestConstants.DotGit.Head); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetSoftTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetSoftTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --soft 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --soft 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetMixedTwice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixedTwice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset --mixed 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset --mixed 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetMixed2Twice() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetMixed2Twice"); + + // A folder rename occured between 99fc72275f950b0052c8548bbcf83a851f2b4467 and + // the subsequent commit 60d19c87328120d11618ad563c396044a50985b2 + this.ValidateGitCommand("reset 60d19c87328120d11618ad563c396044a50985b2"); + this.ValidateGitCommand("reset 99fc72275f950b0052c8548bbcf83a851f2b4467"); + } + + [TestCase] + public void ResetHardAfterCreate() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreate"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEdit() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEdit"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDelete() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDelete"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterCreateAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterCreateAndAdd"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterEditAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterEditAndAdd"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ResetHardAfterDeleteAndAdd() + { + this.ValidateGitCommand("checkout -b tests/functional/ResetHardAfterDeleteAndAdd"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("reset --hard HEAD"); + } + + [TestCase] + public void ChangeTwoBranchesAndMerge() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_1"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeTwoBranchesAndMerge_2"); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for ChangeTwoBranchesAndMerge second branch\""); + this.ValidateGitCommand("merge tests/functional/ChangeTwoBranchesAndMerge_1"); + } + + [TestCase] + public void ChangeBranchAndCherryPickIntoAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + this.ValidateGitCommand("tag DeleteForCherryPick"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchesAndCherryPickIntoAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchesAndCherryPickIntoAnotherBranch_2"); + this.RunGitCommand("cherry-pick DeleteForCherryPick"); + } + + [TestCase] + public void ChangeBranchAndMergeRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndMergeRebaseOnAnotherBranch first branch\""); + + this.RunGitCommand("rebase --merge tests/functional/ChangeBranchAndMergeRebaseOnAnotherBranch_1"); + } + + [TestCase] + public void ChangeBranchAndRebaseOnAnotherBranch() + { + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); + this.CreateFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Create for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + this.DeleteFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Delete for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/ChangeBranchAndRebaseOnAnotherBranch_2"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Edit for ChangeBranchAndRebaseOnAnotherBranch first branch\""); + + this.ValidateGitCommand("rebase tests/functional/ChangeBranchAndRebaseOnAnotherBranch_1"); + } + + [TestCase] + public void StashChanges() + { + this.ValidateGitCommand("checkout -b tests/functional/StashChanges"); + this.EditFile(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("stash"); + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.ValidateGitCommand("checkout -b tests/functional/StashChanges_2"); + this.RunGitCommand("stash pop"); + } + + [TestCase] + public void OpenFileThenCheckout() + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.EditFilePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.EditFilePath); + + // Open files with ReadWrite sharing because depending on the state of the index (and the mtimes), git might need to read the file + // as part of status (while we have the handle open). + using (FileStream virtualFS = File.Open(virtualFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter virtualWriter = new StreamWriter(virtualFS)) + using (FileStream controlFS = File.Open(controlFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.ReadWrite)) + using (StreamWriter controlWriter = new StreamWriter(controlFS)) + { + this.ValidateGitCommand("checkout -b tests/functional/OpenFileThenCheckout"); + virtualWriter.WriteLine("// Adding a line for testing purposes"); + controlWriter.WriteLine("// Adding a line for testing purposes"); + this.ValidateGitCommand("status"); + } + + // NOTE: Due to optimizations in checkout -b, the modified files will not be included as part of the + // success message. Validate that the succcess messages match, and the call to validate "status" below + // will ensure that Scalar is still reporting the edited file as modified. + + string controlRepoRoot = this.ControlGitRepo.RootPath; + string scalarRepoRoot = this.Enlistment.RepoRoot; + string command = "checkout -b tests/functional/OpenFileThenCheckout_2"; + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); + GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult); + actualResult.Errors.ShouldContain("Switched to a new branch"); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void EditFileNeedingUtf8Encoding() + { + this.ValidateGitCommand("checkout -b tests/functional/EditFileNeedingUtf8Encoding"); + this.ValidateGitCommand("status"); + + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, EncodingFileFolder, EncodingFilename); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, EncodingFileFolder, EncodingFilename); + + string contents = virtualFile.ShouldBeAFile(this.FileSystem).WithContents(); + string expectedContents = controlFile.ShouldBeAFile(this.FileSystem).WithContents(); + contents.ShouldEqual(expectedContents); + + this.ValidateGitCommand("status"); + + this.AppendAllText(ContentWhenEditingFile, virtualFile); + this.AppendAllText(ContentWhenEditingFile, controlFile); + + this.ValidateGitCommand("status"); + } + + [TestCase] + public void UseAlias() + { + this.ValidateGitCommand("config --local alias.potato status"); + this.ValidateGitCommand("potato"); + } + + [TestCase] + public void RenameOnlyFileInFolder() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeSource"); + + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.FileSystem.ReadAllText(this.Enlistment.GetVirtualPathTo("Test_EPF_GitCommandsTestOnlyFileFolder", "file.txt")); + this.ValidateGitCommand("merge origin/FunctionalTests/20170202_RenameTestMergeSource"); + } + + [TestCase] + public void BlameTest() + { + this.ValidateGitCommand("blame Readme.md"); + } + + private void BasicCommit(Action fileSystemAction, string addCommand, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand($"checkout -b tests/functional/{test}"); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand(addCommand); + this.RunGitCommand($"commit -m \"BasicCommit for {test}\""); + } + + private void SwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + } + + private void StageChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + } + + private void CommitChangesSwitchBranch(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + this.ValidateGitCommand("checkout -b tests/functional/{0}", test); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", test); + } + + private void CommitChangesSwitchBranchSwitchBack(Action fileSystemAction, [CallerMemberName]string test = GitCommandsTests.UnknownTestName) + { + string branch = string.Format("tests/functional/{0}", test); + this.ValidateGitCommand("checkout -b {0}", branch); + fileSystemAction(); + this.ValidateGitCommand("status"); + this.ValidateGitCommand("add ."); + this.RunGitCommand("commit -m \"Change for {0}\"", branch); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + this.ValidateGitCommand("checkout {0}", branch); + } + + private void CreateFile() + { + this.CreateFile("Some content here", Path.GetRandomFileName() + "tempFile.txt"); + this.CreateFolder(TopLevelFolderToCreate); + this.CreateFolder(Path.Combine(TopLevelFolderToCreate, SubFolderToCreate)); + this.CreateFile("File in new folder", Path.Combine(TopLevelFolderToCreate, SubFolderToCreate, Path.GetRandomFileName() + "folderFile.txt")); + } + + private void EditFile() + { + this.AppendAllText(ContentWhenEditingFile, GitCommandsTests.EditFilePath); + } + + private void DeleteFile() + { + this.DeleteFile(GitCommandsTests.DeleteFilePath); + } + + private void RenameFile() + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, GitCommandsTests.RenameFilePathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, GitCommandsTests.RenameFilePathTo); + this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveFile(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + } + + private void MoveFolder() + { + this.MoveFolder(GitCommandsTests.RenameFolderPathFrom, GitCommandsTests.RenameFolderPathTo); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs index 0fc168d148..fd7353736f 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/GitRepoTests.cs @@ -1,609 +1,609 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System.IO; -using System.Linq; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixture] - public abstract class GitRepoTests - { - protected const string ConflictSourceBranch = "FunctionalTests/20170206_Conflict_Source"; - protected const string ConflictTargetBranch = "FunctionalTests/20170206_Conflict_Target"; - protected const string NoConflictSourceBranch = "FunctionalTests/20170209_NoConflict_Source"; - protected const string DirectoryWithFileBeforeBranch = "FunctionalTests/20171025_DirectoryWithFileBefore"; - protected const string DirectoryWithFileAfterBranch = "FunctionalTests/20171025_DirectoryWithFileAfter"; - protected const string DirectoryWithDifferentFileAfterBranch = "FunctionalTests/20171025_DirectoryWithDifferentFile"; - protected const string DeepDirectoryWithOneFile = "FunctionalTests/20181010_DeepFolderOneFile"; - protected const string DeepDirectoryWithOneDifferentFile = "FunctionalTests/20181010_DeepFolderOneDifferentFile"; - - protected string[] pathPrefixes; - - // These are the folders for the sparse mode that are needed for the functional tests - // because they are the folders that the tests rely on to be there. - private static readonly string[] SparseModeFolders = new string[] - { - "a", - "AddFileAfterFolderRename_Test", - "AddFileAfterFolderRename_TestRenamed", - "AddFoldersAndFilesAndRenameFolder_Test", - "AddFoldersAndFilesAndRenameFolder_TestRenamed", - "c", - "CheckoutNewBranchFromStartingPointTest", - "CheckoutOrhpanBranchFromStartingPointTest", - "d", - "DeleteFileWithNameAheadOfDotAndSwitchCommits", - "EnumerateAndReadTestFiles", - "ErrorWhenPathTreatsFileAsFolderMatchesNTFS", - "file.txt", // Changes to a folder in one test - "foo.cpp", // Changes to a folder in one test - "FilenameEncoding", - "GitCommandsTests", - "GVFlt_BugRegressionTest", - "GVFlt_DeleteFileTest", - "GVFlt_DeleteFolderTest", - "GVFlt_EnumTest", - "GVFlt_FileAttributeTest", - "GVFlt_FileEATest", - "GVFlt_FileOperationTest", - "GVFlt_MoveFileTest", - "GVFlt_MoveFolderTest", - "GVFlt_MultiThreadTest", - "GVFlt_SetLinkTest", - Path.Combine("Scalar", "Scalar"), - Path.Combine("Scalar", "Scalar.Common"), - GitCommandsTests.TopLevelFolderToCreate, - "ResetTwice_OnlyDeletes_Test", - "ResetTwice_OnlyEdits_Test", - "Test_ConflictTests", - "Test_EPF_GitCommandsTestOnlyFileFolder", - "Test_EPF_MoveRenameFileTests", - "Test_EPF_MoveRenameFileTests_2", - "Test_EPF_MoveRenameFolderTests", - "Test_EPF_UpdatePlaceholderTests", - "Test_EPF_WorkingDirectoryTests", - "test_folder", - "TrailingSlashTests", - }; - - // Add directory separator for matching paths since they should be directories - private static readonly string[] PathPrefixesForSparseMode = SparseModeFolders.Select(x => x + Path.DirectorySeparatorChar).ToArray(); - - private bool enlistmentPerTest; - private Settings.ValidateWorkingTreeMode validateWorkingTree; - - public GitRepoTests(bool enlistmentPerTest, Settings.ValidateWorkingTreeMode validateWorkingTree) - { - this.enlistmentPerTest = enlistmentPerTest; - this.validateWorkingTree = validateWorkingTree; - this.FileSystem = new SystemIORunner(); - } - - public static object[] ValidateWorkingTree - { - get - { - return ScalarTestConfig.GitRepoTestsValidateWorkTree; - } - } - - public ControlGitRepo ControlGitRepo - { - get; private set; - } - - protected FileSystemRunner FileSystem - { - get; private set; - } - - protected ScalarFunctionalTestEnlistment Enlistment - { - get; private set; - } - - [OneTimeSetUp] - public virtual void SetupForFixture() - { - if (!this.enlistmentPerTest) - { - this.CreateEnlistment(); - } - } - - [OneTimeTearDown] - public virtual void TearDownForFixture() - { - if (!this.enlistmentPerTest) - { - this.DeleteEnlistment(); - } - } - - [SetUp] - public virtual void SetupForTest() - { - if (this.enlistmentPerTest) - { - this.CreateEnlistment(); - } - - if (this.validateWorkingTree == Settings.ValidateWorkingTreeMode.SparseMode) - { - new ScalarProcess(this.Enlistment).AddSparseFolders(SparseModeFolders); - this.pathPrefixes = PathPrefixesForSparseMode; - } - - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - - this.CheckHeadCommitTree(); - - if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) - { - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - } - - this.ValidateGitCommand("status"); - } - - [TearDown] - public virtual void TearDownForTest() - { - this.TestValidationAndCleanup(); - } - - protected void TestValidationAndCleanup(bool ignoreCase = false) - { - try - { - this.CheckHeadCommitTree(); - - if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System.IO; +using System.Linq; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixture] + public abstract class GitRepoTests + { + protected const string ConflictSourceBranch = "FunctionalTests/20170206_Conflict_Source"; + protected const string ConflictTargetBranch = "FunctionalTests/20170206_Conflict_Target"; + protected const string NoConflictSourceBranch = "FunctionalTests/20170209_NoConflict_Source"; + protected const string DirectoryWithFileBeforeBranch = "FunctionalTests/20171025_DirectoryWithFileBefore"; + protected const string DirectoryWithFileAfterBranch = "FunctionalTests/20171025_DirectoryWithFileAfter"; + protected const string DirectoryWithDifferentFileAfterBranch = "FunctionalTests/20171025_DirectoryWithDifferentFile"; + protected const string DeepDirectoryWithOneFile = "FunctionalTests/20181010_DeepFolderOneFile"; + protected const string DeepDirectoryWithOneDifferentFile = "FunctionalTests/20181010_DeepFolderOneDifferentFile"; + + protected string[] pathPrefixes; + + // These are the folders for the sparse mode that are needed for the functional tests + // because they are the folders that the tests rely on to be there. + private static readonly string[] SparseModeFolders = new string[] + { + "a", + "AddFileAfterFolderRename_Test", + "AddFileAfterFolderRename_TestRenamed", + "AddFoldersAndFilesAndRenameFolder_Test", + "AddFoldersAndFilesAndRenameFolder_TestRenamed", + "c", + "CheckoutNewBranchFromStartingPointTest", + "CheckoutOrhpanBranchFromStartingPointTest", + "d", + "DeleteFileWithNameAheadOfDotAndSwitchCommits", + "EnumerateAndReadTestFiles", + "ErrorWhenPathTreatsFileAsFolderMatchesNTFS", + "file.txt", // Changes to a folder in one test + "foo.cpp", // Changes to a folder in one test + "FilenameEncoding", + "GitCommandsTests", + "GVFlt_BugRegressionTest", + "GVFlt_DeleteFileTest", + "GVFlt_DeleteFolderTest", + "GVFlt_EnumTest", + "GVFlt_FileAttributeTest", + "GVFlt_FileEATest", + "GVFlt_FileOperationTest", + "GVFlt_MoveFileTest", + "GVFlt_MoveFolderTest", + "GVFlt_MultiThreadTest", + "GVFlt_SetLinkTest", + Path.Combine("Scalar", "Scalar"), + Path.Combine("Scalar", "Scalar.Common"), + GitCommandsTests.TopLevelFolderToCreate, + "ResetTwice_OnlyDeletes_Test", + "ResetTwice_OnlyEdits_Test", + "Test_ConflictTests", + "Test_EPF_GitCommandsTestOnlyFileFolder", + "Test_EPF_MoveRenameFileTests", + "Test_EPF_MoveRenameFileTests_2", + "Test_EPF_MoveRenameFolderTests", + "Test_EPF_UpdatePlaceholderTests", + "Test_EPF_WorkingDirectoryTests", + "test_folder", + "TrailingSlashTests", + }; + + // Add directory separator for matching paths since they should be directories + private static readonly string[] PathPrefixesForSparseMode = SparseModeFolders.Select(x => x + Path.DirectorySeparatorChar).ToArray(); + + private bool enlistmentPerTest; + private Settings.ValidateWorkingTreeMode validateWorkingTree; + + public GitRepoTests(bool enlistmentPerTest, Settings.ValidateWorkingTreeMode validateWorkingTree) + { + this.enlistmentPerTest = enlistmentPerTest; + this.validateWorkingTree = validateWorkingTree; + this.FileSystem = new SystemIORunner(); + } + + public static object[] ValidateWorkingTree + { + get + { + return ScalarTestConfig.GitRepoTestsValidateWorkTree; + } + } + + public ControlGitRepo ControlGitRepo + { + get; private set; + } + + protected FileSystemRunner FileSystem + { + get; private set; + } + + protected ScalarFunctionalTestEnlistment Enlistment + { + get; private set; + } + + [OneTimeSetUp] + public virtual void SetupForFixture() + { + if (!this.enlistmentPerTest) + { + this.CreateEnlistment(); + } + } + + [OneTimeTearDown] + public virtual void TearDownForFixture() + { + if (!this.enlistmentPerTest) + { + this.DeleteEnlistment(); + } + } + + [SetUp] + public virtual void SetupForTest() + { + if (this.enlistmentPerTest) + { + this.CreateEnlistment(); + } + + if (this.validateWorkingTree == Settings.ValidateWorkingTreeMode.SparseMode) + { + new ScalarProcess(this.Enlistment).AddSparseFolders(SparseModeFolders); + this.pathPrefixes = PathPrefixesForSparseMode; + } + + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + + if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) + { + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + this.ValidateGitCommand("status"); + } + + [TearDown] + public virtual void TearDownForTest() + { + this.TestValidationAndCleanup(); + } + + protected void TestValidationAndCleanup(bool ignoreCase = false) + { + try + { + this.CheckHeadCommitTree(); + + if (this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) { this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); - } - - this.RunGitCommand("reset --hard -q HEAD"); - this.RunGitCommand("clean -d -f -x"); - this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); - - this.CheckHeadCommitTree(); - - // If enlistmentPerTest is true we can always validate the working tree because - // this is the last place we'll use it - if ((this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) || this.enlistmentPerTest) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); + } + + this.RunGitCommand("reset --hard -q HEAD"); + this.RunGitCommand("clean -d -f -x"); + this.ValidateGitCommand("checkout " + this.ControlGitRepo.Commitish); + + this.CheckHeadCommitTree(); + + // If enlistmentPerTest is true we can always validate the working tree because + // this is the last place we'll use it + if ((this.validateWorkingTree != Settings.ValidateWorkingTreeMode.None) || this.enlistmentPerTest) { this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); - } - } - finally - { - if (this.enlistmentPerTest) - { - this.DeleteEnlistment(); - } - } - } - - protected virtual void CreateEnlistment() - { - this.CreateEnlistment(null); - } - - protected void CreateEnlistment(string commitish = null) - { - this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar, commitish: commitish); - GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false"); - this.ControlGitRepo = ControlGitRepo.Create(commitish); - this.ControlGitRepo.Initialize(); - } - - protected virtual void DeleteEnlistment() - { - if (this.Enlistment != null) - { - this.Enlistment.UnmountAndDeleteAll(); - } - - if (this.ControlGitRepo != null) - { - RepositoryHelpers.DeleteTestDirectory(this.ControlGitRepo.RootPath); - } - } - - protected void CheckHeadCommitTree() - { - this.ValidateGitCommand("ls-tree HEAD"); - } - - protected void RunGitCommand(string command, params object[] args) - { - this.RunGitCommand(string.Format(command, args)); - } - - /* We are using the following method for these scenarios - * 1. Some commands compute a new commit sha, which is dependent on time and therefore - * won't match what is in the control repo. For those commands, we just ensure that - * the errors match what we expect, but we skip comparing the output - * 2. Using the sparse-checkout feature git will error out before checking the untracked files - * so the control repo will show the untracked files as being overwritten while the Scalar - * repo which is using the sparse-checkout will not. - * 3. Scalar is returning not found for files that are outside the sparse-checkout and there - * are cases when git will delete these files during a merge outputting that it removed them - * which the Scalar repo did not have to remove so the message is missing that output. - */ - protected void RunGitCommand(string command, bool ignoreErrors = false, bool checkStatus = true) - { - string controlRepoRoot = this.ControlGitRepo.RootPath; - string scalarRepoRoot = this.Enlistment.RepoRoot; - - ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); - ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); - if (!ignoreErrors) - { - GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult); - } - - if (command != "status" && checkStatus) - { - this.ValidateGitCommand("status"); - } - } - - protected void ValidateGitCommand(string command, params object[] args) - { - GitHelpers.ValidateGitCommand( - this.Enlistment, - this.ControlGitRepo, - command, - args); - } - + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, ignoreCase: ignoreCase, withinPrefixes: this.pathPrefixes); + } + } + finally + { + if (this.enlistmentPerTest) + { + this.DeleteEnlistment(); + } + } + } + + protected virtual void CreateEnlistment() + { + this.CreateEnlistment(null); + } + + protected void CreateEnlistment(string commitish = null) + { + this.Enlistment = ScalarFunctionalTestEnlistment.CloneAndMount(ScalarTestConfig.PathToScalar, commitish: commitish); + GitProcess.Invoke(this.Enlistment.RepoRoot, "config advice.statusUoption false"); + this.ControlGitRepo = ControlGitRepo.Create(commitish); + this.ControlGitRepo.Initialize(); + } + + protected virtual void DeleteEnlistment() + { + if (this.Enlistment != null) + { + this.Enlistment.UnmountAndDeleteAll(); + } + + if (this.ControlGitRepo != null) + { + RepositoryHelpers.DeleteTestDirectory(this.ControlGitRepo.RootPath); + } + } + + protected void CheckHeadCommitTree() + { + this.ValidateGitCommand("ls-tree HEAD"); + } + + protected void RunGitCommand(string command, params object[] args) + { + this.RunGitCommand(string.Format(command, args)); + } + + /* We are using the following method for these scenarios + * 1. Some commands compute a new commit sha, which is dependent on time and therefore + * won't match what is in the control repo. For those commands, we just ensure that + * the errors match what we expect, but we skip comparing the output + * 2. Using the sparse-checkout feature git will error out before checking the untracked files + * so the control repo will show the untracked files as being overwritten while the Scalar + * repo which is using the sparse-checkout will not. + * 3. Scalar is returning not found for files that are outside the sparse-checkout and there + * are cases when git will delete these files during a merge outputting that it removed them + * which the Scalar repo did not have to remove so the message is missing that output. + */ + protected void RunGitCommand(string command, bool ignoreErrors = false, bool checkStatus = true) + { + string controlRepoRoot = this.ControlGitRepo.RootPath; + string scalarRepoRoot = this.Enlistment.RepoRoot; + + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); + if (!ignoreErrors) + { + GitHelpers.ErrorsShouldMatch(command, expectedResult, actualResult); + } + + if (command != "status" && checkStatus) + { + this.ValidateGitCommand("status"); + } + } + + protected void ValidateGitCommand(string command, params object[] args) + { + GitHelpers.ValidateGitCommand( + this.Enlistment, + this.ControlGitRepo, + command, + args); + } + protected void ChangeMode(string filePath, ushort mode) { - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.ChangeMode(virtualFile, mode); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.ChangeMode(virtualFile, mode); this.FileSystem.ChangeMode(controlFile, mode); - } - - protected void CreateEmptyFile() - { - string filePath = Path.GetRandomFileName() + "emptyFile.txt"; - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.CreateEmptyFile(virtualFile); - this.FileSystem.CreateEmptyFile(controlFile); - } - - protected void CreateFile(string content, params string[] filePathPaths) - { - string filePath = Path.Combine(filePathPaths); - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.WriteAllText(virtualFile, content); - this.FileSystem.WriteAllText(controlFile, content); - } - - protected void CreateFileWithoutClose(string path) - { - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path); - this.FileSystem.CreateFileWithoutClose(virtualFile); - this.FileSystem.CreateFileWithoutClose(controlFile); - } - - protected void ReadFileAndWriteWithoutClose(string path, string contents) - { - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path); - this.FileSystem.ReadAllText(virtualFile); - this.FileSystem.ReadAllText(controlFile); - this.FileSystem.OpenFileAndWriteWithoutClose(virtualFile, contents); - this.FileSystem.OpenFileAndWriteWithoutClose(controlFile, contents); - } - - protected void CreateFolder(string folderPath) - { - string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); - string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); - this.FileSystem.CreateDirectory(virtualFolder); - this.FileSystem.CreateDirectory(controlFolder); - } - - protected void EditFile(string content, params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.AppendAllText(virtualFile, content); - this.FileSystem.AppendAllText(controlFile, content); - } - - protected void CreateHardLink(string newLinkFileName, string existingFileName) - { - string virtualExistingFile = Path.Combine(this.Enlistment.RepoRoot, existingFileName); - string controlExistingFile = Path.Combine(this.ControlGitRepo.RootPath, existingFileName); - string virtualNewLinkFile = Path.Combine(this.Enlistment.RepoRoot, newLinkFileName); - string controlNewLinkFile = Path.Combine(this.ControlGitRepo.RootPath, newLinkFileName); - - this.FileSystem.CreateHardLink(virtualNewLinkFile, virtualExistingFile); - this.FileSystem.CreateHardLink(controlNewLinkFile, controlExistingFile); - } - - protected void SetFileAsReadOnly(string filePath) - { - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - - File.SetAttributes(virtualFile, File.GetAttributes(virtualFile) | FileAttributes.ReadOnly); - File.SetAttributes(virtualFile, File.GetAttributes(controlFile) | FileAttributes.ReadOnly); - } - - protected void MoveFile(string pathFrom, string pathTo) - { - string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); - string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); - string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); - string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); - this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); - this.FileSystem.MoveFile(controlFileFrom, controlFileTo); - virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); - controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); - virtualFileTo.ShouldBeAFile(this.FileSystem); - controlFileTo.ShouldBeAFile(this.FileSystem); - } - - protected void DeleteFile(params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.DeleteFile(virtualFile); - this.FileSystem.DeleteFile(controlFile); - virtualFile.ShouldNotExistOnDisk(this.FileSystem); - controlFile.ShouldNotExistOnDisk(this.FileSystem); - } - - protected void DeleteFolder(params string[] folderPathParts) - { - string folderPath = Path.Combine(folderPathParts); - string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); - string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); - this.FileSystem.DeleteDirectory(virtualFolder); - this.FileSystem.DeleteDirectory(controlFolder); - virtualFolder.ShouldNotExistOnDisk(this.FileSystem); - controlFolder.ShouldNotExistOnDisk(this.FileSystem); - } - - protected void MoveFolder(string pathFrom, string pathTo) - { - string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); - string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); - string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); - string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); - this.FileSystem.MoveDirectory(virtualFileFrom, virtualFileTo); - this.FileSystem.MoveDirectory(controlFileFrom, controlFileTo); - virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); - controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); - } - - protected void FolderShouldExist(params string[] folderPathParts) - { - string folderPath = Path.Combine(folderPathParts); - string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); - string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); - virtualFolder.ShouldBeADirectory(this.FileSystem); - controlFolder.ShouldBeADirectory(this.FileSystem); - } - - protected void FolderShouldExistAndHaveFile(params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string folderPath = Path.GetDirectoryName(filePath); - string fileName = Path.GetFileName(filePath); - - string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); - string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); - virtualFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); - controlFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); - } - - protected void FolderShouldExistAndBeEmpty(params string[] folderPathParts) - { - string folderPath = Path.Combine(folderPathParts); - string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); - string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); - virtualFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); - controlFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); - } - - protected void ShouldNotExistOnDisk(params string[] pathParts) - { - string path = Path.Combine(pathParts); - string virtualPath = Path.Combine(this.Enlistment.RepoRoot, path); - string controlPath = Path.Combine(this.ControlGitRepo.RootPath, path); - virtualPath.ShouldNotExistOnDisk(this.FileSystem); - controlPath.ShouldNotExistOnDisk(this.FileSystem); - } - - protected void FileShouldHaveContents(string contents, params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); - virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); - controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); - } - - protected void FileContentsShouldMatch(params string[] filePathPaths) - { - string filePath = Path.Combine(filePathPaths); - string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); - virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(controlFilePath.ShouldBeAFile(this.FileSystem).WithContents()); - } - - protected void FileShouldHaveCaseMatchingName(string caseSensitiveFilePath) - { - string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFilePath); - string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFilePath); - string caseSensitiveName = Path.GetFileName(caseSensitiveFilePath); - virtualFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); - controlFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); - } - - protected void FolderShouldHaveCaseMatchingName(string caseSensitiveFolderPath) - { - string virtualFolderPath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFolderPath); - string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFolderPath); - string caseSensitiveName = Path.GetFileName(caseSensitiveFolderPath); - virtualFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); - controlFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); - } - - protected void AppendAllText(string content, params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.AppendAllText(virtualFile, content); - this.FileSystem.AppendAllText(controlFile, content); - } - - protected void ReplaceText(string newContent, params string[] filePathParts) - { - string filePath = Path.Combine(filePathParts); - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - this.FileSystem.WriteAllText(virtualFile, newContent); - this.FileSystem.WriteAllText(controlFile, newContent); - } - - protected void SetupForFileDirectoryTest(string commandBranch = DirectoryWithFileAfterBranch) - { - this.ControlGitRepo.Fetch(DirectoryWithFileBeforeBranch); - this.ControlGitRepo.Fetch(commandBranch); - this.ValidateGitCommand($"checkout {DirectoryWithFileBeforeBranch}"); - } - - protected void ValidateFileDirectoryTest(string command, string commandBranch = DirectoryWithFileAfterBranch) - { - this.EditFile("Change file", "Readme.md"); - this.ValidateGitCommand("add --all"); - this.RunGitCommand("commit -m \"Some change\""); - this.ValidateGitCommand($"{command} {commandBranch}"); - } - - protected void RunFileDirectoryEnumerateTest(string command, string commandBranch = DirectoryWithFileAfterBranch) - { - this.SetupForFileDirectoryTest(commandBranch); - - // file.txt is a folder with a file named file.txt to test checking out branches - // that have folders with the same name as files - this.FileSystem.EnumerateDirectory(this.Enlistment.GetVirtualPathTo("file.txt")); - this.ValidateFileDirectoryTest(command, commandBranch); - } - - protected void RunFileDirectoryReadTest(string command, string commandBranch = DirectoryWithFileAfterBranch) - { - this.SetupForFileDirectoryTest(commandBranch); - this.FileContentsShouldMatch("file.txt", "file.txt"); - this.ValidateFileDirectoryTest(command, commandBranch); - } - - protected void RunFileDirectoryWriteTest(string command, string commandBranch = DirectoryWithFileAfterBranch) - { - this.SetupForFileDirectoryTest(commandBranch); - this.EditFile("Change file", "file.txt", "file.txt"); - this.ValidateFileDirectoryTest(command, commandBranch); - } - - protected void ReadConflictTargetFiles() - { - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); - } - - protected void FilesShouldMatchCheckoutOfTargetBranch() - { - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); - } - - protected void FilesShouldMatchCheckoutOfSourceBranch() - { - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInTarget.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); - } - - protected void FilesShouldMatchAfterNoConflict() - { - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); - } - - protected void FilesShouldMatchAfterConflict() - { - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); - - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); - this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); - } - } -} + } + + protected void CreateEmptyFile() + { + string filePath = Path.GetRandomFileName() + "emptyFile.txt"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.CreateEmptyFile(virtualFile); + this.FileSystem.CreateEmptyFile(controlFile); + } + + protected void CreateFile(string content, params string[] filePathPaths) + { + string filePath = Path.Combine(filePathPaths); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.WriteAllText(virtualFile, content); + this.FileSystem.WriteAllText(controlFile, content); + } + + protected void CreateFileWithoutClose(string path) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path); + this.FileSystem.CreateFileWithoutClose(virtualFile); + this.FileSystem.CreateFileWithoutClose(controlFile); + } + + protected void ReadFileAndWriteWithoutClose(string path, string contents) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, path); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, path); + this.FileSystem.ReadAllText(virtualFile); + this.FileSystem.ReadAllText(controlFile); + this.FileSystem.OpenFileAndWriteWithoutClose(virtualFile, contents); + this.FileSystem.OpenFileAndWriteWithoutClose(controlFile, contents); + } + + protected void CreateFolder(string folderPath) + { + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.FileSystem.CreateDirectory(virtualFolder); + this.FileSystem.CreateDirectory(controlFolder); + } + + protected void EditFile(string content, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.AppendAllText(virtualFile, content); + this.FileSystem.AppendAllText(controlFile, content); + } + + protected void CreateHardLink(string newLinkFileName, string existingFileName) + { + string virtualExistingFile = Path.Combine(this.Enlistment.RepoRoot, existingFileName); + string controlExistingFile = Path.Combine(this.ControlGitRepo.RootPath, existingFileName); + string virtualNewLinkFile = Path.Combine(this.Enlistment.RepoRoot, newLinkFileName); + string controlNewLinkFile = Path.Combine(this.ControlGitRepo.RootPath, newLinkFileName); + + this.FileSystem.CreateHardLink(virtualNewLinkFile, virtualExistingFile); + this.FileSystem.CreateHardLink(controlNewLinkFile, controlExistingFile); + } + + protected void SetFileAsReadOnly(string filePath) + { + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + + File.SetAttributes(virtualFile, File.GetAttributes(virtualFile) | FileAttributes.ReadOnly); + File.SetAttributes(virtualFile, File.GetAttributes(controlFile) | FileAttributes.ReadOnly); + } + + protected void MoveFile(string pathFrom, string pathTo) + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); + this.FileSystem.MoveFile(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveFile(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + virtualFileTo.ShouldBeAFile(this.FileSystem); + controlFileTo.ShouldBeAFile(this.FileSystem); + } + + protected void DeleteFile(params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.DeleteFile(virtualFile); + this.FileSystem.DeleteFile(controlFile); + virtualFile.ShouldNotExistOnDisk(this.FileSystem); + controlFile.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void DeleteFolder(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + this.FileSystem.DeleteDirectory(virtualFolder); + this.FileSystem.DeleteDirectory(controlFolder); + virtualFolder.ShouldNotExistOnDisk(this.FileSystem); + controlFolder.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void MoveFolder(string pathFrom, string pathTo) + { + string virtualFileFrom = Path.Combine(this.Enlistment.RepoRoot, pathFrom); + string virtualFileTo = Path.Combine(this.Enlistment.RepoRoot, pathTo); + string controlFileFrom = Path.Combine(this.ControlGitRepo.RootPath, pathFrom); + string controlFileTo = Path.Combine(this.ControlGitRepo.RootPath, pathTo); + this.FileSystem.MoveDirectory(virtualFileFrom, virtualFileTo); + this.FileSystem.MoveDirectory(controlFileFrom, controlFileTo); + virtualFileFrom.ShouldNotExistOnDisk(this.FileSystem); + controlFileFrom.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void FolderShouldExist(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem); + controlFolder.ShouldBeADirectory(this.FileSystem); + } + + protected void FolderShouldExistAndHaveFile(params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string folderPath = Path.GetDirectoryName(filePath); + string fileName = Path.GetFileName(filePath); + + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); + controlFolder.ShouldBeADirectory(this.FileSystem).WithItems(fileName).Count().ShouldEqual(1); + } + + protected void FolderShouldExistAndBeEmpty(params string[] folderPathParts) + { + string folderPath = Path.Combine(folderPathParts); + string virtualFolder = Path.Combine(this.Enlistment.RepoRoot, folderPath); + string controlFolder = Path.Combine(this.ControlGitRepo.RootPath, folderPath); + virtualFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); + controlFolder.ShouldBeADirectory(this.FileSystem).WithNoItems(); + } + + protected void ShouldNotExistOnDisk(params string[] pathParts) + { + string path = Path.Combine(pathParts); + string virtualPath = Path.Combine(this.Enlistment.RepoRoot, path); + string controlPath = Path.Combine(this.ControlGitRepo.RootPath, path); + virtualPath.ShouldNotExistOnDisk(this.FileSystem); + controlPath.ShouldNotExistOnDisk(this.FileSystem); + } + + protected void FileShouldHaveContents(string contents, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); + controlFilePath.ShouldBeAFile(this.FileSystem).WithContents(contents); + } + + protected void FileContentsShouldMatch(params string[] filePathPaths) + { + string filePath = Path.Combine(filePathPaths); + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, filePath); + virtualFilePath.ShouldBeAFile(this.FileSystem).WithContents(controlFilePath.ShouldBeAFile(this.FileSystem).WithContents()); + } + + protected void FileShouldHaveCaseMatchingName(string caseSensitiveFilePath) + { + string virtualFilePath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFilePath); + string controlFilePath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFilePath); + string caseSensitiveName = Path.GetFileName(caseSensitiveFilePath); + virtualFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + controlFilePath.ShouldBeAFile(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + } + + protected void FolderShouldHaveCaseMatchingName(string caseSensitiveFolderPath) + { + string virtualFolderPath = Path.Combine(this.Enlistment.RepoRoot, caseSensitiveFolderPath); + string controlFolderPath = Path.Combine(this.ControlGitRepo.RootPath, caseSensitiveFolderPath); + string caseSensitiveName = Path.GetFileName(caseSensitiveFolderPath); + virtualFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + controlFolderPath.ShouldBeADirectory(this.FileSystem).WithCaseMatchingName(caseSensitiveName); + } + + protected void AppendAllText(string content, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.AppendAllText(virtualFile, content); + this.FileSystem.AppendAllText(controlFile, content); + } + + protected void ReplaceText(string newContent, params string[] filePathParts) + { + string filePath = Path.Combine(filePathParts); + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + this.FileSystem.WriteAllText(virtualFile, newContent); + this.FileSystem.WriteAllText(controlFile, newContent); + } + + protected void SetupForFileDirectoryTest(string commandBranch = DirectoryWithFileAfterBranch) + { + this.ControlGitRepo.Fetch(DirectoryWithFileBeforeBranch); + this.ControlGitRepo.Fetch(commandBranch); + this.ValidateGitCommand($"checkout {DirectoryWithFileBeforeBranch}"); + } + + protected void ValidateFileDirectoryTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.EditFile("Change file", "Readme.md"); + this.ValidateGitCommand("add --all"); + this.RunGitCommand("commit -m \"Some change\""); + this.ValidateGitCommand($"{command} {commandBranch}"); + } + + protected void RunFileDirectoryEnumerateTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + + // file.txt is a folder with a file named file.txt to test checking out branches + // that have folders with the same name as files + this.FileSystem.EnumerateDirectory(this.Enlistment.GetVirtualPathTo("file.txt")); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void RunFileDirectoryReadTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + this.FileContentsShouldMatch("file.txt", "file.txt"); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void RunFileDirectoryWriteTest(string command, string commandBranch = DirectoryWithFileAfterBranch) + { + this.SetupForFileDirectoryTest(commandBranch); + this.EditFile("Change file", "file.txt", "file.txt"); + this.ValidateFileDirectoryTest(command, commandBranch); + } + + protected void ReadConflictTargetFiles() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); + } + + protected void FilesShouldMatchCheckoutOfTargetBranch() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInSource.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchCheckoutOfSourceBranch() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "DeletedFiles", "DeleteInTarget.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchAfterNoConflict() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + + protected void FilesShouldMatchAfterConflict() + { + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothDifferentContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByBothSameContent.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedBySource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "AddedByTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "AddedFiles", "NoChange.txt"); + + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInSourceDeleteInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTarget.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ChangeInTargetDeleteInSource.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "ConflictingChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SameChange.txt"); + this.FileContentsShouldMatch("Test_ConflictTests", "ModifiedFiles", "SuccessfulMerge.txt"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs index d8b6b27c0d..37ff5324b6 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/MergeConflictTests.cs @@ -1,121 +1,121 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Tools; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class MergeConflictTests : GitRepoTests - { - public MergeConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void MergeConflict() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void MergeConflictWithFileReads() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ReadConflictTargetFiles(); - this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void MergeConflict_ThenAbort() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("merge --abort"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void MergeConflict_UsingOurs() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand($"merge -s ours {GitRepoTests.ConflictSourceBranch}"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void MergeConflict_UsingStrategyTheirs() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand($"merge -s recursive -Xtheirs {GitRepoTests.ConflictSourceBranch}"); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void MergeConflict_UsingStrategyOurs() - { - // No need to tear down this config since these tests are for enlistment per test. - this.SetupRenameDetectionAvoidanceInConfig(); - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand($"merge -s recursive -Xours {GitRepoTests.ConflictSourceBranch}"); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void MergeConflictEnsureStatusFailsDueToConfig() - { - // This is compared against the message emitted by Scalar.Hooks\Program.cs - string expectedErrorMessagePart = "--no-renames"; - - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch, checkStatus: false); - - ProcessResult result1 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status"); - result1.Errors.Contains(expectedErrorMessagePart); - - ProcessResult result2 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status --no-renames"); - result2.Errors.Contains(expectedErrorMessagePart); - - // Complete setup to ensure teardown succeeds - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "config --local test.renames false"); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - } - - private void SetupRenameDetectionAvoidanceInConfig() - { - // Tell the pre-command hook that it shouldn't check for "--no-renames" when runing "git status" - // as the control repo won't do that. When the pre-command hook has been updated to properly - // check for "status.renames" we can set that value here instead. - this.ValidateGitCommand("config --local test.renames false"); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class MergeConflictTests : GitRepoTests + { + public MergeConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void MergeConflict() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflictWithFileReads() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflict_ThenAbort() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("merge --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void MergeConflict_UsingOurs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s ours {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void MergeConflict_UsingStrategyTheirs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s recursive -Xtheirs {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflict_UsingStrategyOurs() + { + // No need to tear down this config since these tests are for enlistment per test. + this.SetupRenameDetectionAvoidanceInConfig(); + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand($"merge -s recursive -Xours {GitRepoTests.ConflictSourceBranch}"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void MergeConflictEnsureStatusFailsDueToConfig() + { + // This is compared against the message emitted by Scalar.Hooks\Program.cs + string expectedErrorMessagePart = "--no-renames"; + + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("merge " + GitRepoTests.ConflictSourceBranch, checkStatus: false); + + ProcessResult result1 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status"); + result1.Errors.Contains(expectedErrorMessagePart); + + ProcessResult result2 = GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "status --no-renames"); + result2.Errors.Contains(expectedErrorMessagePart); + + // Complete setup to ensure teardown succeeds + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "config --local test.renames false"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + + private void SetupRenameDetectionAvoidanceInConfig() + { + // Tell the pre-command hook that it shouldn't check for "--no-renames" when runing "git status" + // as the control repo won't do that. When the pre-command hook has been updated to properly + // check for "status.renames" we can set that value here instead. + this.ValidateGitCommand("config --local test.renames false"); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs index ea0027f28a..ff4de8334f 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/RebaseConflictTests.cs @@ -1,101 +1,101 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class RebaseConflictTests : GitRepoTests - { - public RebaseConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void RebaseConflict() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void RebaseConflictWithPrefetch() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.Enlistment.Prefetch("--files * --hydrate"); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void RebaseConflictWithFileReads() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ReadConflictTargetFiles(); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void RebaseConflict_ThenAbort() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("rebase --abort"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void RebaseConflict_ThenSkip() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("rebase --skip"); - this.FilesShouldMatchCheckoutOfSourceBranch(); - } - - [TestCase] - public void RebaseConflict_RemoveDeletedTheirsFile() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("rm Test_ConflictTests/ModifiedFiles/ChangeInSourceDeleteInTarget.txt"); - } - - [TestCase] - public void RebaseConflict_AddThenContinue() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("add ."); - this.ValidateGitCommand("rebase --continue"); - this.FilesShouldMatchAfterConflict(); - } - - [TestCase] - public void RebaseMultipleCommits() - { - string sourceCommit = "FunctionalTests/20170403_rebase_multiple_source"; - string targetCommit = "FunctionalTests/20170403_rebase_multiple_onto"; - - this.ControlGitRepo.Fetch(sourceCommit); - this.ControlGitRepo.Fetch(targetCommit); - - this.ValidateGitCommand("checkout " + sourceCommit); - this.RunGitCommand("rebase origin/" + targetCommit); - this.ValidateGitCommand("rebase --abort"); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class RebaseConflictTests : GitRepoTests + { + public RebaseConflictTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void RebaseConflict() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseConflictWithPrefetch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.Enlistment.Prefetch("--files * --hydrate"); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseConflictWithFileReads() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ReadConflictTargetFiles(); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseConflict_ThenAbort() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rebase --abort"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void RebaseConflict_ThenSkip() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rebase --skip"); + this.FilesShouldMatchCheckoutOfSourceBranch(); + } + + [TestCase] + public void RebaseConflict_RemoveDeletedTheirsFile() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("rm Test_ConflictTests/ModifiedFiles/ChangeInSourceDeleteInTarget.txt"); + } + + [TestCase] + public void RebaseConflict_AddThenContinue() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.RunGitCommand("rebase " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("add ."); + this.ValidateGitCommand("rebase --continue"); + this.FilesShouldMatchAfterConflict(); + } + + [TestCase] + public void RebaseMultipleCommits() + { + string sourceCommit = "FunctionalTests/20170403_rebase_multiple_source"; + string targetCommit = "FunctionalTests/20170403_rebase_multiple_onto"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout " + sourceCommit); + this.RunGitCommand("rebase origin/" + targetCommit); + this.ValidateGitCommand("rebase --abort"); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs index 35ccc0a122..222a723c3d 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/RebaseTests.cs @@ -1,119 +1,119 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class RebaseTests : GitRepoTests - { - public RebaseTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] - public void RebaseSmallNoConflicts() - { - // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 - string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; - - // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of - // FunctionalTests/20170130 - string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; - - this.ControlGitRepo.Fetch(sourceCommit); - this.ControlGitRepo.Fetch(targetCommit); - - this.ValidateGitCommand("checkout {0}", sourceCommit); - this.ValidateGitCommand("rebase {0}", targetCommit); - } - - [TestCase] - public void RebaseSmallOneFileConflict() - { - // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 - string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; - - // Target commit 99fc72275f950b0052c8548bbcf83a851f2b4467 is part of the history of - // FunctionalTests/20170130 - string targetCommit = "99fc72275f950b0052c8548bbcf83a851f2b4467"; - - this.ControlGitRepo.Fetch(sourceCommit); - this.ControlGitRepo.Fetch(targetCommit); - - this.ValidateGitCommand("checkout {0}", sourceCommit); - this.ValidateGitCommand("rebase {0}", targetCommit); - } - - [TestCase] - [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] - public void RebaseEditThenDelete() - { - // 23a238b04497da2449fd730966c06f84b6326c3a is the tip of FunctionalTests/RebaseTestsSource_20170208 - string sourceCommit = "23a238b04497da2449fd730966c06f84b6326c3a"; - - // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of - // FunctionalTests/20170208 - string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; - - this.ControlGitRepo.Fetch(sourceCommit); - this.ControlGitRepo.Fetch(targetCommit); - - this.ValidateGitCommand("checkout {0}", sourceCommit); - this.ValidateGitCommand("rebase {0}", targetCommit); - } - - [TestCase] - public void RebaseWithDirectoryNameSameAsFile() - { - this.SetupForFileDirectoryTest(); - this.ValidateFileDirectoryTest("rebase"); - } - - [TestCase] - public void RebaseWithDirectoryNameSameAsFileEnumerate() - { - this.RunFileDirectoryEnumerateTest("rebase"); - } - - [TestCase] - public void RebaseWithDirectoryNameSameAsFileWithRead() - { - this.RunFileDirectoryReadTest("rebase"); - } - - [TestCase] - public void RebaseWithDirectoryNameSameAsFileWithWrite() - { - this.RunFileDirectoryWriteTest("rebase"); - } - - [TestCase] - public void RebaseDirectoryWithOneFile() - { - this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - this.ValidateFileDirectoryTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void RebaseDirectoryWithOneFileEnumerate() - { - this.RunFileDirectoryEnumerateTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void RebaseDirectoryWithOneFileRead() - { - this.RunFileDirectoryReadTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void RebaseDirectoryWithOneFileWrite() - { - this.RunFileDirectoryWriteTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class RebaseTests : GitRepoTests + { + public RebaseTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] + public void RebaseSmallNoConflicts() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + public void RebaseSmallOneFileConflict() + { + // 5d299512450f4029d7a1fe8d67e833b84247d393 is the tip of FunctionalTests/RebaseTestsSource_20170130 + string sourceCommit = "5d299512450f4029d7a1fe8d67e833b84247d393"; + + // Target commit 99fc72275f950b0052c8548bbcf83a851f2b4467 is part of the history of + // FunctionalTests/20170130 + string targetCommit = "99fc72275f950b0052c8548bbcf83a851f2b4467"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + [Ignore("This is producing different output because git is not checking out files in the rebase. The virtual file system changes should address this issue.")] + public void RebaseEditThenDelete() + { + // 23a238b04497da2449fd730966c06f84b6326c3a is the tip of FunctionalTests/RebaseTestsSource_20170208 + string sourceCommit = "23a238b04497da2449fd730966c06f84b6326c3a"; + + // Target commit 47fabb534c35af40156db6e8365165cb04f9dd75 is part of the history of + // FunctionalTests/20170208 + string targetCommit = "47fabb534c35af40156db6e8365165cb04f9dd75"; + + this.ControlGitRepo.Fetch(sourceCommit); + this.ControlGitRepo.Fetch(targetCommit); + + this.ValidateGitCommand("checkout {0}", sourceCommit); + this.ValidateGitCommand("rebase {0}", targetCommit); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFile() + { + this.SetupForFileDirectoryTest(); + this.ValidateFileDirectoryTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileEnumerate() + { + this.RunFileDirectoryEnumerateTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileWithRead() + { + this.RunFileDirectoryReadTest("rebase"); + } + + [TestCase] + public void RebaseWithDirectoryNameSameAsFileWithWrite() + { + this.RunFileDirectoryWriteTest("rebase"); + } + + [TestCase] + public void RebaseDirectoryWithOneFile() + { + this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + this.ValidateFileDirectoryTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileEnumerate() + { + this.RunFileDirectoryEnumerateTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileRead() + { + this.RunFileDirectoryReadTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void RebaseDirectoryWithOneFileWrite() + { + this.RunFileDirectoryWriteTest("rebase", commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs index 7fab46cc00..db001fd6db 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetHardTests.cs @@ -1,80 +1,80 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class ResetHardTests : GitRepoTests - { - private const string ResetHardCommand = "reset --hard"; - - public ResetHardTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void VerifyResetHardDeletesEmptyFolders() - { - this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); - this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); - this.ValidateGitCommand("reset --hard HEAD~1"); - this.ShouldNotExistOnDisk("Test_EPF_GitCommandsTestOnlyFileFolder"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - } - - [TestCase] - public void ResetHardWithDirectoryNameSameAsFile() - { - this.SetupForFileDirectoryTest(); - this.ValidateFileDirectoryTest(ResetHardCommand); - } - - [TestCase] - public void ResetHardWithDirectoryNameSameAsFileEnumerate() - { - this.RunFileDirectoryEnumerateTest(ResetHardCommand); - } - - [TestCase] - public void ResetHardWithDirectoryNameSameAsFileWithRead() - { - this.RunFileDirectoryReadTest(ResetHardCommand); - } - - [TestCase] - public void ResetHardWithDirectoryNameSameAsFileWithWrite() - { - this.RunFileDirectoryWriteTest(ResetHardCommand); - } - - [TestCase] - public void ResetHardDirectoryWithOneFile() - { - this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithFileAfterBranch); - this.ValidateFileDirectoryTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void ResetHardDirectoryWithOneFileEnumerate() - { - this.RunFileDirectoryEnumerateTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void ResetHardDirectoryWithOneFileRead() - { - this.RunFileDirectoryReadTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - - [TestCase] - public void ResetHardDirectoryWithOneFileWrite() - { - this.RunFileDirectoryWriteTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class ResetHardTests : GitRepoTests + { + private const string ResetHardCommand = "reset --hard"; + + public ResetHardTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void VerifyResetHardDeletesEmptyFolders() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("checkout FunctionalTests/20170202_RenameTestMergeTarget"); + this.ValidateGitCommand("reset --hard HEAD~1"); + this.ShouldNotExistOnDisk("Test_EPF_GitCommandsTestOnlyFileFolder"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFile() + { + this.SetupForFileDirectoryTest(); + this.ValidateFileDirectoryTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileEnumerate() + { + this.RunFileDirectoryEnumerateTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileWithRead() + { + this.RunFileDirectoryReadTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardWithDirectoryNameSameAsFileWithWrite() + { + this.RunFileDirectoryWriteTest(ResetHardCommand); + } + + [TestCase] + public void ResetHardDirectoryWithOneFile() + { + this.SetupForFileDirectoryTest(commandBranch: GitRepoTests.DirectoryWithFileAfterBranch); + this.ValidateFileDirectoryTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileEnumerate() + { + this.RunFileDirectoryEnumerateTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileRead() + { + this.RunFileDirectoryReadTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + + [TestCase] + public void ResetHardDirectoryWithOneFileWrite() + { + this.RunFileDirectoryWriteTest(ResetHardCommand, commandBranch: GitRepoTests.DirectoryWithDifferentFileAfterBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs index 7a0d5cc6f1..ab4427c5f6 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetMixedTests.cs @@ -1,126 +1,126 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Should; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class ResetMixedTests : GitRepoTests - { - public ResetMixedTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void ResetMixed() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedAfterPrefetch() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.Enlistment.Prefetch("--files * --hydrate"); - this.ValidateGitCommand("reset --mixed HEAD~1"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedAndCheckoutNewBranch() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - - // Use RunGitCommand rather than ValidateGitCommand as G4W optimizations for "checkout -b" mean that the - // command will not report modified and deleted files - this.RunGitCommand("checkout -b tests/functional/ResetMixedAndCheckoutNewBranch"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void ResetMixedAndCheckoutOrphanBranch() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - this.ValidateGitCommand("checkout --orphan tests/functional/ResetMixedAndCheckoutOrphanBranch"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedAndRemount() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - - this.Enlistment.UnmountScalar(); - this.Enlistment.MountScalar(); - this.ValidateGitCommand("status"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedThenCheckoutWithConflicts() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - - // Because git while using the sparse-checkout feature - // will check for index merge conflicts and error out before it checks - // for untracked files that will be overwritten we just run the command - this.RunGitCommand("checkout " + GitRepoTests.ConflictSourceBranch, ignoreErrors: true); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedOnlyAddedThenCheckoutWithConflicts() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --mixed HEAD~1"); - - // This will reset all the files except the files that were added - // and are untracked to make sure we error out with those using sparse-checkout - this.ValidateGitCommand("checkout -f"); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetMixedAndCheckoutFile() - { - this.ControlGitRepo.Fetch("FunctionalTests/20170602"); - - // We start with a branch that deleted two files that were present in its parent commit - this.ValidateGitCommand("checkout FunctionalTests/20170602"); - - // Then reset --mixed to the parent commit, and validate that the deleted files did not come back into the projection - this.ValidateGitCommand("reset --mixed HEAD~1"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - - // And checkout a file (without changing branches) and ensure that that doesn't update the projection either - this.ValidateGitCommand("checkout HEAD~2 .gitattributes"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - - // And now if we checkout the original commit, the deleted files should stay deleted - this.ValidateGitCommand("checkout FunctionalTests/20170602"); - this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) - .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Should; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class ResetMixedTests : GitRepoTests + { + public ResetMixedTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ResetMixed() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAfterPrefetch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.Enlistment.Prefetch("--files * --hydrate"); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAndCheckoutNewBranch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // Use RunGitCommand rather than ValidateGitCommand as G4W optimizations for "checkout -b" mean that the + // command will not report modified and deleted files + this.RunGitCommand("checkout -b tests/functional/ResetMixedAndCheckoutNewBranch"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void ResetMixedAndCheckoutOrphanBranch() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.ValidateGitCommand("checkout --orphan tests/functional/ResetMixedAndCheckoutOrphanBranch"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAndRemount() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + this.Enlistment.UnmountScalar(); + this.Enlistment.MountScalar(); + this.ValidateGitCommand("status"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // Because git while using the sparse-checkout feature + // will check for index merge conflicts and error out before it checks + // for untracked files that will be overwritten we just run the command + this.RunGitCommand("checkout " + GitRepoTests.ConflictSourceBranch, ignoreErrors: true); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedOnlyAddedThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --mixed HEAD~1"); + + // This will reset all the files except the files that were added + // and are untracked to make sure we error out with those using sparse-checkout + this.ValidateGitCommand("checkout -f"); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetMixedAndCheckoutFile() + { + this.ControlGitRepo.Fetch("FunctionalTests/20170602"); + + // We start with a branch that deleted two files that were present in its parent commit + this.ValidateGitCommand("checkout FunctionalTests/20170602"); + + // Then reset --mixed to the parent commit, and validate that the deleted files did not come back into the projection + this.ValidateGitCommand("reset --mixed HEAD~1"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + // And checkout a file (without changing branches) and ensure that that doesn't update the projection either + this.ValidateGitCommand("checkout HEAD~2 .gitattributes"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + + // And now if we checkout the original commit, the deleted files should stay deleted + this.ValidateGitCommand("checkout FunctionalTests/20170602"); + this.Enlistment.RepoRoot.ShouldBeADirectory(this.FileSystem) + .WithDeepStructure(this.FileSystem, this.ControlGitRepo.RootPath, withinPrefixes: this.pathPrefixes); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs index 3e07f26a49..9490f3ec63 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/ResetSoftTests.cs @@ -1,73 +1,73 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class ResetSoftTests : GitRepoTests - { - public ResetSoftTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void ResetSoft() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetSoftThenRemount() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - - this.Enlistment.UnmountScalar(); - this.Enlistment.MountScalar(); - this.ValidateGitCommand("status"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetSoftThenCheckoutWithConflicts() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void ResetSoftThenCheckoutNoConflicts() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); - this.FilesShouldMatchAfterNoConflict(); - } - - [TestCase] - public void ResetSoftThenResetHeadThenCheckoutNoConflicts() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("reset --soft HEAD~1"); - this.ValidateGitCommand("reset HEAD Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); - this.FilesShouldMatchAfterNoConflict(); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - this.ControlGitRepo.Fetch(GitRepoTests.NoConflictSourceBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class ResetSoftTests : GitRepoTests + { + public ResetSoftTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void ResetSoft() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetSoftThenRemount() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + this.Enlistment.UnmountScalar(); + this.Enlistment.MountScalar(); + this.ValidateGitCommand("status"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetSoftThenCheckoutWithConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictSourceBranch); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void ResetSoftThenCheckoutNoConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); + this.FilesShouldMatchAfterNoConflict(); + } + + [TestCase] + public void ResetSoftThenResetHeadThenCheckoutNoConflicts() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("reset --soft HEAD~1"); + this.ValidateGitCommand("reset HEAD Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.ValidateGitCommand("checkout " + GitRepoTests.NoConflictSourceBranch); + this.FilesShouldMatchAfterNoConflict(); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + this.ControlGitRepo.Fetch(GitRepoTests.NoConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs index 5c359ac337..58ae27da90 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/StatusTests.cs @@ -1,228 +1,228 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Properties; +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Properties; using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.IO; -using System.Threading; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.IO; +using System.Threading; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class StatusTests : GitRepoTests - { - public StatusTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void MoveFileIntoDotGitDirectory() - { - string srcPath = @"Readme.md"; - string dstPath = Path.Combine(".git", "destination.txt"); - - this.MoveFile(srcPath, dstPath); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void DeleteThenCreateThenDeleteFile() - { - string srcPath = @"Readme.md"; - - this.DeleteFile(srcPath); - this.ValidateGitCommand("status"); - this.CreateFile("Testing", srcPath); - this.ValidateGitCommand("status"); - this.DeleteFile(srcPath); - this.ValidateGitCommand("status"); - } - - [TestCase] - public void CreateFileWithoutClose() - { - string srcPath = @"CreateFileWithoutClose.md"; - this.CreateFileWithoutClose(srcPath); - this.ValidGitStatusWithRetry(srcPath); - } - - [TestCase] - public void WriteWithoutClose() - { - string srcPath = @"Readme.md"; - this.ReadFileAndWriteWithoutClose(srcPath, "More Stuff"); - this.ValidGitStatusWithRetry(srcPath); - } - - [TestCase] - public void AppendFileUsingBash() - { - // Bash will perform the append using '>>' which will cause KAUTH_VNODE_APPEND_DATA to be sent without hydration - // Other Runners may cause hydration before append - BashRunner bash = new BashRunner(); - string filePath = Path.Combine("Test_EPF_UpdatePlaceholderTests", "LockToPreventUpdate", "test.txt"); - string content = "Apended Data"; - string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); - string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); - bash.AppendAllText(virtualFile, content); - bash.AppendAllText(controlFile, content); - - this.ValidateGitCommand("status"); - - // We check the contents after status, to ensure this check didn't cause the hydration - string appendedContent = string.Concat("Commit2LockToPreventUpdate \r\n", content); + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class StatusTests : GitRepoTests + { + public StatusTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void MoveFileIntoDotGitDirectory() + { + string srcPath = @"Readme.md"; + string dstPath = Path.Combine(".git", "destination.txt"); + + this.MoveFile(srcPath, dstPath); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void DeleteThenCreateThenDeleteFile() + { + string srcPath = @"Readme.md"; + + this.DeleteFile(srcPath); + this.ValidateGitCommand("status"); + this.CreateFile("Testing", srcPath); + this.ValidateGitCommand("status"); + this.DeleteFile(srcPath); + this.ValidateGitCommand("status"); + } + + [TestCase] + public void CreateFileWithoutClose() + { + string srcPath = @"CreateFileWithoutClose.md"; + this.CreateFileWithoutClose(srcPath); + this.ValidGitStatusWithRetry(srcPath); + } + + [TestCase] + public void WriteWithoutClose() + { + string srcPath = @"Readme.md"; + this.ReadFileAndWriteWithoutClose(srcPath, "More Stuff"); + this.ValidGitStatusWithRetry(srcPath); + } + + [TestCase] + public void AppendFileUsingBash() + { + // Bash will perform the append using '>>' which will cause KAUTH_VNODE_APPEND_DATA to be sent without hydration + // Other Runners may cause hydration before append + BashRunner bash = new BashRunner(); + string filePath = Path.Combine("Test_EPF_UpdatePlaceholderTests", "LockToPreventUpdate", "test.txt"); + string content = "Apended Data"; + string virtualFile = Path.Combine(this.Enlistment.RepoRoot, filePath); + string controlFile = Path.Combine(this.ControlGitRepo.RootPath, filePath); + bash.AppendAllText(virtualFile, content); + bash.AppendAllText(controlFile, content); + + this.ValidateGitCommand("status"); + + // We check the contents after status, to ensure this check didn't cause the hydration + string appendedContent = string.Concat("Commit2LockToPreventUpdate \r\n", content); virtualFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent); controlFile.ShouldBeAFile(this.FileSystem).WithContents(appendedContent); - } - - [TestCase] - [Category(Categories.MacTODO.NeedsStatusCache)] - public void ModifyingAndDeletingRepositoryExcludeFileInvalidatesCache() - { - string repositoryExcludeFile = Path.Combine(".git", "info", "exclude"); - - this.RepositoryIgnoreTestSetup(); - - // Add ignore pattern to existing exclude file - this.EditFile("*.ign", repositoryExcludeFile); - - // The exclude file has been modified, verify this status - // excludes the "test.ign" file as expected. - this.ValidateGitCommand("status"); - - // Wait for status cache - this.WaitForStatusCacheToBeGenerated(); - - // Delete repository exclude file - this.DeleteFile(repositoryExcludeFile); - - // The exclude file has been deleted, verify this status - // includes the "test.ign" file as expected. - this.ValidateGitCommand("status"); - } - - [TestCase] - [Category(Categories.MacTODO.NeedsStatusCache)] - public void NewRepositoryExcludeFileInvalidatesCache() - { - string repositoryExcludeFileRelativePath = Path.Combine(".git", "info", "exclude"); - string repositoryExcludeFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, repositoryExcludeFileRelativePath); - - this.DeleteFile(repositoryExcludeFileRelativePath); - - this.RepositoryIgnoreTestSetup(); - - File.Exists(repositoryExcludeFilePath).ShouldBeFalse("Repository exclude path should not exist"); - - // Create new exclude file with ignore pattern - this.CreateFile("*.ign", repositoryExcludeFileRelativePath); - - // The exclude file has been modified, verify this status - // excludes the "test.ign" file as expected. - this.ValidateGitCommand("status"); - } - - [TestCase] - [Category(Categories.MacTODO.NeedsStatusCache)] - public void ModifyingHeadSymbolicRefInvalidatesCache() - { - this.ValidateGitCommand("status"); - - this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); - - this.ValidateGitCommand("branch other_branch"); - - this.WaitForStatusCacheToBeGenerated(); - this.ValidateGitCommand("status"); - - this.ValidateGitCommand("symbolic-ref HEAD refs/heads/other_branch"); - } - - [TestCase] - [Category(Categories.MacTODO.NeedsStatusCache)] - public void ModifyingHeadRefInvalidatesCache() - { - this.ValidateGitCommand("status"); - - this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); - - this.ValidateGitCommand("update-ref HEAD HEAD~1"); - - this.WaitForStatusCacheToBeGenerated(); - this.ValidateGitCommand("status"); - } - - private void RepositoryIgnoreTestSetup() - { - this.WaitForUpToDateStatusCache(); - - string statusCachePath = Path.Combine(this.Enlistment.DotScalarRoot, "GitStatusCache", "GitStatusCache.dat"); - File.Delete(statusCachePath); - - // Create a new file with an extension that will be ignored later in the test. - this.CreateFile("file to be ignored", "test.ign"); - - this.WaitForStatusCacheToBeGenerated(); - - // Verify that status from the status cache includes the "test.ign" entry - this.ValidateGitCommand("status"); - } - - /// - /// Wait for an up-to-date status cache file to exist on disk. - /// - private void WaitForUpToDateStatusCache() - { - // Run "git status" for the side effect that it will delete any stale status cache file. - this.ValidateGitCommand("status"); - - // Wait for a new status cache to be generated. - this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); - } - - private void WaitForStatusCacheToBeGenerated(bool waitForNewFile = true) - { - string statusCachePath = Path.Combine(this.Enlistment.DotScalarRoot, "GitStatusCache", "GitStatusCache.dat"); - - if (waitForNewFile) - { - File.Exists(statusCachePath).ShouldEqual(false, "Status cache file should not exist at this point - it should have been deleted by previous status command."); - } - - // Wait for the status cache file to be regenerated - for (int i = 0; i < 10; i++) - { - if (File.Exists(statusCachePath)) - { - break; - } - - Thread.Sleep(1000); - } - - // The cache file should exist by now. We want the next status to come from the - // cache and include the "test.ign" entry. - File.Exists(statusCachePath).ShouldEqual(true, "Status cache file should be regenerated by this point."); - } - - private void ValidGitStatusWithRetry(string srcPath) - { - this.Enlistment.WaitForBackgroundOperations(); - try - { - this.ValidateGitCommand("status"); - } - catch (Exception ex) - { - Thread.Sleep(1000); - this.ValidateGitCommand("status"); - Assert.Fail("{0} was succesful on the second try, but failed on first: {1}", nameof(this.ValidateGitCommand), ex.Message); - } - } - } -} + } + + [TestCase] + [Category(Categories.MacTODO.NeedsStatusCache)] + public void ModifyingAndDeletingRepositoryExcludeFileInvalidatesCache() + { + string repositoryExcludeFile = Path.Combine(".git", "info", "exclude"); + + this.RepositoryIgnoreTestSetup(); + + // Add ignore pattern to existing exclude file + this.EditFile("*.ign", repositoryExcludeFile); + + // The exclude file has been modified, verify this status + // excludes the "test.ign" file as expected. + this.ValidateGitCommand("status"); + + // Wait for status cache + this.WaitForStatusCacheToBeGenerated(); + + // Delete repository exclude file + this.DeleteFile(repositoryExcludeFile); + + // The exclude file has been deleted, verify this status + // includes the "test.ign" file as expected. + this.ValidateGitCommand("status"); + } + + [TestCase] + [Category(Categories.MacTODO.NeedsStatusCache)] + public void NewRepositoryExcludeFileInvalidatesCache() + { + string repositoryExcludeFileRelativePath = Path.Combine(".git", "info", "exclude"); + string repositoryExcludeFilePath = Path.Combine(this.Enlistment.EnlistmentRoot, repositoryExcludeFileRelativePath); + + this.DeleteFile(repositoryExcludeFileRelativePath); + + this.RepositoryIgnoreTestSetup(); + + File.Exists(repositoryExcludeFilePath).ShouldBeFalse("Repository exclude path should not exist"); + + // Create new exclude file with ignore pattern + this.CreateFile("*.ign", repositoryExcludeFileRelativePath); + + // The exclude file has been modified, verify this status + // excludes the "test.ign" file as expected. + this.ValidateGitCommand("status"); + } + + [TestCase] + [Category(Categories.MacTODO.NeedsStatusCache)] + public void ModifyingHeadSymbolicRefInvalidatesCache() + { + this.ValidateGitCommand("status"); + + this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); + + this.ValidateGitCommand("branch other_branch"); + + this.WaitForStatusCacheToBeGenerated(); + this.ValidateGitCommand("status"); + + this.ValidateGitCommand("symbolic-ref HEAD refs/heads/other_branch"); + } + + [TestCase] + [Category(Categories.MacTODO.NeedsStatusCache)] + public void ModifyingHeadRefInvalidatesCache() + { + this.ValidateGitCommand("status"); + + this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); + + this.ValidateGitCommand("update-ref HEAD HEAD~1"); + + this.WaitForStatusCacheToBeGenerated(); + this.ValidateGitCommand("status"); + } + + private void RepositoryIgnoreTestSetup() + { + this.WaitForUpToDateStatusCache(); + + string statusCachePath = Path.Combine(this.Enlistment.DotScalarRoot, "GitStatusCache", "GitStatusCache.dat"); + File.Delete(statusCachePath); + + // Create a new file with an extension that will be ignored later in the test. + this.CreateFile("file to be ignored", "test.ign"); + + this.WaitForStatusCacheToBeGenerated(); + + // Verify that status from the status cache includes the "test.ign" entry + this.ValidateGitCommand("status"); + } + + /// + /// Wait for an up-to-date status cache file to exist on disk. + /// + private void WaitForUpToDateStatusCache() + { + // Run "git status" for the side effect that it will delete any stale status cache file. + this.ValidateGitCommand("status"); + + // Wait for a new status cache to be generated. + this.WaitForStatusCacheToBeGenerated(waitForNewFile: false); + } + + private void WaitForStatusCacheToBeGenerated(bool waitForNewFile = true) + { + string statusCachePath = Path.Combine(this.Enlistment.DotScalarRoot, "GitStatusCache", "GitStatusCache.dat"); + + if (waitForNewFile) + { + File.Exists(statusCachePath).ShouldEqual(false, "Status cache file should not exist at this point - it should have been deleted by previous status command."); + } + + // Wait for the status cache file to be regenerated + for (int i = 0; i < 10; i++) + { + if (File.Exists(statusCachePath)) + { + break; + } + + Thread.Sleep(1000); + } + + // The cache file should exist by now. We want the next status to come from the + // cache and include the "test.ign" entry. + File.Exists(statusCachePath).ShouldEqual(true, "Status cache file should be regenerated by this point."); + } + + private void ValidGitStatusWithRetry(string srcPath) + { + this.Enlistment.WaitForBackgroundOperations(); + try + { + this.ValidateGitCommand("status"); + } + catch (Exception ex) + { + Thread.Sleep(1000); + this.ValidateGitCommand("status"); + Assert.Fail("{0} was succesful on the second try, but failed on first: {1}", nameof(this.ValidateGitCommand), ex.Message); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs index f53e81b3cd..c2117af7f3 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/UpdateIndexTests.cs @@ -1,76 +1,76 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.FunctionalTests.Tools; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class UpdateIndexTests : GitRepoTests - { - public UpdateIndexTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - [Ignore("TODO 940287: git update-index --remove does not check if the file is on disk if the skip-worktree bit is set")] - public void UpdateIndexRemoveFileOnDisk() - { - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - this.ValidateGitCommand("update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - } - - [TestCase] - public void UpdateIndexRemoveFileOnDiskDontCheckStatus() - { - // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - - // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, - // meaning it will always remove the file from the index - GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - - // Add the files back to the index so the git-status that is run during teardown matches - GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - } - - [TestCase] - public void UpdateIndexRemoveAddFileOpenForWrite() - { - // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk - this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); - - // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, - // meaning it will always remove the file from the index - GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - this.FilesShouldMatchCheckoutOfTargetBranch(); - - // Add the files back to the index so the git-status that is run during teardown matches - GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); - } - - [TestCase] - public void UpdateIndexWithCacheInfo() - { - // Update Protocol.md with the contents from blob 583f1... - string command = $"update-index --cacheinfo 100644 \"583f1a56db7cc884d54534c5d9c56b93a1e00a2b\n\" Protocol.md"; - - this.ValidateGitCommand(command); - } - - protected override void CreateEnlistment() - { - base.CreateEnlistment(); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); - this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.FunctionalTests.Tools; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class UpdateIndexTests : GitRepoTests + { + public UpdateIndexTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + [Ignore("TODO 940287: git update-index --remove does not check if the file is on disk if the skip-worktree bit is set")] + public void UpdateIndexRemoveFileOnDisk() + { + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + this.ValidateGitCommand("update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + } + + [TestCase] + public void UpdateIndexRemoveFileOnDiskDontCheckStatus() + { + // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + + // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, + // meaning it will always remove the file from the index + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + // Add the files back to the index so the git-status that is run during teardown matches + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + } + + [TestCase] + public void UpdateIndexRemoveAddFileOpenForWrite() + { + // TODO 940287: Remove this test and re-enable UpdateIndexRemoveFileOnDisk + this.ValidateGitCommand("checkout " + GitRepoTests.ConflictTargetBranch); + + // git-status will not match because update-index --remove does not check what is on disk if the skip-worktree bit is set, + // meaning it will always remove the file from the index + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --remove Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + this.FilesShouldMatchCheckoutOfTargetBranch(); + + // Add the files back to the index so the git-status that is run during teardown matches + GitProcess.InvokeProcess(this.ControlGitRepo.RootPath, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + GitHelpers.InvokeGitAgainstScalarRepo(this.Enlistment.RepoRoot, "update-index --add Test_ConflictTests/AddedFiles/AddedByBothDifferentContent.txt"); + } + + [TestCase] + public void UpdateIndexWithCacheInfo() + { + // Update Protocol.md with the contents from blob 583f1... + string command = $"update-index --cacheinfo 100644 \"583f1a56db7cc884d54534c5d9c56b93a1e00a2b\n\" Protocol.md"; + + this.ValidateGitCommand(command); + } + + protected override void CreateEnlistment() + { + base.CreateEnlistment(); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictTargetBranch); + this.ControlGitRepo.Fetch(GitRepoTests.ConflictSourceBranch); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs b/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs index 5725724a93..2f7866528c 100644 --- a/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs +++ b/Scalar.FunctionalTests/Tests/GitCommands/UpdateRefTests.cs @@ -1,40 +1,40 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; - -namespace Scalar.FunctionalTests.Tests.GitCommands -{ - [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] - [Category(Categories.GitCommands)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class UpdateRefTests : GitRepoTests - { - public UpdateRefTests(Settings.ValidateWorkingTreeMode validateWorkingTree) - : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) - { - } - - [TestCase] - public void UpdateRefModifiesHead() - { - this.ValidateGitCommand("status"); - this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); - } - - [TestCase] - public void UpdateRefModifiesHeadThenResets() - { - this.ValidateGitCommand("status"); - this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); - this.ValidateGitCommand("reset HEAD"); - } - - public override void TearDownForTest() - { - // Need to ignore case changes in this test because the update-ref will have - // folder names that only changed the case and when checking the folder structure - // it will create partial folders with that case and will not get updated to the - // previous case when the reset --hard running in the tear down step - this.TestValidationAndCleanup(ignoreCase: true); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; + +namespace Scalar.FunctionalTests.Tests.GitCommands +{ + [TestFixtureSource(typeof(GitRepoTests), nameof(GitRepoTests.ValidateWorkingTree))] + [Category(Categories.GitCommands)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class UpdateRefTests : GitRepoTests + { + public UpdateRefTests(Settings.ValidateWorkingTreeMode validateWorkingTree) + : base(enlistmentPerTest: true, validateWorkingTree: validateWorkingTree) + { + } + + [TestCase] + public void UpdateRefModifiesHead() + { + this.ValidateGitCommand("status"); + this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + } + + [TestCase] + public void UpdateRefModifiesHeadThenResets() + { + this.ValidateGitCommand("status"); + this.ValidateGitCommand("update-ref HEAD f1bce402a7a980a8320f3f235cf8c8fdade4b17a"); + this.ValidateGitCommand("reset HEAD"); + } + + public override void TearDownForTest() + { + // Need to ignore case changes in this test because the update-ref will have + // folder names that only changed the case and when checking the folder structure + // it will create partial folders with that case and will not get updated to the + // previous case when the reset --hard running in the tear down step + this.TestValidationAndCleanup(ignoreCase: true); + } + } +} diff --git a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ConfigVerbTests.cs b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ConfigVerbTests.cs index ff66e4189c..50e09d8f9e 100644 --- a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ConfigVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ConfigVerbTests.cs @@ -1,172 +1,172 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; - -namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.MacTODO.NeedsScalarConfig)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class ConfigVerbTests : TestsWithMultiEnlistment - { - private const string IntegerSettingKey = "functionalTest_Integer"; - private const string FloatSettingKey = "functionalTest_Float"; - private const string RegularStringSettingKey = "functionalTest_RegularString"; - private const string SpacedStringSettingKey = "functionalTest_SpacedString"; - private const string SpacesOnlyStringSettingKey = "functionalTest_SpacesOnlyString"; - private const string EmptyStringSettingKey = "functionalTest_EmptyString"; - private const string NonExistentSettingKey = "functionalTest_NonExistentSetting"; - - private const int GenericErrorExitCode = 3; - - private readonly Dictionary initialSettings = new Dictionary() - { - { IntegerSettingKey, "213" }, - { FloatSettingKey, "213.15" }, - { RegularStringSettingKey, "foobar" }, - { SpacedStringSettingKey, "quick brown fox" } - }; - - private readonly Dictionary updateSettings = new Dictionary() - { - { IntegerSettingKey, "32123" }, - { FloatSettingKey, "3.14159" }, - { RegularStringSettingKey, "helloWorld!" }, - { SpacedStringSettingKey, "jumped over lazy dog" } - }; - - [OneTimeSetUp] - public void ResetTestConfig() - { - this.DeleteSettings(this.initialSettings); - this.DeleteSettings(this.updateSettings); - } - - [TestCase, Order(1)] - public void CreateSettings() - { - this.ApplySettings(this.initialSettings); - this.ConfigShouldContainSettings(this.initialSettings); - } - - [TestCase, Order(2)] - public void UpdateSettings() - { - this.ApplySettings(this.updateSettings); - this.ConfigShouldContainSettings(this.updateSettings); - } - - [TestCase, Order(3)] - public void ListSettings() - { - this.ConfigShouldContainSettings(this.updateSettings); - } - - [TestCase, Order(4)] - public void ReadSingleSetting() - { - foreach (KeyValuePair setting in this.updateSettings) - { - string value = this.RunConfigCommand($"{setting.Key}"); - value.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual($"{setting.Value}"); - } - } - - [TestCase, Order(5)] - public void AddSpaceValueSetting() - { - string writeSpacesValue = " "; - this.WriteSetting(SpacesOnlyStringSettingKey, writeSpacesValue); - - string readSpacesValue = this.ReadSetting($"{SpacesOnlyStringSettingKey}"); - readSpacesValue.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual(writeSpacesValue); - } - - [TestCase, Order(6)] - public void AddNullValueSetting() - { - string writeEmptyValue = string.Empty; - this.WriteSetting(EmptyStringSettingKey, writeEmptyValue, GenericErrorExitCode); - - string readEmptyValue = this.ReadSetting(EmptyStringSettingKey, GenericErrorExitCode); - readEmptyValue.ShouldBeEmpty(); - } - - [TestCase, Order(7)] - public void ReadNonExistentSetting() - { - string nonExistentValue = this.ReadSetting(NonExistentSettingKey, GenericErrorExitCode); - nonExistentValue.ShouldBeEmpty(); - } - - [TestCase, Order(8)] - public void DeleteSettings() - { - this.DeleteSettings(this.updateSettings); - - List deletedLines = new List(); - foreach (KeyValuePair setting in this.updateSettings) - { - deletedLines.Add(this.GetSettingLineInConfigFileFormat(setting)); - } - - string allSettings = this.RunConfigCommand("--list"); - allSettings.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: deletedLines.ToArray()); - } - - private void DeleteSettings(Dictionary settings) - { - List deletedLines = new List(); - foreach (KeyValuePair setting in settings) - { - this.RunConfigCommand($"--delete {setting.Key}"); - } - } - - private void ConfigShouldContainSettings(Dictionary expectedSettings) - { - List expectedLines = new List(); - foreach (KeyValuePair setting in expectedSettings) - { - expectedLines.Add(this.GetSettingLineInConfigFileFormat(setting)); - } - - string allSettings = this.RunConfigCommand("--list"); - allSettings.ShouldContain(expectedLines.ToArray()); - } - - private string GetSettingLineInConfigFileFormat(KeyValuePair setting) - { - return $"{setting.Key}={setting.Value}"; - } - - private void ApplySettings(Dictionary settings) - { - foreach (KeyValuePair setting in settings) - { - this.WriteSetting(setting.Key, setting.Value); - } - } - - private void WriteSetting(string key, string value, int expectedExitCode = 0) - { - this.RunConfigCommand($"{key} \"{value}\"", expectedExitCode); - } - - private string ReadSetting(string key, int expectedExitCode = 0) - { - return this.RunConfigCommand($"{key}", expectedExitCode); - } - - private string RunConfigCommand(string argument, int expectedExitCode = 0) - { - ProcessResult result = ProcessHelper.Run(ScalarTestConfig.PathToScalar, $"config {argument}"); - result.ExitCode.ShouldEqual(expectedExitCode, result.Errors); - - return result.Output; - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; + +namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.MacTODO.NeedsScalarConfig)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class ConfigVerbTests : TestsWithMultiEnlistment + { + private const string IntegerSettingKey = "functionalTest_Integer"; + private const string FloatSettingKey = "functionalTest_Float"; + private const string RegularStringSettingKey = "functionalTest_RegularString"; + private const string SpacedStringSettingKey = "functionalTest_SpacedString"; + private const string SpacesOnlyStringSettingKey = "functionalTest_SpacesOnlyString"; + private const string EmptyStringSettingKey = "functionalTest_EmptyString"; + private const string NonExistentSettingKey = "functionalTest_NonExistentSetting"; + + private const int GenericErrorExitCode = 3; + + private readonly Dictionary initialSettings = new Dictionary() + { + { IntegerSettingKey, "213" }, + { FloatSettingKey, "213.15" }, + { RegularStringSettingKey, "foobar" }, + { SpacedStringSettingKey, "quick brown fox" } + }; + + private readonly Dictionary updateSettings = new Dictionary() + { + { IntegerSettingKey, "32123" }, + { FloatSettingKey, "3.14159" }, + { RegularStringSettingKey, "helloWorld!" }, + { SpacedStringSettingKey, "jumped over lazy dog" } + }; + + [OneTimeSetUp] + public void ResetTestConfig() + { + this.DeleteSettings(this.initialSettings); + this.DeleteSettings(this.updateSettings); + } + + [TestCase, Order(1)] + public void CreateSettings() + { + this.ApplySettings(this.initialSettings); + this.ConfigShouldContainSettings(this.initialSettings); + } + + [TestCase, Order(2)] + public void UpdateSettings() + { + this.ApplySettings(this.updateSettings); + this.ConfigShouldContainSettings(this.updateSettings); + } + + [TestCase, Order(3)] + public void ListSettings() + { + this.ConfigShouldContainSettings(this.updateSettings); + } + + [TestCase, Order(4)] + public void ReadSingleSetting() + { + foreach (KeyValuePair setting in this.updateSettings) + { + string value = this.RunConfigCommand($"{setting.Key}"); + value.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual($"{setting.Value}"); + } + } + + [TestCase, Order(5)] + public void AddSpaceValueSetting() + { + string writeSpacesValue = " "; + this.WriteSetting(SpacesOnlyStringSettingKey, writeSpacesValue); + + string readSpacesValue = this.ReadSetting($"{SpacesOnlyStringSettingKey}"); + readSpacesValue.TrimEnd(Environment.NewLine.ToCharArray()).ShouldEqual(writeSpacesValue); + } + + [TestCase, Order(6)] + public void AddNullValueSetting() + { + string writeEmptyValue = string.Empty; + this.WriteSetting(EmptyStringSettingKey, writeEmptyValue, GenericErrorExitCode); + + string readEmptyValue = this.ReadSetting(EmptyStringSettingKey, GenericErrorExitCode); + readEmptyValue.ShouldBeEmpty(); + } + + [TestCase, Order(7)] + public void ReadNonExistentSetting() + { + string nonExistentValue = this.ReadSetting(NonExistentSettingKey, GenericErrorExitCode); + nonExistentValue.ShouldBeEmpty(); + } + + [TestCase, Order(8)] + public void DeleteSettings() + { + this.DeleteSettings(this.updateSettings); + + List deletedLines = new List(); + foreach (KeyValuePair setting in this.updateSettings) + { + deletedLines.Add(this.GetSettingLineInConfigFileFormat(setting)); + } + + string allSettings = this.RunConfigCommand("--list"); + allSettings.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: deletedLines.ToArray()); + } + + private void DeleteSettings(Dictionary settings) + { + List deletedLines = new List(); + foreach (KeyValuePair setting in settings) + { + this.RunConfigCommand($"--delete {setting.Key}"); + } + } + + private void ConfigShouldContainSettings(Dictionary expectedSettings) + { + List expectedLines = new List(); + foreach (KeyValuePair setting in expectedSettings) + { + expectedLines.Add(this.GetSettingLineInConfigFileFormat(setting)); + } + + string allSettings = this.RunConfigCommand("--list"); + allSettings.ShouldContain(expectedLines.ToArray()); + } + + private string GetSettingLineInConfigFileFormat(KeyValuePair setting) + { + return $"{setting.Key}={setting.Value}"; + } + + private void ApplySettings(Dictionary settings) + { + foreach (KeyValuePair setting in settings) + { + this.WriteSetting(setting.Key, setting.Value); + } + } + + private void WriteSetting(string key, string value, int expectedExitCode = 0) + { + this.RunConfigCommand($"{key} \"{value}\"", expectedExitCode); + } + + private string ReadSetting(string key, int expectedExitCode = 0) + { + return this.RunConfigCommand($"{key}", expectedExitCode); + } + + private string RunConfigCommand(string argument, int expectedExitCode = 0) + { + ProcessResult result = ProcessHelper.Run(ScalarTestConfig.PathToScalar, $"config {argument}"); + result.ExitCode.ShouldEqual(expectedExitCode, result.Errors); + + return result.Output; + } + } +} diff --git a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs index 15ec007fc0..ad65ff72ec 100644 --- a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/ServiceVerbTests.cs @@ -1,105 +1,105 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; - -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() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(); - ScalarFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(); - - string[] repoRootList = new string[] { enlistment1.EnlistmentRoot, enlistment2.EnlistmentRoot }; - - ScalarProcess scalarProcess1 = new ScalarProcess( - ScalarTestConfig.PathToScalar, - enlistment1.EnlistmentRoot, - enlistment1.LocalCacheRoot); - - ScalarProcess scalarProcess2 = new ScalarProcess( - ScalarTestConfig.PathToScalar, - 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); - } - - private void RunServiceCommandAndCheckOutput(string argument, string[] expectedRepoRoots, string[] unexpectedRepoRoots = null) - { - ScalarProcess scalarProcess = new ScalarProcess( - ScalarTestConfig.PathToScalar, - enlistmentRoot: null, - localCacheRoot: null); - - string result = scalarProcess.RunServiceVerb(argument); - result.ShouldContain(expectedRepoRoots); - - if (unexpectedRepoRoots != null) - { - result.ShouldNotContain(false, unexpectedRepoRoots); - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; + +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() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(); + ScalarFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(); + + string[] repoRootList = new string[] { enlistment1.EnlistmentRoot, enlistment2.EnlistmentRoot }; + + ScalarProcess scalarProcess1 = new ScalarProcess( + ScalarTestConfig.PathToScalar, + enlistment1.EnlistmentRoot, + enlistment1.LocalCacheRoot); + + ScalarProcess scalarProcess2 = new ScalarProcess( + ScalarTestConfig.PathToScalar, + 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); + } + + private void RunServiceCommandAndCheckOutput(string argument, string[] expectedRepoRoots, string[] unexpectedRepoRoots = null) + { + ScalarProcess scalarProcess = new ScalarProcess( + ScalarTestConfig.PathToScalar, + enlistmentRoot: null, + localCacheRoot: null); + + string result = scalarProcess.RunServiceVerb(argument); + result.ShouldContain(expectedRepoRoots); + + if (unexpectedRepoRoots != null) + { + result.ShouldNotContain(false, unexpectedRepoRoots); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs index 6eee8bf4d7..03e6e5ba85 100644 --- a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs +++ b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/SharedCacheTests.cs @@ -1,327 +1,327 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests -{ - [TestFixture] - [Category(Categories.ExtraCoverage)] - [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] - public class SharedCacheTests : TestsWithMultiEnlistment - { - private const string WellKnownFile = "Readme.md"; - - // This branch and commit sha should point to the same place. - private const string WellKnownBranch = "FunctionalTests/20170602"; - private const string WellKnownCommitSha = "42eb6632beffae26893a3d6e1a9f48d652327c6f"; - - private string localCachePath; - private string localCacheParentPath; - - private FileSystemRunner fileSystem; - - public SharedCacheTests() - { - this.fileSystem = new SystemIORunner(); - } - - [SetUp] - public void SetCacheLocation() - { - this.localCacheParentPath = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", Guid.NewGuid().ToString("N")); - this.localCachePath = Path.Combine(this.localCacheParentPath, ".customScalarCache"); - } - - [TestCase] - public void SecondCloneDoesNotDownloadAdditionalObjects() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile)); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); - - string[] allObjects = Directory.EnumerateFiles(enlistment1.LocalCacheRoot, "*", SearchOption.AllDirectories).ToArray(); - - ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); - File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); - - enlistment2.LocalCacheRoot.ShouldEqual(enlistment1.LocalCacheRoot, "Sanity: Local cache roots are expected to match."); - Directory.EnumerateFiles(enlistment2.LocalCacheRoot, "*", SearchOption.AllDirectories) - .ShouldMatchInOrder(allObjects); - } - - [TestCase] - public void RepairFixesCorruptBlobSizesDatabase() - { - ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); - enlistment.UnmountScalar(); - - // Repair on a healthy enlistment should succeed - enlistment.Repair(confirm: true); - - string blobSizesRoot = ScalarHelpers.GetPersistedBlobSizesRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); - string blobSizesDbPath = Path.Combine(blobSizesRoot, "BlobSizes.sql"); - blobSizesDbPath.ShouldBeAFile(this.fileSystem); - this.fileSystem.WriteAllText(blobSizesDbPath, "0000"); - - enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when blob size db is corrupt"); - enlistment.Repair(confirm: true); - enlistment.MountScalar(); - } - - [TestCase] - [Category(Categories.MacTODO.NeedsServiceVerb)] - public void CloneCleansUpStaleMetadataLock() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - string metadataLockPath = Path.Combine(this.localCachePath, "mapping.dat.lock"); - metadataLockPath.ShouldNotExistOnDisk(this.fileSystem); - this.fileSystem.WriteAllText(metadataLockPath, enlistment1.EnlistmentRoot); - metadataLockPath.ShouldBeAFile(this.fileSystem); - - ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); - metadataLockPath.ShouldNotExistOnDisk(this.fileSystem); - - enlistment1.Status().ShouldContain("Mount status: Ready"); - enlistment2.Status().ShouldContain("Mount status: Ready"); - } - - [TestCase] - public void ParallelReadsInASharedCache() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); - ScalarFunctionalTestEnlistment enlistment3 = null; - - Task task1 = Task.Run(() => this.HydrateEntireRepo(enlistment1)); - Task task2 = Task.Run(() => this.HydrateEntireRepo(enlistment2)); - Task task3 = Task.Run(() => enlistment3 = this.CloneAndMountEnlistment()); - - task1.Wait(); - task2.Wait(); - task3.Wait(); - - task1.Exception.ShouldBeNull(); - task2.Exception.ShouldBeNull(); - task3.Exception.ShouldBeNull(); - - enlistment1.Status().ShouldContain("Mount status: Ready"); - enlistment2.Status().ShouldContain("Mount status: Ready"); - enlistment3.Status().ShouldContain("Mount status: Ready"); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment3); - } - - [TestCase] - public void DeleteObjectsCacheAndCacheMappingBeforeMount() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); - - enlistment1.UnmountScalar(); - - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment1.DotScalarRoot).ShouldNotBeNull(); - objectsRoot.ShouldBeADirectory(this.fileSystem); - RepositoryHelpers.DeleteTestDirectory(objectsRoot); - - string metadataPath = Path.Combine(this.localCachePath, "mapping.dat"); - metadataPath.ShouldBeAFile(this.fileSystem); - this.fileSystem.DeleteFile(metadataPath); - - enlistment1.MountScalar(); - - Task task1 = Task.Run(() => this.HydrateRootFolder(enlistment1)); - Task task2 = Task.Run(() => this.HydrateRootFolder(enlistment2)); - task1.Wait(); - task2.Wait(); - task1.Exception.ShouldBeNull(); - task2.Exception.ShouldBeNull(); - - enlistment1.Status().ShouldContain("Mount status: Ready"); - enlistment2.Status().ShouldContain("Mount status: Ready"); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); - } - - [TestCase] - public void DeleteCacheDuringHydrations() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment1.DotScalarRoot).ShouldNotBeNull(); - objectsRoot.ShouldBeADirectory(this.fileSystem); - - Task task1 = Task.Run(() => - { - this.HydrateEntireRepo(enlistment1); - }); - - while (!task1.IsCompleted) - { - try - { - // Delete objectsRoot rather than this.localCachePath as the blob sizes database cannot be deleted while Scalar is mounted - RepositoryHelpers.DeleteTestDirectory(objectsRoot); - Thread.Sleep(100); - } - catch (IOException) - { - // Hydration may have handles into the cache, so failing this delete is expected. - } - } - - task1.Exception.ShouldBeNull(); - - enlistment1.Status().ShouldContain("Mount status: Ready"); - } - - [TestCase] - public void DownloadingACommitWithoutTreesDoesntBreakNextClone() - { - ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); - GitProcess.Invoke(enlistment1.RepoRoot, "cat-file -s " + WellKnownCommitSha).ShouldEqual("293\n"); - - ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(WellKnownBranch); - enlistment2.Status().ShouldContain("Mount status: Ready"); - } - - [TestCase] - public void MountReusesLocalCacheKeyWhenGitObjectsRootDeleted() - { - ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); - - enlistment.UnmountScalar(); - - // Find the current git objects root and ensure it's on disk - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); - objectsRoot.ShouldBeADirectory(this.fileSystem); - - string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat"); - string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath); - mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty"); - - // Delete the git objects root folder, mount should re-create it and the mapping.dat file should not change - RepositoryHelpers.DeleteTestDirectory(objectsRoot); - - enlistment.MountScalar(); - - ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldEqual(objectsRoot); - objectsRoot.ShouldBeADirectory(this.fileSystem); - mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents(mappingFileContents); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment); - } - - [TestCase] - public void MountUsesNewLocalCacheKeyWhenLocalCacheDeleted() - { - ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); - - enlistment.UnmountScalar(); - - // Find the current git objects root and ensure it's on disk - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); - objectsRoot.ShouldBeADirectory(this.fileSystem); - - string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat"); - string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath); - mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty"); - - // Delete the local cache folder, mount should re-create it and generate a new mapping file and local cache key - RepositoryHelpers.DeleteTestDirectory(enlistment.LocalCacheRoot); - - enlistment.MountScalar(); - - // Mount should recreate the local cache root - enlistment.LocalCacheRoot.ShouldBeADirectory(this.fileSystem); - - // Determine the new local cache key - string newMappingFileContents = mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents(); - const int GuidStringLength = 32; - string mappingFileKey = "A {\"Key\":\"https://scalar.visualstudio.com/ci/_git/fortests\",\"Value\":\""; - int localKeyIndex = newMappingFileContents.IndexOf(mappingFileKey); - string newCacheKey = newMappingFileContents.Substring(localKeyIndex + mappingFileKey.Length, GuidStringLength); - - // Validate the new objects root is on disk and uses the new key - objectsRoot.ShouldNotExistOnDisk(this.fileSystem); - string newObjectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot); - newObjectsRoot.ShouldNotEqual(objectsRoot); - newObjectsRoot.ShouldContain(newCacheKey); - newObjectsRoot.ShouldBeADirectory(this.fileSystem); - - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment); - } - - [TestCase] - public void SecondCloneSucceedsWithMissingTrees() - { - string newCachePath = Path.Combine(this.localCacheParentPath, ".customGvfsCache2"); - ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(localCacheRoot: newCachePath, skipPrefetch: true); - File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile)); - this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); - - // This Git command loads the commit and root tree for WellKnownCommitSha, - // but does not download any more reachable objects. - string command = "cat-file -p origin/" + WellKnownBranch + "^{tree}"; - ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo(enlistment1.RepoRoot, command); - result.ExitCode.ShouldEqual(0, $"git {command} failed with error: " + result.Errors); - - // If we did not properly check the failed checkout at this step, then clone will fail during checkout. - ScalarFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(localCacheRoot: newCachePath, branch: WellKnownBranch, skipPrefetch: true); - File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); - } - - // Override OnTearDownEnlistmentsDeleted rathern than using [TearDown] as the enlistments need to be unmounted before - // localCacheParentPath can be deleted (as the SQLite blob sizes database cannot be deleted while Scalar is mounted) - protected override void OnTearDownEnlistmentsDeleted() - { - RepositoryHelpers.DeleteTestDirectory(this.localCacheParentPath); - } - - private ScalarFunctionalTestEnlistment CloneAndMountEnlistment(string branch = null) - { - return this.CreateNewEnlistment(this.localCachePath, branch); - } - - private void AlternatesFileShouldHaveGitObjectsRoot(ScalarFunctionalTestEnlistment enlistment) - { - string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot); - string alternatesFileContents = Path.Combine(enlistment.RepoRoot, ".git", "objects", "info", "alternates").ShouldBeAFile(this.fileSystem).WithContents(); - alternatesFileContents.ShouldEqual(objectsRoot); - } - - private void HydrateRootFolder(ScalarFunctionalTestEnlistment enlistment) - { - List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.TopDirectoryOnly).ToList(); - for (int i = 0; i < allFiles.Count; ++i) - { - File.ReadAllText(allFiles[i]); - } - } - - private void HydrateEntireRepo(ScalarFunctionalTestEnlistment enlistment) - { - List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.AllDirectories).ToList(); - for (int i = 0; i < allFiles.Count; ++i) - { - if (!allFiles[i].StartsWith(enlistment.RepoRoot + "\\.git\\", StringComparison.OrdinalIgnoreCase)) - { - File.ReadAllText(allFiles[i]); - } - } - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests +{ + [TestFixture] + [Category(Categories.ExtraCoverage)] + [Category(Categories.NeedsUpdatesForNonVirtualizedMode)] + public class SharedCacheTests : TestsWithMultiEnlistment + { + private const string WellKnownFile = "Readme.md"; + + // This branch and commit sha should point to the same place. + private const string WellKnownBranch = "FunctionalTests/20170602"; + private const string WellKnownCommitSha = "42eb6632beffae26893a3d6e1a9f48d652327c6f"; + + private string localCachePath; + private string localCacheParentPath; + + private FileSystemRunner fileSystem; + + public SharedCacheTests() + { + this.fileSystem = new SystemIORunner(); + } + + [SetUp] + public void SetCacheLocation() + { + this.localCacheParentPath = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", Guid.NewGuid().ToString("N")); + this.localCachePath = Path.Combine(this.localCacheParentPath, ".customScalarCache"); + } + + [TestCase] + public void SecondCloneDoesNotDownloadAdditionalObjects() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile)); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); + + string[] allObjects = Directory.EnumerateFiles(enlistment1.LocalCacheRoot, "*", SearchOption.AllDirectories).ToArray(); + + ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); + File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); + + enlistment2.LocalCacheRoot.ShouldEqual(enlistment1.LocalCacheRoot, "Sanity: Local cache roots are expected to match."); + Directory.EnumerateFiles(enlistment2.LocalCacheRoot, "*", SearchOption.AllDirectories) + .ShouldMatchInOrder(allObjects); + } + + [TestCase] + public void RepairFixesCorruptBlobSizesDatabase() + { + ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); + enlistment.UnmountScalar(); + + // Repair on a healthy enlistment should succeed + enlistment.Repair(confirm: true); + + string blobSizesRoot = ScalarHelpers.GetPersistedBlobSizesRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); + string blobSizesDbPath = Path.Combine(blobSizesRoot, "BlobSizes.sql"); + blobSizesDbPath.ShouldBeAFile(this.fileSystem); + this.fileSystem.WriteAllText(blobSizesDbPath, "0000"); + + enlistment.TryMountScalar().ShouldEqual(false, "Scalar shouldn't mount when blob size db is corrupt"); + enlistment.Repair(confirm: true); + enlistment.MountScalar(); + } + + [TestCase] + [Category(Categories.MacTODO.NeedsServiceVerb)] + public void CloneCleansUpStaleMetadataLock() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + string metadataLockPath = Path.Combine(this.localCachePath, "mapping.dat.lock"); + metadataLockPath.ShouldNotExistOnDisk(this.fileSystem); + this.fileSystem.WriteAllText(metadataLockPath, enlistment1.EnlistmentRoot); + metadataLockPath.ShouldBeAFile(this.fileSystem); + + ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); + metadataLockPath.ShouldNotExistOnDisk(this.fileSystem); + + enlistment1.Status().ShouldContain("Mount status: Ready"); + enlistment2.Status().ShouldContain("Mount status: Ready"); + } + + [TestCase] + public void ParallelReadsInASharedCache() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); + ScalarFunctionalTestEnlistment enlistment3 = null; + + Task task1 = Task.Run(() => this.HydrateEntireRepo(enlistment1)); + Task task2 = Task.Run(() => this.HydrateEntireRepo(enlistment2)); + Task task3 = Task.Run(() => enlistment3 = this.CloneAndMountEnlistment()); + + task1.Wait(); + task2.Wait(); + task3.Wait(); + + task1.Exception.ShouldBeNull(); + task2.Exception.ShouldBeNull(); + task3.Exception.ShouldBeNull(); + + enlistment1.Status().ShouldContain("Mount status: Ready"); + enlistment2.Status().ShouldContain("Mount status: Ready"); + enlistment3.Status().ShouldContain("Mount status: Ready"); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment3); + } + + [TestCase] + public void DeleteObjectsCacheAndCacheMappingBeforeMount() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(); + + enlistment1.UnmountScalar(); + + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment1.DotScalarRoot).ShouldNotBeNull(); + objectsRoot.ShouldBeADirectory(this.fileSystem); + RepositoryHelpers.DeleteTestDirectory(objectsRoot); + + string metadataPath = Path.Combine(this.localCachePath, "mapping.dat"); + metadataPath.ShouldBeAFile(this.fileSystem); + this.fileSystem.DeleteFile(metadataPath); + + enlistment1.MountScalar(); + + Task task1 = Task.Run(() => this.HydrateRootFolder(enlistment1)); + Task task2 = Task.Run(() => this.HydrateRootFolder(enlistment2)); + task1.Wait(); + task2.Wait(); + task1.Exception.ShouldBeNull(); + task2.Exception.ShouldBeNull(); + + enlistment1.Status().ShouldContain("Mount status: Ready"); + enlistment2.Status().ShouldContain("Mount status: Ready"); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment2); + } + + [TestCase] + public void DeleteCacheDuringHydrations() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment1.DotScalarRoot).ShouldNotBeNull(); + objectsRoot.ShouldBeADirectory(this.fileSystem); + + Task task1 = Task.Run(() => + { + this.HydrateEntireRepo(enlistment1); + }); + + while (!task1.IsCompleted) + { + try + { + // Delete objectsRoot rather than this.localCachePath as the blob sizes database cannot be deleted while Scalar is mounted + RepositoryHelpers.DeleteTestDirectory(objectsRoot); + Thread.Sleep(100); + } + catch (IOException) + { + // Hydration may have handles into the cache, so failing this delete is expected. + } + } + + task1.Exception.ShouldBeNull(); + + enlistment1.Status().ShouldContain("Mount status: Ready"); + } + + [TestCase] + public void DownloadingACommitWithoutTreesDoesntBreakNextClone() + { + ScalarFunctionalTestEnlistment enlistment1 = this.CloneAndMountEnlistment(); + GitProcess.Invoke(enlistment1.RepoRoot, "cat-file -s " + WellKnownCommitSha).ShouldEqual("293\n"); + + ScalarFunctionalTestEnlistment enlistment2 = this.CloneAndMountEnlistment(WellKnownBranch); + enlistment2.Status().ShouldContain("Mount status: Ready"); + } + + [TestCase] + public void MountReusesLocalCacheKeyWhenGitObjectsRootDeleted() + { + ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); + + enlistment.UnmountScalar(); + + // Find the current git objects root and ensure it's on disk + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); + objectsRoot.ShouldBeADirectory(this.fileSystem); + + string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat"); + string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath); + mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty"); + + // Delete the git objects root folder, mount should re-create it and the mapping.dat file should not change + RepositoryHelpers.DeleteTestDirectory(objectsRoot); + + enlistment.MountScalar(); + + ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldEqual(objectsRoot); + objectsRoot.ShouldBeADirectory(this.fileSystem); + mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents(mappingFileContents); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment); + } + + [TestCase] + public void MountUsesNewLocalCacheKeyWhenLocalCacheDeleted() + { + ScalarFunctionalTestEnlistment enlistment = this.CloneAndMountEnlistment(); + + enlistment.UnmountScalar(); + + // Find the current git objects root and ensure it's on disk + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot).ShouldNotBeNull(); + objectsRoot.ShouldBeADirectory(this.fileSystem); + + string mappingFilePath = Path.Combine(enlistment.LocalCacheRoot, "mapping.dat"); + string mappingFileContents = this.fileSystem.ReadAllText(mappingFilePath); + mappingFileContents.Length.ShouldNotEqual(0, "mapping.dat should not be empty"); + + // Delete the local cache folder, mount should re-create it and generate a new mapping file and local cache key + RepositoryHelpers.DeleteTestDirectory(enlistment.LocalCacheRoot); + + enlistment.MountScalar(); + + // Mount should recreate the local cache root + enlistment.LocalCacheRoot.ShouldBeADirectory(this.fileSystem); + + // Determine the new local cache key + string newMappingFileContents = mappingFilePath.ShouldBeAFile(this.fileSystem).WithContents(); + const int GuidStringLength = 32; + string mappingFileKey = "A {\"Key\":\"https://scalar.visualstudio.com/ci/_git/fortests\",\"Value\":\""; + int localKeyIndex = newMappingFileContents.IndexOf(mappingFileKey); + string newCacheKey = newMappingFileContents.Substring(localKeyIndex + mappingFileKey.Length, GuidStringLength); + + // Validate the new objects root is on disk and uses the new key + objectsRoot.ShouldNotExistOnDisk(this.fileSystem); + string newObjectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot); + newObjectsRoot.ShouldNotEqual(objectsRoot); + newObjectsRoot.ShouldContain(newCacheKey); + newObjectsRoot.ShouldBeADirectory(this.fileSystem); + + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment); + } + + [TestCase] + public void SecondCloneSucceedsWithMissingTrees() + { + string newCachePath = Path.Combine(this.localCacheParentPath, ".customGvfsCache2"); + ScalarFunctionalTestEnlistment enlistment1 = this.CreateNewEnlistment(localCacheRoot: newCachePath, skipPrefetch: true); + File.ReadAllText(Path.Combine(enlistment1.RepoRoot, WellKnownFile)); + this.AlternatesFileShouldHaveGitObjectsRoot(enlistment1); + + // This Git command loads the commit and root tree for WellKnownCommitSha, + // but does not download any more reachable objects. + string command = "cat-file -p origin/" + WellKnownBranch + "^{tree}"; + ProcessResult result = GitHelpers.InvokeGitAgainstScalarRepo(enlistment1.RepoRoot, command); + result.ExitCode.ShouldEqual(0, $"git {command} failed with error: " + result.Errors); + + // If we did not properly check the failed checkout at this step, then clone will fail during checkout. + ScalarFunctionalTestEnlistment enlistment2 = this.CreateNewEnlistment(localCacheRoot: newCachePath, branch: WellKnownBranch, skipPrefetch: true); + File.ReadAllText(Path.Combine(enlistment2.RepoRoot, WellKnownFile)); + } + + // Override OnTearDownEnlistmentsDeleted rathern than using [TearDown] as the enlistments need to be unmounted before + // localCacheParentPath can be deleted (as the SQLite blob sizes database cannot be deleted while Scalar is mounted) + protected override void OnTearDownEnlistmentsDeleted() + { + RepositoryHelpers.DeleteTestDirectory(this.localCacheParentPath); + } + + private ScalarFunctionalTestEnlistment CloneAndMountEnlistment(string branch = null) + { + return this.CreateNewEnlistment(this.localCachePath, branch); + } + + private void AlternatesFileShouldHaveGitObjectsRoot(ScalarFunctionalTestEnlistment enlistment) + { + string objectsRoot = ScalarHelpers.GetPersistedGitObjectsRoot(enlistment.DotScalarRoot); + string alternatesFileContents = Path.Combine(enlistment.RepoRoot, ".git", "objects", "info", "alternates").ShouldBeAFile(this.fileSystem).WithContents(); + alternatesFileContents.ShouldEqual(objectsRoot); + } + + private void HydrateRootFolder(ScalarFunctionalTestEnlistment enlistment) + { + List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.TopDirectoryOnly).ToList(); + for (int i = 0; i < allFiles.Count; ++i) + { + File.ReadAllText(allFiles[i]); + } + } + + private void HydrateEntireRepo(ScalarFunctionalTestEnlistment enlistment) + { + List allFiles = Directory.EnumerateFiles(enlistment.RepoRoot, "*", SearchOption.AllDirectories).ToList(); + for (int i = 0; i < allFiles.Count; ++i) + { + if (!allFiles[i].StartsWith(enlistment.RepoRoot + "\\.git\\", StringComparison.OrdinalIgnoreCase)) + { + File.ReadAllText(allFiles[i]); + } + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/TestsWithMultiEnlistment.cs b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/TestsWithMultiEnlistment.cs index c80dc0ee80..7f5216dbe2 100644 --- a/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/TestsWithMultiEnlistment.cs +++ b/Scalar.FunctionalTests/Tests/MultiEnlistmentTests/TestsWithMultiEnlistment.cs @@ -1,45 +1,45 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Tools; -using System.Collections.Generic; - -namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests -{ - public class TestsWithMultiEnlistment - { - private List enlistmentsToDelete = new List(); - - [TearDown] - public void DeleteEnlistments() - { - foreach (ScalarFunctionalTestEnlistment enlistment in this.enlistmentsToDelete) - { - enlistment.UnmountAndDeleteAll(); - } - - this.OnTearDownEnlistmentsDeleted(); - - this.enlistmentsToDelete.Clear(); - } - - /// - /// Can be overridden for custom [TearDown] steps that occur after the test enlistements have been unmounted and deleted - /// - protected virtual void OnTearDownEnlistmentsDeleted() - { - } - - protected ScalarFunctionalTestEnlistment CreateNewEnlistment( - string localCacheRoot = null, - string branch = null, - bool skipPrefetch = false) - { - ScalarFunctionalTestEnlistment output = ScalarFunctionalTestEnlistment.CloneAndMount( - ScalarTestConfig.PathToScalar, - branch, - localCacheRoot, - skipPrefetch); - this.enlistmentsToDelete.Add(output); - return output; - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Tools; +using System.Collections.Generic; + +namespace Scalar.FunctionalTests.Tests.MultiEnlistmentTests +{ + public class TestsWithMultiEnlistment + { + private List enlistmentsToDelete = new List(); + + [TearDown] + public void DeleteEnlistments() + { + foreach (ScalarFunctionalTestEnlistment enlistment in this.enlistmentsToDelete) + { + enlistment.UnmountAndDeleteAll(); + } + + this.OnTearDownEnlistmentsDeleted(); + + this.enlistmentsToDelete.Clear(); + } + + /// + /// Can be overridden for custom [TearDown] steps that occur after the test enlistements have been unmounted and deleted + /// + protected virtual void OnTearDownEnlistmentsDeleted() + { + } + + protected ScalarFunctionalTestEnlistment CreateNewEnlistment( + string localCacheRoot = null, + string branch = null, + bool skipPrefetch = false) + { + ScalarFunctionalTestEnlistment output = ScalarFunctionalTestEnlistment.CloneAndMount( + ScalarTestConfig.PathToScalar, + branch, + localCacheRoot, + skipPrefetch); + this.enlistmentsToDelete.Add(output); + return output; + } + } +} diff --git a/Scalar.FunctionalTests/Tests/PrintTestCaseStats.cs b/Scalar.FunctionalTests/Tests/PrintTestCaseStats.cs index fbc8c9b296..60fa20ae6a 100644 --- a/Scalar.FunctionalTests/Tests/PrintTestCaseStats.cs +++ b/Scalar.FunctionalTests/Tests/PrintTestCaseStats.cs @@ -1,70 +1,70 @@ -using NUnit.Framework; -using NUnit.Framework.Interfaces; -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; - -[assembly: Scalar.FunctionalTests.Tests.PrintTestCaseStats] - -namespace Scalar.FunctionalTests.Tests -{ - public class PrintTestCaseStats : TestActionAttribute - { - private const string StartTimeKey = "StartTime"; - - private static ConcurrentDictionary fixtureRunTimes = new ConcurrentDictionary(); - private static ConcurrentDictionary testRunTimes = new ConcurrentDictionary(); - - public override ActionTargets Targets - { - get { return ActionTargets.Test; } - } - - public static void PrintRunTimeStats() - { - Console.WriteLine(); - Console.WriteLine("Fixture run times:"); - foreach (KeyValuePair fixture in fixtureRunTimes.OrderByDescending(kvp => kvp.Value)) - { - Console.WriteLine(" {0}\t{1}", fixture.Value, fixture.Key); - } - - Console.WriteLine(); - Console.WriteLine("Test case run times:"); - foreach (KeyValuePair testcase in testRunTimes.OrderByDescending(kvp => kvp.Value)) - { - Console.WriteLine(" {0}\t{1}", testcase.Value, testcase.Key); - } - } - - public override void BeforeTest(ITest test) - { - test.Properties.Add(StartTimeKey, DateTime.Now); - } - - public override void AfterTest(ITest test) - { - DateTime startTime = (DateTime)test.Properties.Get(StartTimeKey); - DateTime endTime = DateTime.Now; - TimeSpan duration = endTime - startTime; - string message = TestContext.CurrentContext.Result.Message; - TestStatus status = TestContext.CurrentContext.Result.Outcome.Status; - - Console.WriteLine("Test " + test.FullName); - Console.WriteLine($"{status} at {endTime.ToLongTimeString()} taking {duration}"); - if (status != TestStatus.Passed) - { - Console.WriteLine(message); - } - - Console.WriteLine(); - - fixtureRunTimes.AddOrUpdate( - test.ClassName, - duration, - (key, existingValue) => existingValue + duration); - testRunTimes.TryAdd(test.FullName, duration); - } - } +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +[assembly: Scalar.FunctionalTests.Tests.PrintTestCaseStats] + +namespace Scalar.FunctionalTests.Tests +{ + public class PrintTestCaseStats : TestActionAttribute + { + private const string StartTimeKey = "StartTime"; + + private static ConcurrentDictionary fixtureRunTimes = new ConcurrentDictionary(); + private static ConcurrentDictionary testRunTimes = new ConcurrentDictionary(); + + public override ActionTargets Targets + { + get { return ActionTargets.Test; } + } + + public static void PrintRunTimeStats() + { + Console.WriteLine(); + Console.WriteLine("Fixture run times:"); + foreach (KeyValuePair fixture in fixtureRunTimes.OrderByDescending(kvp => kvp.Value)) + { + Console.WriteLine(" {0}\t{1}", fixture.Value, fixture.Key); + } + + Console.WriteLine(); + Console.WriteLine("Test case run times:"); + foreach (KeyValuePair testcase in testRunTimes.OrderByDescending(kvp => kvp.Value)) + { + Console.WriteLine(" {0}\t{1}", testcase.Value, testcase.Key); + } + } + + public override void BeforeTest(ITest test) + { + test.Properties.Add(StartTimeKey, DateTime.Now); + } + + public override void AfterTest(ITest test) + { + DateTime startTime = (DateTime)test.Properties.Get(StartTimeKey); + DateTime endTime = DateTime.Now; + TimeSpan duration = endTime - startTime; + string message = TestContext.CurrentContext.Result.Message; + TestStatus status = TestContext.CurrentContext.Result.Outcome.Status; + + Console.WriteLine("Test " + test.FullName); + Console.WriteLine($"{status} at {endTime.ToLongTimeString()} taking {duration}"); + if (status != TestStatus.Passed) + { + Console.WriteLine(message); + } + + Console.WriteLine(); + + fixtureRunTimes.AddOrUpdate( + test.ClassName, + duration, + (key, existingValue) => existingValue + duration); + testRunTimes.TryAdd(test.FullName, duration); + } + } } diff --git a/Scalar.FunctionalTests/Tests/ScalarVerbTests.cs b/Scalar.FunctionalTests/Tests/ScalarVerbTests.cs index 7c8789c797..4c627183a4 100644 --- a/Scalar.FunctionalTests/Tests/ScalarVerbTests.cs +++ b/Scalar.FunctionalTests/Tests/ScalarVerbTests.cs @@ -1,52 +1,52 @@ -using NUnit.Framework; -using Scalar.Tests.Should; -using System.Diagnostics; - -namespace Scalar.FunctionalTests.Tests -{ - [TestFixture] - public class ScalarVerbTests - { - public ScalarVerbTests() - { - } - - private enum ExpectedReturnCode - { - Success = 0, - ParsingError = 1, - } - - [TestCase] - public void UnknownVerb() - { - this.CallScalar("help", ExpectedReturnCode.Success); - this.CallScalar("unknownverb", ExpectedReturnCode.ParsingError); - } - - [TestCase] - public void UnknownArgs() - { - this.CallScalar("log --help", ExpectedReturnCode.Success); - this.CallScalar("log --unknown-arg", ExpectedReturnCode.ParsingError); - } - - private void CallScalar(string args, ExpectedReturnCode expectedErrorCode) - { - ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); - processInfo.Arguments = args; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - processInfo.RedirectStandardError = true; - - using (Process process = Process.Start(processInfo)) - { - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - process.ExitCode.ShouldEqual((int)expectedErrorCode, result); - } - } - } -} +using NUnit.Framework; +using Scalar.Tests.Should; +using System.Diagnostics; + +namespace Scalar.FunctionalTests.Tests +{ + [TestFixture] + public class ScalarVerbTests + { + public ScalarVerbTests() + { + } + + private enum ExpectedReturnCode + { + Success = 0, + ParsingError = 1, + } + + [TestCase] + public void UnknownVerb() + { + this.CallScalar("help", ExpectedReturnCode.Success); + this.CallScalar("unknownverb", ExpectedReturnCode.ParsingError); + } + + [TestCase] + public void UnknownArgs() + { + this.CallScalar("log --help", ExpectedReturnCode.Success); + this.CallScalar("log --unknown-arg", ExpectedReturnCode.ParsingError); + } + + private void CallScalar(string args, ExpectedReturnCode expectedErrorCode) + { + ProcessStartInfo processInfo = new ProcessStartInfo(ScalarTestConfig.PathToScalar); + processInfo.Arguments = args; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + + using (Process process = Process.Start(processInfo)) + { + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + process.ExitCode.ShouldEqual((int)expectedErrorCode, result); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tests/TestResultsHelper.cs b/Scalar.FunctionalTests/Tests/TestResultsHelper.cs index b26ec617dc..665093fc87 100644 --- a/Scalar.FunctionalTests/Tests/TestResultsHelper.cs +++ b/Scalar.FunctionalTests/Tests/TestResultsHelper.cs @@ -1,73 +1,73 @@ -using Scalar.FunctionalTests.Tools; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.FunctionalTests.Tests -{ - public static class TestResultsHelper - { - public static void OutputScalarLogs(ScalarFunctionalTestEnlistment enlistment) - { - if (enlistment == null) - { - return; - } - - Console.WriteLine("Scalar logs output attached below.\n\n"); - - foreach (string filename in GetAllFilesInDirectory(enlistment.ScalarLogsRoot)) - { +using Scalar.FunctionalTests.Tools; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.FunctionalTests.Tests +{ + public static class TestResultsHelper + { + public static void OutputScalarLogs(ScalarFunctionalTestEnlistment enlistment) + { + if (enlistment == null) + { + return; + } + + Console.WriteLine("Scalar logs output attached below.\n\n"); + + foreach (string filename in GetAllFilesInDirectory(enlistment.ScalarLogsRoot)) + { if (filename.Contains("mount_process")) - { - // Validate that all mount processes started by the functional tests were started - // by verbs, and that "StartedByVerb" was set to true when the mount process was launched - OutputFileContents( + { + // Validate that all mount processes started by the functional tests were started + // by verbs, and that "StartedByVerb" was set to true when the mount process was launched + OutputFileContents( filename, contents => contents.ShouldContain("\"StartedByVerb\":true")); - } - else + } + else { OutputFileContents(filename); - } - } - } - - public static void OutputFileContents(string filename, Action contentsValidator = null) - { - try - { - using (StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - Console.WriteLine("----- {0} -----", filename); - - string contents = reader.ReadToEnd(); - + } + } + } + + public static void OutputFileContents(string filename, Action contentsValidator = null) + { + try + { + using (StreamReader reader = new StreamReader(new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) + { + Console.WriteLine("----- {0} -----", filename); + + string contents = reader.ReadToEnd(); + if (contentsValidator != null) - { + { contentsValidator(contents); - } - - Console.WriteLine(contents + "\n\n"); - } - } - catch (IOException ex) - { - Console.WriteLine("Unable to read logfile at {0}: {1}", filename, ex.ToString()); - } - } - - public static IEnumerable GetAllFilesInDirectory(string folderName) - { - DirectoryInfo directory = new DirectoryInfo(folderName); - if (!directory.Exists) - { - return Enumerable.Empty(); - } - - return directory.GetFiles().Select(file => file.FullName); - } - } -} + } + + Console.WriteLine(contents + "\n\n"); + } + } + catch (IOException ex) + { + Console.WriteLine("Unable to read logfile at {0}: {1}", filename, ex.ToString()); + } + } + + public static IEnumerable GetAllFilesInDirectory(string folderName) + { + DirectoryInfo directory = new DirectoryInfo(folderName); + if (!directory.Exists) + { + return Enumerable.Empty(); + } + + return directory.GetFiles().Select(file => file.FullName); + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ControlGitRepo.cs b/Scalar.FunctionalTests/Tools/ControlGitRepo.cs index be6acd0727..892a6e2c65 100644 --- a/Scalar.FunctionalTests/Tools/ControlGitRepo.cs +++ b/Scalar.FunctionalTests/Tools/ControlGitRepo.cs @@ -1,76 +1,76 @@ -using Scalar.FunctionalTests.FileSystemRunners; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Tools -{ - public class ControlGitRepo - { - static ControlGitRepo() - { - if (!Directory.Exists(CachePath)) - { - GitProcess.Invoke(Environment.SystemDirectory, "clone " + ScalarTestConfig.RepoToClone + " " + CachePath + " --bare"); - } - else - { - GitProcess.Invoke(CachePath, "fetch origin +refs/*:refs/*"); - } - } - - private ControlGitRepo(string repoUrl, string rootPath, string commitish) - { - this.RootPath = rootPath; - this.RepoUrl = repoUrl; - this.Commitish = commitish; - } - - public string RootPath { get; private set; } - public string RepoUrl { get; private set; } - public string Commitish { get; private set; } - - private static string CachePath - { - get { return Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, "cache"); } - } - - public static ControlGitRepo Create(string commitish = null) - { - string clonePath = Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, Guid.NewGuid().ToString("N")); - return new ControlGitRepo( - ScalarTestConfig.RepoToClone, - clonePath, - commitish == null ? Properties.Settings.Default.Commitish : commitish); - } - - // - // IMPORTANT! These must parallel the settings in ScalarVerb:TrySetRequiredGitConfigSettings - // - public void Initialize() - { - Directory.CreateDirectory(this.RootPath); - GitProcess.Invoke(this.RootPath, "init"); - GitProcess.Invoke(this.RootPath, "config core.autocrlf false"); - GitProcess.Invoke(this.RootPath, "config merge.stat false"); - GitProcess.Invoke(this.RootPath, "config merge.renames false"); - GitProcess.Invoke(this.RootPath, "config advice.statusUoption false"); - GitProcess.Invoke(this.RootPath, "config core.abbrev 40"); - GitProcess.Invoke(this.RootPath, "config pack.useSparse true"); - GitProcess.Invoke(this.RootPath, "config reset.quiet true"); - GitProcess.Invoke(this.RootPath, "config status.aheadbehind false"); - GitProcess.Invoke(this.RootPath, "config user.name \"Functional Test User\""); - GitProcess.Invoke(this.RootPath, "config user.email \"functional@test.com\""); - GitProcess.Invoke(this.RootPath, "remote add origin " + CachePath); - this.Fetch(this.Commitish); - GitProcess.Invoke(this.RootPath, "branch --set-upstream " + this.Commitish + " origin/" + this.Commitish); - GitProcess.Invoke(this.RootPath, "checkout " + this.Commitish); - GitProcess.Invoke(this.RootPath, "branch --unset-upstream"); - } - - public void Fetch(string commitish) - { - GitProcess.Invoke(this.RootPath, "fetch origin " + commitish); - } - } -} +using Scalar.FunctionalTests.FileSystemRunners; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Tools +{ + public class ControlGitRepo + { + static ControlGitRepo() + { + if (!Directory.Exists(CachePath)) + { + GitProcess.Invoke(Environment.SystemDirectory, "clone " + ScalarTestConfig.RepoToClone + " " + CachePath + " --bare"); + } + else + { + GitProcess.Invoke(CachePath, "fetch origin +refs/*:refs/*"); + } + } + + private ControlGitRepo(string repoUrl, string rootPath, string commitish) + { + this.RootPath = rootPath; + this.RepoUrl = repoUrl; + this.Commitish = commitish; + } + + public string RootPath { get; private set; } + public string RepoUrl { get; private set; } + public string Commitish { get; private set; } + + private static string CachePath + { + get { return Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, "cache"); } + } + + public static ControlGitRepo Create(string commitish = null) + { + string clonePath = Path.Combine(Properties.Settings.Default.ControlGitRepoRoot, Guid.NewGuid().ToString("N")); + return new ControlGitRepo( + ScalarTestConfig.RepoToClone, + clonePath, + commitish == null ? Properties.Settings.Default.Commitish : commitish); + } + + // + // IMPORTANT! These must parallel the settings in ScalarVerb:TrySetRequiredGitConfigSettings + // + public void Initialize() + { + Directory.CreateDirectory(this.RootPath); + GitProcess.Invoke(this.RootPath, "init"); + GitProcess.Invoke(this.RootPath, "config core.autocrlf false"); + GitProcess.Invoke(this.RootPath, "config merge.stat false"); + GitProcess.Invoke(this.RootPath, "config merge.renames false"); + GitProcess.Invoke(this.RootPath, "config advice.statusUoption false"); + GitProcess.Invoke(this.RootPath, "config core.abbrev 40"); + GitProcess.Invoke(this.RootPath, "config pack.useSparse true"); + GitProcess.Invoke(this.RootPath, "config reset.quiet true"); + GitProcess.Invoke(this.RootPath, "config status.aheadbehind false"); + GitProcess.Invoke(this.RootPath, "config user.name \"Functional Test User\""); + GitProcess.Invoke(this.RootPath, "config user.email \"functional@test.com\""); + GitProcess.Invoke(this.RootPath, "remote add origin " + CachePath); + this.Fetch(this.Commitish); + GitProcess.Invoke(this.RootPath, "branch --set-upstream " + this.Commitish + " origin/" + this.Commitish); + GitProcess.Invoke(this.RootPath, "checkout " + this.Commitish); + GitProcess.Invoke(this.RootPath, "branch --unset-upstream"); + } + + public void Fetch(string commitish) + { + GitProcess.Invoke(this.RootPath, "fetch origin " + commitish); + } + } +} diff --git a/Scalar.FunctionalTests/Tools/GitHelpers.cs b/Scalar.FunctionalTests/Tools/GitHelpers.cs index aa9c3cf75f..a9a89dd22e 100644 --- a/Scalar.FunctionalTests/Tools/GitHelpers.cs +++ b/Scalar.FunctionalTests/Tools/GitHelpers.cs @@ -1,269 +1,269 @@ -using NUnit.Framework; -using Scalar.FunctionalTests.Properties; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.FunctionalTests.Tools -{ - public static class GitHelpers - { - /// - /// This string must match the command name provided in the - /// Scalar.FunctionalTests.LockHolder program. - /// - private const string LockHolderCommandName = @"Scalar.FunctionalTests.LockHolder"; - private const string LockHolderCommand = @"Scalar.FunctionalTests.LockHolder.dll"; - - private const string WindowsPathSeparator = "\\"; - private const string GitPathSeparator = "/"; - - private static string LockHolderCommandPath - { - get - { - // On OSX functional tests are run from inside Publish directory. Dependent - // assemblies including LockHolder test are available at the same level in - // the same directory. - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - return Path.Combine( - Settings.Default.CurrentDirectory, - LockHolderCommand); - } - else - { - // On Windows, FT is run from the Output directory of Scalar.FunctionalTest project. - // LockHolder is a .netcore assembly and can be found inside netcoreapp2.1 - // subdirectory of Scalar.FunctionalTest Output directory. - return Path.Combine( - Settings.Default.CurrentDirectory, - "netcoreapp2.1", - LockHolderCommand); - } - } - } - - public static string ConvertPathToGitFormat(string relativePath) - { - return relativePath.Replace(WindowsPathSeparator, GitPathSeparator); - } - - public static void CheckGitCommand(string virtualRepoRoot, string command, params string[] expectedLinesInResult) - { - ProcessResult result = GitProcess.InvokeProcess(virtualRepoRoot, command); - result.Errors.ShouldBeEmpty(); - foreach (string line in expectedLinesInResult) - { - result.Output.ShouldContain(line); - } - } - - public static void CheckGitCommandAgainstScalarRepo(string virtualRepoRoot, string command, params string[] expectedLinesInResult) - { - ProcessResult result = InvokeGitAgainstScalarRepo(virtualRepoRoot, command); - result.Errors.ShouldBeEmpty(); - foreach (string line in expectedLinesInResult) - { - result.Output.ShouldContain(line); - } - } - - public static ProcessResult InvokeGitAgainstScalarRepo( - string scalarRepoRoot, - string command, - Dictionary environmentVariables = null, - bool removeWaitingMessages = true, - bool removeUpgradeMessages = true) - { - ProcessResult result = GitProcess.InvokeProcess(scalarRepoRoot, command, environmentVariables); - string errors = result.Errors; - - if (!string.IsNullOrEmpty(errors) && (removeWaitingMessages || removeUpgradeMessages)) - { - IEnumerable errorLines = errors.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); - IEnumerable filteredErrorLines = errorLines.Where(line => - { - if (string.IsNullOrWhiteSpace(line) || - (removeUpgradeMessages && line.StartsWith("A new version of Scalar is available.")) || - (removeWaitingMessages && line.StartsWith("Waiting for "))) - { - return false; - } - else - { - return true; - } - }); - - errors = filteredErrorLines.Any() ? string.Join(Environment.NewLine, filteredErrorLines) : string.Empty; - } - - return new ProcessResult( - result.Output, - errors, - result.ExitCode); - } - - public static void ValidateGitCommand( - ScalarFunctionalTestEnlistment enlistment, - ControlGitRepo controlGitRepo, - string command, - params object[] args) - { - command = string.Format(command, args); - string controlRepoRoot = controlGitRepo.RootPath; - string scalarRepoRoot = enlistment.RepoRoot; - - ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); - ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); - - ErrorsShouldMatch(command, expectedResult, actualResult); - actualResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .ShouldMatchInOrder(expectedResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Output Lines"); - - if (command != "status") - { - ValidateGitCommand(enlistment, controlGitRepo, "status"); - } - } - - /// - /// Acquire the ScalarLock. This method will return once the ScalarLock has been acquired. - /// - /// The ID of the process that acquired the lock. - /// that can be signaled to exit the lock acquisition program. - public static ManualResetEventSlim AcquireScalarLock( - ScalarFunctionalTestEnlistment enlistment, - out int processId, - int resetTimeout = Timeout.Infinite, - bool skipReleaseLock = false) - { - string args = LockHolderCommandPath; - if (skipReleaseLock) - { - args += " --skip-release-lock"; - } - - return RunCommandWithWaitAndStdIn( - enlistment, - resetTimeout, - "dotnet", - args, - GitHelpers.LockHolderCommandName, - "done", - out processId); - } - - /// - /// Run the specified Git command. This method will return once the ScalarLock has been acquired. - /// - /// The ID of the process that acquired the lock. - /// that can be signaled to exit the lock acquisition program. - public static ManualResetEventSlim RunGitCommandWithWaitAndStdIn( - ScalarFunctionalTestEnlistment enlistment, - int resetTimeout, - string command, - string stdinToQuit, - out int processId) - { - return - RunCommandWithWaitAndStdIn( - enlistment, - resetTimeout, - Properties.Settings.Default.PathToGit, - command, - "git " + command, - stdinToQuit, - out processId); - } - - public static void ErrorsShouldMatch(string command, ProcessResult expectedResult, ProcessResult actualResult) - { - actualResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .ShouldMatchInOrder(expectedResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Errors Lines"); - } - - /// - /// Run the specified command as an external program. This method will return once the ScalarLock has been acquired. - /// - /// The ID of the process that acquired the lock. - /// that can be signaled to exit the lock acquisition program. - private static ManualResetEventSlim RunCommandWithWaitAndStdIn( - ScalarFunctionalTestEnlistment enlistment, - int resetTimeout, - string pathToCommand, - string args, - string lockingProcessCommandName, - string stdinToQuit, - out int processId) - { - ManualResetEventSlim resetEvent = new ManualResetEventSlim(initialState: false); - - ProcessStartInfo processInfo = new ProcessStartInfo(pathToCommand); - processInfo.WorkingDirectory = enlistment.RepoRoot; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - processInfo.RedirectStandardError = true; - processInfo.RedirectStandardInput = true; - processInfo.Arguments = args; - - Process holdingProcess = Process.Start(processInfo); - StreamWriter stdin = holdingProcess.StandardInput; - processId = holdingProcess.Id; - - enlistment.WaitForLock(lockingProcessCommandName); - - Task.Run( - () => - { - resetEvent.Wait(resetTimeout); - - try - { - // Make sure to let the holding process end. - if (stdin != null) - { - stdin.WriteLine(stdinToQuit); - stdin.Close(); - } - - if (holdingProcess != null) - { - bool holdingProcessHasExited = holdingProcess.WaitForExit(10000); - - if (!holdingProcess.HasExited) - { - holdingProcess.Kill(); - } - - holdingProcess.Dispose(); - - holdingProcessHasExited.ShouldBeTrue("Locking process did not exit in time."); - } - } - catch (Exception ex) - { - Assert.Fail($"{nameof(RunCommandWithWaitAndStdIn)} exception closing stdin {ex.ToString()}"); - } - finally - { - resetEvent.Set(); - } - }); - - return resetEvent; - } - - private static bool LinesAreEqual(string actualLine, string expectedLine) - { - return actualLine.Equals(expectedLine); - } - } -} +using NUnit.Framework; +using Scalar.FunctionalTests.Properties; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.FunctionalTests.Tools +{ + public static class GitHelpers + { + /// + /// This string must match the command name provided in the + /// Scalar.FunctionalTests.LockHolder program. + /// + private const string LockHolderCommandName = @"Scalar.FunctionalTests.LockHolder"; + private const string LockHolderCommand = @"Scalar.FunctionalTests.LockHolder.dll"; + + private const string WindowsPathSeparator = "\\"; + private const string GitPathSeparator = "/"; + + private static string LockHolderCommandPath + { + get + { + // On OSX functional tests are run from inside Publish directory. Dependent + // assemblies including LockHolder test are available at the same level in + // the same directory. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return Path.Combine( + Settings.Default.CurrentDirectory, + LockHolderCommand); + } + else + { + // On Windows, FT is run from the Output directory of Scalar.FunctionalTest project. + // LockHolder is a .netcore assembly and can be found inside netcoreapp2.1 + // subdirectory of Scalar.FunctionalTest Output directory. + return Path.Combine( + Settings.Default.CurrentDirectory, + "netcoreapp2.1", + LockHolderCommand); + } + } + } + + public static string ConvertPathToGitFormat(string relativePath) + { + return relativePath.Replace(WindowsPathSeparator, GitPathSeparator); + } + + public static void CheckGitCommand(string virtualRepoRoot, string command, params string[] expectedLinesInResult) + { + ProcessResult result = GitProcess.InvokeProcess(virtualRepoRoot, command); + result.Errors.ShouldBeEmpty(); + foreach (string line in expectedLinesInResult) + { + result.Output.ShouldContain(line); + } + } + + public static void CheckGitCommandAgainstScalarRepo(string virtualRepoRoot, string command, params string[] expectedLinesInResult) + { + ProcessResult result = InvokeGitAgainstScalarRepo(virtualRepoRoot, command); + result.Errors.ShouldBeEmpty(); + foreach (string line in expectedLinesInResult) + { + result.Output.ShouldContain(line); + } + } + + public static ProcessResult InvokeGitAgainstScalarRepo( + string scalarRepoRoot, + string command, + Dictionary environmentVariables = null, + bool removeWaitingMessages = true, + bool removeUpgradeMessages = true) + { + ProcessResult result = GitProcess.InvokeProcess(scalarRepoRoot, command, environmentVariables); + string errors = result.Errors; + + if (!string.IsNullOrEmpty(errors) && (removeWaitingMessages || removeUpgradeMessages)) + { + IEnumerable errorLines = errors.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + IEnumerable filteredErrorLines = errorLines.Where(line => + { + if (string.IsNullOrWhiteSpace(line) || + (removeUpgradeMessages && line.StartsWith("A new version of Scalar is available.")) || + (removeWaitingMessages && line.StartsWith("Waiting for "))) + { + return false; + } + else + { + return true; + } + }); + + errors = filteredErrorLines.Any() ? string.Join(Environment.NewLine, filteredErrorLines) : string.Empty; + } + + return new ProcessResult( + result.Output, + errors, + result.ExitCode); + } + + public static void ValidateGitCommand( + ScalarFunctionalTestEnlistment enlistment, + ControlGitRepo controlGitRepo, + string command, + params object[] args) + { + command = string.Format(command, args); + string controlRepoRoot = controlGitRepo.RootPath; + string scalarRepoRoot = enlistment.RepoRoot; + + ProcessResult expectedResult = GitProcess.InvokeProcess(controlRepoRoot, command); + ProcessResult actualResult = GitHelpers.InvokeGitAgainstScalarRepo(scalarRepoRoot, command); + + ErrorsShouldMatch(command, expectedResult, actualResult); + actualResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .ShouldMatchInOrder(expectedResult.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Output Lines"); + + if (command != "status") + { + ValidateGitCommand(enlistment, controlGitRepo, "status"); + } + } + + /// + /// Acquire the ScalarLock. This method will return once the ScalarLock has been acquired. + /// + /// The ID of the process that acquired the lock. + /// that can be signaled to exit the lock acquisition program. + public static ManualResetEventSlim AcquireScalarLock( + ScalarFunctionalTestEnlistment enlistment, + out int processId, + int resetTimeout = Timeout.Infinite, + bool skipReleaseLock = false) + { + string args = LockHolderCommandPath; + if (skipReleaseLock) + { + args += " --skip-release-lock"; + } + + return RunCommandWithWaitAndStdIn( + enlistment, + resetTimeout, + "dotnet", + args, + GitHelpers.LockHolderCommandName, + "done", + out processId); + } + + /// + /// Run the specified Git command. This method will return once the ScalarLock has been acquired. + /// + /// The ID of the process that acquired the lock. + /// that can be signaled to exit the lock acquisition program. + public static ManualResetEventSlim RunGitCommandWithWaitAndStdIn( + ScalarFunctionalTestEnlistment enlistment, + int resetTimeout, + string command, + string stdinToQuit, + out int processId) + { + return + RunCommandWithWaitAndStdIn( + enlistment, + resetTimeout, + Properties.Settings.Default.PathToGit, + command, + "git " + command, + stdinToQuit, + out processId); + } + + public static void ErrorsShouldMatch(string command, ProcessResult expectedResult, ProcessResult actualResult) + { + actualResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .ShouldMatchInOrder(expectedResult.Errors.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries), LinesAreEqual, command + " Errors Lines"); + } + + /// + /// Run the specified command as an external program. This method will return once the ScalarLock has been acquired. + /// + /// The ID of the process that acquired the lock. + /// that can be signaled to exit the lock acquisition program. + private static ManualResetEventSlim RunCommandWithWaitAndStdIn( + ScalarFunctionalTestEnlistment enlistment, + int resetTimeout, + string pathToCommand, + string args, + string lockingProcessCommandName, + string stdinToQuit, + out int processId) + { + ManualResetEventSlim resetEvent = new ManualResetEventSlim(initialState: false); + + ProcessStartInfo processInfo = new ProcessStartInfo(pathToCommand); + processInfo.WorkingDirectory = enlistment.RepoRoot; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + processInfo.RedirectStandardInput = true; + processInfo.Arguments = args; + + Process holdingProcess = Process.Start(processInfo); + StreamWriter stdin = holdingProcess.StandardInput; + processId = holdingProcess.Id; + + enlistment.WaitForLock(lockingProcessCommandName); + + Task.Run( + () => + { + resetEvent.Wait(resetTimeout); + + try + { + // Make sure to let the holding process end. + if (stdin != null) + { + stdin.WriteLine(stdinToQuit); + stdin.Close(); + } + + if (holdingProcess != null) + { + bool holdingProcessHasExited = holdingProcess.WaitForExit(10000); + + if (!holdingProcess.HasExited) + { + holdingProcess.Kill(); + } + + holdingProcess.Dispose(); + + holdingProcessHasExited.ShouldBeTrue("Locking process did not exit in time."); + } + } + catch (Exception ex) + { + Assert.Fail($"{nameof(RunCommandWithWaitAndStdIn)} exception closing stdin {ex.ToString()}"); + } + finally + { + resetEvent.Set(); + } + }); + + return resetEvent; + } + + private static bool LinesAreEqual(string actualLine, string expectedLine) + { + return actualLine.Equals(expectedLine); + } + } +} diff --git a/Scalar.FunctionalTests/Tools/GitProcess.cs b/Scalar.FunctionalTests/Tools/GitProcess.cs index b40d4ca5d1..3b65ab95f8 100644 --- a/Scalar.FunctionalTests/Tools/GitProcess.cs +++ b/Scalar.FunctionalTests/Tools/GitProcess.cs @@ -1,41 +1,41 @@ -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -namespace Scalar.FunctionalTests.Tools -{ - public static class GitProcess - { - public static string Invoke(string executionWorkingDirectory, string command) - { - return InvokeProcess(executionWorkingDirectory, command).Output; - } - - public static ProcessResult InvokeProcess(string executionWorkingDirectory, string command, Dictionary environmentVariables = null, Stream inputStream = null) - { - ProcessStartInfo processInfo = new ProcessStartInfo(Properties.Settings.Default.PathToGit); - processInfo.WorkingDirectory = executionWorkingDirectory; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - processInfo.RedirectStandardError = true; - processInfo.Arguments = command; - - if (inputStream != null) - { - processInfo.RedirectStandardInput = true; - } - - processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; - - if (environmentVariables != null) - { - foreach (string key in environmentVariables.Keys) - { - processInfo.EnvironmentVariables[key] = environmentVariables[key]; - } - } - - return ProcessHelper.Run(processInfo, inputStream: inputStream); - } - } -} +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Scalar.FunctionalTests.Tools +{ + public static class GitProcess + { + public static string Invoke(string executionWorkingDirectory, string command) + { + return InvokeProcess(executionWorkingDirectory, command).Output; + } + + public static ProcessResult InvokeProcess(string executionWorkingDirectory, string command, Dictionary environmentVariables = null, Stream inputStream = null) + { + ProcessStartInfo processInfo = new ProcessStartInfo(Properties.Settings.Default.PathToGit); + processInfo.WorkingDirectory = executionWorkingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + processInfo.RedirectStandardError = true; + processInfo.Arguments = command; + + if (inputStream != null) + { + processInfo.RedirectStandardInput = true; + } + + processInfo.EnvironmentVariables["GIT_TERMINAL_PROMPT"] = "0"; + + if (environmentVariables != null) + { + foreach (string key in environmentVariables.Keys) + { + processInfo.EnvironmentVariables[key] = environmentVariables[key]; + } + } + + return ProcessHelper.Run(processInfo, inputStream: inputStream); + } + } +} diff --git a/Scalar.FunctionalTests/Tools/NativeMethods.cs b/Scalar.FunctionalTests/Tools/NativeMethods.cs index c713942d1c..cb3170b120 100644 --- a/Scalar.FunctionalTests/Tools/NativeMethods.cs +++ b/Scalar.FunctionalTests/Tools/NativeMethods.cs @@ -1,23 +1,23 @@ -using Microsoft.Win32.SafeHandles; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Tools -{ - public class NativeMethods - { - [DllImport("kernel32.dll", SetLastError = true)] - public static extern bool MoveFile(string lpExistingFileName, string lpNewFileName); - - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - public static extern SafeFileHandle CreateFile( - [In] string lpFileName, - uint dwDesiredAccess, - FileShare dwShareMode, - [In] IntPtr lpSecurityAttributes, - [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, - uint dwFlagsAndAttributes, - [In] IntPtr hTemplateFile); - } -} +using Microsoft.Win32.SafeHandles; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Tools +{ + public class NativeMethods + { + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool MoveFile(string lpExistingFileName, string lpNewFileName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern SafeFileHandle CreateFile( + [In] string lpFileName, + uint dwDesiredAccess, + FileShare dwShareMode, + [In] IntPtr lpSecurityAttributes, + [MarshalAs(UnmanagedType.U4)]FileMode dwCreationDisposition, + uint dwFlagsAndAttributes, + [In] IntPtr hTemplateFile); + } +} diff --git a/Scalar.FunctionalTests/Tools/ProcessHelper.cs b/Scalar.FunctionalTests/Tools/ProcessHelper.cs index 730fe38ca1..764041c792 100644 --- a/Scalar.FunctionalTests/Tools/ProcessHelper.cs +++ b/Scalar.FunctionalTests/Tools/ProcessHelper.cs @@ -1,81 +1,81 @@ -using System.Diagnostics; -using System.IO; - -namespace Scalar.FunctionalTests.Tools -{ - public static class ProcessHelper - { - public static ProcessResult Run(string fileName, string arguments) - { - ProcessStartInfo startInfo = new ProcessStartInfo(); - startInfo.UseShellExecute = false; - startInfo.RedirectStandardOutput = true; - startInfo.RedirectStandardError = true; - startInfo.CreateNoWindow = true; - startInfo.FileName = fileName; - startInfo.Arguments = arguments; - - return Run(startInfo); - } - - public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null, Stream inputStream = null) - { - using (Process executingProcess = new Process()) - { - string output = string.Empty; - string errors = string.Empty; - - // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx - // To avoid deadlocks, use asynchronous read operations on at least one of the streams. - // Do not perform a synchronous read to the end of both redirected streams. - executingProcess.StartInfo = processInfo; - executingProcess.ErrorDataReceived += (sender, args) => - { - if (args.Data != null) - { - errors = errors + args.Data + errorMsgDelimeter; - } - }; - - if (executionLock != null) - { - lock (executionLock) - { - output = StartProcess(executingProcess, inputStream); - } - } - else - { - output = StartProcess(executingProcess, inputStream); - } - - return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); - } - } - - private static string StartProcess(Process executingProcess, Stream inputStream = null) - { - executingProcess.Start(); - - if (inputStream != null) - { - inputStream.CopyTo(executingProcess.StandardInput.BaseStream); - } - - if (executingProcess.StartInfo.RedirectStandardError) - { - executingProcess.BeginErrorReadLine(); - } - - string output = string.Empty; - if (executingProcess.StartInfo.RedirectStandardOutput) - { - output = executingProcess.StandardOutput.ReadToEnd(); - } - - executingProcess.WaitForExit(); - - return output; - } - } -} +using System.Diagnostics; +using System.IO; + +namespace Scalar.FunctionalTests.Tools +{ + public static class ProcessHelper + { + public static ProcessResult Run(string fileName, string arguments) + { + ProcessStartInfo startInfo = new ProcessStartInfo(); + startInfo.UseShellExecute = false; + startInfo.RedirectStandardOutput = true; + startInfo.RedirectStandardError = true; + startInfo.CreateNoWindow = true; + startInfo.FileName = fileName; + startInfo.Arguments = arguments; + + return Run(startInfo); + } + + public static ProcessResult Run(ProcessStartInfo processInfo, string errorMsgDelimeter = "\r\n", object executionLock = null, Stream inputStream = null) + { + using (Process executingProcess = new Process()) + { + string output = string.Empty; + string errors = string.Empty; + + // From https://msdn.microsoft.com/en-us/library/system.diagnostics.process.standardoutput.aspx + // To avoid deadlocks, use asynchronous read operations on at least one of the streams. + // Do not perform a synchronous read to the end of both redirected streams. + executingProcess.StartInfo = processInfo; + executingProcess.ErrorDataReceived += (sender, args) => + { + if (args.Data != null) + { + errors = errors + args.Data + errorMsgDelimeter; + } + }; + + if (executionLock != null) + { + lock (executionLock) + { + output = StartProcess(executingProcess, inputStream); + } + } + else + { + output = StartProcess(executingProcess, inputStream); + } + + return new ProcessResult(output.ToString(), errors.ToString(), executingProcess.ExitCode); + } + } + + private static string StartProcess(Process executingProcess, Stream inputStream = null) + { + executingProcess.Start(); + + if (inputStream != null) + { + inputStream.CopyTo(executingProcess.StandardInput.BaseStream); + } + + if (executingProcess.StartInfo.RedirectStandardError) + { + executingProcess.BeginErrorReadLine(); + } + + string output = string.Empty; + if (executingProcess.StartInfo.RedirectStandardOutput) + { + output = executingProcess.StandardOutput.ReadToEnd(); + } + + executingProcess.WaitForExit(); + + return output; + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ProcessResult.cs b/Scalar.FunctionalTests/Tools/ProcessResult.cs index 477cc23237..b42c2c4b2b 100644 --- a/Scalar.FunctionalTests/Tools/ProcessResult.cs +++ b/Scalar.FunctionalTests/Tools/ProcessResult.cs @@ -1,16 +1,16 @@ -namespace Scalar.FunctionalTests.Tools -{ - public class ProcessResult - { - public ProcessResult(string output, string errors, int exitCode) - { - this.Output = output; - this.Errors = errors; - this.ExitCode = exitCode; - } - - public string Output { get; } - public string Errors { get; } - public int ExitCode { get; } - } -} +namespace Scalar.FunctionalTests.Tools +{ + public class ProcessResult + { + public ProcessResult(string output, string errors, int exitCode) + { + this.Output = output; + this.Errors = errors; + this.ExitCode = exitCode; + } + + public string Output { get; } + public string Errors { get; } + public int ExitCode { get; } + } +} diff --git a/Scalar.FunctionalTests/Tools/RepositoryHelpers.cs b/Scalar.FunctionalTests/Tools/RepositoryHelpers.cs index 3aab51a23f..f122aa73dc 100644 --- a/Scalar.FunctionalTests/Tools/RepositoryHelpers.cs +++ b/Scalar.FunctionalTests/Tools/RepositoryHelpers.cs @@ -1,21 +1,21 @@ -using Scalar.FunctionalTests.FileSystemRunners; -using System.Runtime.InteropServices; - -namespace Scalar.FunctionalTests.Tools -{ - public static class RepositoryHelpers - { - public static void DeleteTestDirectory(string repoPath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - // Use cmd.exe to delete the enlistment as it properly handles tombstones and reparse points - CmdRunner.DeleteDirectoryWithUnlimitedRetries(repoPath); - } - else - { - BashRunner.DeleteDirectoryWithUnlimitedRetries(repoPath); - } - } - } -} +using Scalar.FunctionalTests.FileSystemRunners; +using System.Runtime.InteropServices; + +namespace Scalar.FunctionalTests.Tools +{ + public static class RepositoryHelpers + { + public static void DeleteTestDirectory(string repoPath) + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // Use cmd.exe to delete the enlistment as it properly handles tombstones and reparse points + CmdRunner.DeleteDirectoryWithUnlimitedRetries(repoPath); + } + else + { + BashRunner.DeleteDirectoryWithUnlimitedRetries(repoPath); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ScalarFunctionalTestEnlistment.cs b/Scalar.FunctionalTests/Tools/ScalarFunctionalTestEnlistment.cs index 64e68dd229..6344374b58 100644 --- a/Scalar.FunctionalTests/Tools/ScalarFunctionalTestEnlistment.cs +++ b/Scalar.FunctionalTests/Tools/ScalarFunctionalTestEnlistment.cs @@ -1,326 +1,326 @@ -using Newtonsoft.Json.Linq; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.FunctionalTests.Tests; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; - -namespace Scalar.FunctionalTests.Tools -{ - public class ScalarFunctionalTestEnlistment - { - private const string LockHeldByGit = "Scalar Lock: Held by {0}"; - private const int SleepMSWaitingForStatusCheck = 100; - private const int DefaultMaxWaitMSForStatusCheck = 5000; - private static readonly string ZeroBackgroundOperations = "Background operations: 0" + Environment.NewLine; - - private ScalarProcess scalarProcess; - - private ScalarFunctionalTestEnlistment(string pathToScalar, string enlistmentRoot, string repoUrl, string commitish, string localCacheRoot = null) - { - this.EnlistmentRoot = enlistmentRoot; - this.RepoUrl = repoUrl; - this.Commitish = commitish; - - if (localCacheRoot == null) - { - if (ScalarTestConfig.NoSharedCache) - { - // eg C:\Repos\ScalarFunctionalTests\enlistment\7942ca69d7454acbb45ea39ef5be1d15\.scalar\.scalarCache - localCacheRoot = GetRepoSpecificLocalCacheRoot(enlistmentRoot); - } - else - { - // eg C:\Repos\ScalarFunctionalTests\.scalarCache - // Ensures the general cache is not cleaned up between test runs - localCacheRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", ".scalarCache"); - } - } - - this.LocalCacheRoot = localCacheRoot; - this.scalarProcess = new ScalarProcess(pathToScalar, this.EnlistmentRoot, this.LocalCacheRoot); - } - - public string EnlistmentRoot - { - get; private set; - } - - public string RepoUrl - { - get; private set; - } - - public string LocalCacheRoot { get; } - - public string RepoRoot - { - get { return Path.Combine(this.EnlistmentRoot, "src"); } - } - - public string DotScalarRoot - { - get { return Path.Combine(this.EnlistmentRoot, ScalarTestConfig.DotScalarRoot); } - } - - public string ScalarLogsRoot - { - get { return Path.Combine(this.DotScalarRoot, "logs"); } - } - - public string DiagnosticsRoot - { - get { return Path.Combine(this.DotScalarRoot, "diagnostics"); } - } - - public string Commitish - { - get; private set; - } - - public static ScalarFunctionalTestEnlistment CloneAndMountWithPerRepoCache(string pathToGvfs, bool skipPrefetch) - { - string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); - string localCache = ScalarFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot); - return CloneAndMount(pathToGvfs, enlistmentRoot, null, localCache, skipPrefetch); - } - - public static ScalarFunctionalTestEnlistment CloneAndMount( - string pathToGvfs, - string commitish = null, - string localCacheRoot = null, - bool skipPrefetch = false) - { - string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); - return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCacheRoot, skipPrefetch); - } - - public static ScalarFunctionalTestEnlistment CloneAndMountEnlistmentWithSpacesInPath(string pathToGvfs, string commitish = null) - { - string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRootWithSpaces(); - string localCache = ScalarFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot); - return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCache); - } - - public static string GetUniqueEnlistmentRoot() - { - return Path.Combine(Properties.Settings.Default.EnlistmentRoot, Guid.NewGuid().ToString("N").Substring(0, 20)); - } - - public static string GetUniqueEnlistmentRootWithSpaces() - { - return Path.Combine(Properties.Settings.Default.EnlistmentRoot, "test " + Guid.NewGuid().ToString("N").Substring(0, 15)); - } - - public string GetObjectRoot(FileSystemRunner fileSystem) - { - string mappingFile = Path.Combine(this.LocalCacheRoot, "mapping.dat"); - mappingFile.ShouldBeAFile(fileSystem); - - HashSet allowedFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "mapping.dat", - "mapping.dat.lock" // mapping.dat.lock can be present, but doesn't have to be present - }; - - this.LocalCacheRoot.ShouldBeADirectory(fileSystem).WithFiles().ShouldNotContain(f => !allowedFileNames.Contains(f.Name)); - - string mappingFileContents = File.ReadAllText(mappingFile); - mappingFileContents.ShouldNotBeNull(); - string[] objectRootEntries = mappingFileContents.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) - .Where(x => x.IndexOf(this.RepoUrl, StringComparison.OrdinalIgnoreCase) >= 0) - .ToArray(); - objectRootEntries.Length.ShouldEqual(1, $"Should be only one entry for repo url: {this.RepoUrl} mapping file content: {mappingFileContents}"); - objectRootEntries[0].Substring(0, 2).ShouldEqual("A ", $"Invalid mapping entry for repo: {objectRootEntries[0]}"); - JObject rootEntryJson = JObject.Parse(objectRootEntries[0].Substring(2)); - string objectRootFolder = rootEntryJson.GetValue("Value").ToString(); - objectRootFolder.ShouldNotBeNull(); - objectRootFolder.Length.ShouldBeAtLeast(1, $"Invalid object root folder: {objectRootFolder} for {this.RepoUrl} mapping file content: {mappingFileContents}"); - - return Path.Combine(this.LocalCacheRoot, objectRootFolder, "gitObjects"); - } - - public string GetPackRoot(FileSystemRunner fileSystem) - { - return Path.Combine(this.GetObjectRoot(fileSystem), "pack"); - } - - public void DeleteEnlistment() - { - TestResultsHelper.OutputScalarLogs(this); - RepositoryHelpers.DeleteTestDirectory(this.EnlistmentRoot); - } - - public void CloneAndMount(bool skipPrefetch) - { - this.scalarProcess.Clone(this.RepoUrl, this.Commitish, skipPrefetch); - - GitProcess.Invoke(this.RepoRoot, "checkout " + this.Commitish); - GitProcess.Invoke(this.RepoRoot, "branch --unset-upstream"); - GitProcess.Invoke(this.RepoRoot, "config core.abbrev 40"); - GitProcess.Invoke(this.RepoRoot, "config user.name \"Functional Test User\""); - GitProcess.Invoke(this.RepoRoot, "config user.email \"functional@test.com\""); - - // If this repository has a .gitignore file in the root directory, force it to be - // hydrated. This is because if the GitStatusCache feature is enabled, it will run - // a "git status" command asynchronously, which will hydrate the .gitignore file - // as it reads the ignore rules. Hydrate this file here so that it is consistently - // hydrated and there are no race conditions depending on when / if it is hydrated - // as part of an asynchronous status scan to rebuild the GitStatusCache. - string rootGitIgnorePath = Path.Combine(this.RepoRoot, ".gitignore"); - if (File.Exists(rootGitIgnorePath)) - { - File.ReadAllBytes(rootGitIgnorePath); - } - } - - public void MountScalar() - { - this.scalarProcess.Mount(); - } - - public bool TryMountScalar() - { - string output; - return this.TryMountScalar(out output); - } - - public bool TryMountScalar(out string output) - { - return this.scalarProcess.TryMount(out output); - } - - public string Prefetch(string args, bool failOnError = true, string standardInput = null) - { - return this.scalarProcess.Prefetch(args, failOnError, standardInput); - } - - public void Repair(bool confirm) - { - this.scalarProcess.Repair(confirm); - } - - public string Diagnose() - { - return this.scalarProcess.Diagnose(); - } - - public string LooseObjectStep() - { - return this.scalarProcess.LooseObjectStep(); - } - - public string PackfileMaintenanceStep(long? batchSize = null) - { - return this.scalarProcess.PackfileMaintenanceStep(batchSize); - } - - public string PostFetchStep() - { - return this.scalarProcess.PostFetchStep(); - } - - public string Status(string trace = null) - { - return this.scalarProcess.Status(trace); - } - - public bool WaitForBackgroundOperations(int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) - { - return this.WaitForStatus(maxWaitMilliseconds, ZeroBackgroundOperations).ShouldBeTrue("Background operations failed to complete."); - } - - public bool WaitForLock(string lockCommand, int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) - { - return this.WaitForStatus(maxWaitMilliseconds, string.Format(LockHeldByGit, lockCommand)); - } - - public void UnmountScalar() - { - this.scalarProcess.Unmount(); - } - - public string GetCacheServer() - { - return this.scalarProcess.CacheServer("--get"); - } - - public string SetCacheServer(string arg) - { - return this.scalarProcess.CacheServer("--set " + arg); - } - - public void UnmountAndDeleteAll() - { - this.UnmountScalar(); - this.DeleteEnlistment(); - } - - public string GetVirtualPathTo(string path) - { - // Replace '/' with Path.DirectorySeparatorChar to ensure that any - // Git paths are converted to system paths - return Path.Combine(this.RepoRoot, path.Replace('/', Path.DirectorySeparatorChar)); - } - - public string GetVirtualPathTo(params string[] pathParts) - { - return Path.Combine(this.RepoRoot, Path.Combine(pathParts)); - } - - public string GetObjectPathTo(string objectHash) - { - return Path.Combine( - this.RepoRoot, - TestConstants.DotGit.Objects.Root, - objectHash.Substring(0, 2), - objectHash.Substring(2)); - } - - private static ScalarFunctionalTestEnlistment CloneAndMount(string pathToGvfs, string enlistmentRoot, string commitish, string localCacheRoot, bool skipPrefetch = false) - { - ScalarFunctionalTestEnlistment enlistment = new ScalarFunctionalTestEnlistment( - pathToGvfs, - enlistmentRoot ?? GetUniqueEnlistmentRoot(), - ScalarTestConfig.RepoToClone, - commitish ?? Properties.Settings.Default.Commitish, - localCacheRoot ?? ScalarTestConfig.LocalCacheRoot); - - try - { - enlistment.CloneAndMount(skipPrefetch); - } - catch (Exception e) - { - Console.WriteLine("Unhandled exception in CloneAndMount: " + e.ToString()); - TestResultsHelper.OutputScalarLogs(enlistment); - throw; - } - - return enlistment; - } - - private static string GetRepoSpecificLocalCacheRoot(string enlistmentRoot) - { - return Path.Combine(enlistmentRoot, ScalarTestConfig.DotScalarRoot, ".scalarCache"); - } - - private bool WaitForStatus(int maxWaitMilliseconds, string statusShouldContain) - { - string status = null; - int totalWaitMilliseconds = 0; - while (totalWaitMilliseconds <= maxWaitMilliseconds && (status == null || !status.Contains(statusShouldContain))) - { - Thread.Sleep(SleepMSWaitingForStatusCheck); - status = this.Status(); - totalWaitMilliseconds += SleepMSWaitingForStatusCheck; - } - - return totalWaitMilliseconds <= maxWaitMilliseconds; - } - } -} +using Newtonsoft.Json.Linq; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.FunctionalTests.Tests; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; + +namespace Scalar.FunctionalTests.Tools +{ + public class ScalarFunctionalTestEnlistment + { + private const string LockHeldByGit = "Scalar Lock: Held by {0}"; + private const int SleepMSWaitingForStatusCheck = 100; + private const int DefaultMaxWaitMSForStatusCheck = 5000; + private static readonly string ZeroBackgroundOperations = "Background operations: 0" + Environment.NewLine; + + private ScalarProcess scalarProcess; + + private ScalarFunctionalTestEnlistment(string pathToScalar, string enlistmentRoot, string repoUrl, string commitish, string localCacheRoot = null) + { + this.EnlistmentRoot = enlistmentRoot; + this.RepoUrl = repoUrl; + this.Commitish = commitish; + + if (localCacheRoot == null) + { + if (ScalarTestConfig.NoSharedCache) + { + // eg C:\Repos\ScalarFunctionalTests\enlistment\7942ca69d7454acbb45ea39ef5be1d15\.scalar\.scalarCache + localCacheRoot = GetRepoSpecificLocalCacheRoot(enlistmentRoot); + } + else + { + // eg C:\Repos\ScalarFunctionalTests\.scalarCache + // Ensures the general cache is not cleaned up between test runs + localCacheRoot = Path.Combine(Properties.Settings.Default.EnlistmentRoot, "..", ".scalarCache"); + } + } + + this.LocalCacheRoot = localCacheRoot; + this.scalarProcess = new ScalarProcess(pathToScalar, this.EnlistmentRoot, this.LocalCacheRoot); + } + + public string EnlistmentRoot + { + get; private set; + } + + public string RepoUrl + { + get; private set; + } + + public string LocalCacheRoot { get; } + + public string RepoRoot + { + get { return Path.Combine(this.EnlistmentRoot, "src"); } + } + + public string DotScalarRoot + { + get { return Path.Combine(this.EnlistmentRoot, ScalarTestConfig.DotScalarRoot); } + } + + public string ScalarLogsRoot + { + get { return Path.Combine(this.DotScalarRoot, "logs"); } + } + + public string DiagnosticsRoot + { + get { return Path.Combine(this.DotScalarRoot, "diagnostics"); } + } + + public string Commitish + { + get; private set; + } + + public static ScalarFunctionalTestEnlistment CloneAndMountWithPerRepoCache(string pathToGvfs, bool skipPrefetch) + { + string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); + string localCache = ScalarFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot); + return CloneAndMount(pathToGvfs, enlistmentRoot, null, localCache, skipPrefetch); + } + + public static ScalarFunctionalTestEnlistment CloneAndMount( + string pathToGvfs, + string commitish = null, + string localCacheRoot = null, + bool skipPrefetch = false) + { + string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRoot(); + return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCacheRoot, skipPrefetch); + } + + public static ScalarFunctionalTestEnlistment CloneAndMountEnlistmentWithSpacesInPath(string pathToGvfs, string commitish = null) + { + string enlistmentRoot = ScalarFunctionalTestEnlistment.GetUniqueEnlistmentRootWithSpaces(); + string localCache = ScalarFunctionalTestEnlistment.GetRepoSpecificLocalCacheRoot(enlistmentRoot); + return CloneAndMount(pathToGvfs, enlistmentRoot, commitish, localCache); + } + + public static string GetUniqueEnlistmentRoot() + { + return Path.Combine(Properties.Settings.Default.EnlistmentRoot, Guid.NewGuid().ToString("N").Substring(0, 20)); + } + + public static string GetUniqueEnlistmentRootWithSpaces() + { + return Path.Combine(Properties.Settings.Default.EnlistmentRoot, "test " + Guid.NewGuid().ToString("N").Substring(0, 15)); + } + + public string GetObjectRoot(FileSystemRunner fileSystem) + { + string mappingFile = Path.Combine(this.LocalCacheRoot, "mapping.dat"); + mappingFile.ShouldBeAFile(fileSystem); + + HashSet allowedFileNames = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "mapping.dat", + "mapping.dat.lock" // mapping.dat.lock can be present, but doesn't have to be present + }; + + this.LocalCacheRoot.ShouldBeADirectory(fileSystem).WithFiles().ShouldNotContain(f => !allowedFileNames.Contains(f.Name)); + + string mappingFileContents = File.ReadAllText(mappingFile); + mappingFileContents.ShouldNotBeNull(); + string[] objectRootEntries = mappingFileContents.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Where(x => x.IndexOf(this.RepoUrl, StringComparison.OrdinalIgnoreCase) >= 0) + .ToArray(); + objectRootEntries.Length.ShouldEqual(1, $"Should be only one entry for repo url: {this.RepoUrl} mapping file content: {mappingFileContents}"); + objectRootEntries[0].Substring(0, 2).ShouldEqual("A ", $"Invalid mapping entry for repo: {objectRootEntries[0]}"); + JObject rootEntryJson = JObject.Parse(objectRootEntries[0].Substring(2)); + string objectRootFolder = rootEntryJson.GetValue("Value").ToString(); + objectRootFolder.ShouldNotBeNull(); + objectRootFolder.Length.ShouldBeAtLeast(1, $"Invalid object root folder: {objectRootFolder} for {this.RepoUrl} mapping file content: {mappingFileContents}"); + + return Path.Combine(this.LocalCacheRoot, objectRootFolder, "gitObjects"); + } + + public string GetPackRoot(FileSystemRunner fileSystem) + { + return Path.Combine(this.GetObjectRoot(fileSystem), "pack"); + } + + public void DeleteEnlistment() + { + TestResultsHelper.OutputScalarLogs(this); + RepositoryHelpers.DeleteTestDirectory(this.EnlistmentRoot); + } + + public void CloneAndMount(bool skipPrefetch) + { + this.scalarProcess.Clone(this.RepoUrl, this.Commitish, skipPrefetch); + + GitProcess.Invoke(this.RepoRoot, "checkout " + this.Commitish); + GitProcess.Invoke(this.RepoRoot, "branch --unset-upstream"); + GitProcess.Invoke(this.RepoRoot, "config core.abbrev 40"); + GitProcess.Invoke(this.RepoRoot, "config user.name \"Functional Test User\""); + GitProcess.Invoke(this.RepoRoot, "config user.email \"functional@test.com\""); + + // If this repository has a .gitignore file in the root directory, force it to be + // hydrated. This is because if the GitStatusCache feature is enabled, it will run + // a "git status" command asynchronously, which will hydrate the .gitignore file + // as it reads the ignore rules. Hydrate this file here so that it is consistently + // hydrated and there are no race conditions depending on when / if it is hydrated + // as part of an asynchronous status scan to rebuild the GitStatusCache. + string rootGitIgnorePath = Path.Combine(this.RepoRoot, ".gitignore"); + if (File.Exists(rootGitIgnorePath)) + { + File.ReadAllBytes(rootGitIgnorePath); + } + } + + public void MountScalar() + { + this.scalarProcess.Mount(); + } + + public bool TryMountScalar() + { + string output; + return this.TryMountScalar(out output); + } + + public bool TryMountScalar(out string output) + { + return this.scalarProcess.TryMount(out output); + } + + public string Prefetch(string args, bool failOnError = true, string standardInput = null) + { + return this.scalarProcess.Prefetch(args, failOnError, standardInput); + } + + public void Repair(bool confirm) + { + this.scalarProcess.Repair(confirm); + } + + public string Diagnose() + { + return this.scalarProcess.Diagnose(); + } + + public string LooseObjectStep() + { + return this.scalarProcess.LooseObjectStep(); + } + + public string PackfileMaintenanceStep(long? batchSize = null) + { + return this.scalarProcess.PackfileMaintenanceStep(batchSize); + } + + public string PostFetchStep() + { + return this.scalarProcess.PostFetchStep(); + } + + public string Status(string trace = null) + { + return this.scalarProcess.Status(trace); + } + + public bool WaitForBackgroundOperations(int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) + { + return this.WaitForStatus(maxWaitMilliseconds, ZeroBackgroundOperations).ShouldBeTrue("Background operations failed to complete."); + } + + public bool WaitForLock(string lockCommand, int maxWaitMilliseconds = DefaultMaxWaitMSForStatusCheck) + { + return this.WaitForStatus(maxWaitMilliseconds, string.Format(LockHeldByGit, lockCommand)); + } + + public void UnmountScalar() + { + this.scalarProcess.Unmount(); + } + + public string GetCacheServer() + { + return this.scalarProcess.CacheServer("--get"); + } + + public string SetCacheServer(string arg) + { + return this.scalarProcess.CacheServer("--set " + arg); + } + + public void UnmountAndDeleteAll() + { + this.UnmountScalar(); + this.DeleteEnlistment(); + } + + public string GetVirtualPathTo(string path) + { + // Replace '/' with Path.DirectorySeparatorChar to ensure that any + // Git paths are converted to system paths + return Path.Combine(this.RepoRoot, path.Replace('/', Path.DirectorySeparatorChar)); + } + + public string GetVirtualPathTo(params string[] pathParts) + { + return Path.Combine(this.RepoRoot, Path.Combine(pathParts)); + } + + public string GetObjectPathTo(string objectHash) + { + return Path.Combine( + this.RepoRoot, + TestConstants.DotGit.Objects.Root, + objectHash.Substring(0, 2), + objectHash.Substring(2)); + } + + private static ScalarFunctionalTestEnlistment CloneAndMount(string pathToGvfs, string enlistmentRoot, string commitish, string localCacheRoot, bool skipPrefetch = false) + { + ScalarFunctionalTestEnlistment enlistment = new ScalarFunctionalTestEnlistment( + pathToGvfs, + enlistmentRoot ?? GetUniqueEnlistmentRoot(), + ScalarTestConfig.RepoToClone, + commitish ?? Properties.Settings.Default.Commitish, + localCacheRoot ?? ScalarTestConfig.LocalCacheRoot); + + try + { + enlistment.CloneAndMount(skipPrefetch); + } + catch (Exception e) + { + Console.WriteLine("Unhandled exception in CloneAndMount: " + e.ToString()); + TestResultsHelper.OutputScalarLogs(enlistment); + throw; + } + + return enlistment; + } + + private static string GetRepoSpecificLocalCacheRoot(string enlistmentRoot) + { + return Path.Combine(enlistmentRoot, ScalarTestConfig.DotScalarRoot, ".scalarCache"); + } + + private bool WaitForStatus(int maxWaitMilliseconds, string statusShouldContain) + { + string status = null; + int totalWaitMilliseconds = 0; + while (totalWaitMilliseconds <= maxWaitMilliseconds && (status == null || !status.Contains(statusShouldContain))) + { + Thread.Sleep(SleepMSWaitingForStatusCheck); + status = this.Status(); + totalWaitMilliseconds += SleepMSWaitingForStatusCheck; + } + + return totalWaitMilliseconds <= maxWaitMilliseconds; + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ScalarHelpers.cs b/Scalar.FunctionalTests/Tools/ScalarHelpers.cs index d9b84dca8c..a740efdb88 100644 --- a/Scalar.FunctionalTests/Tools/ScalarHelpers.cs +++ b/Scalar.FunctionalTests/Tools/ScalarHelpers.cs @@ -1,252 +1,252 @@ -using Microsoft.Data.Sqlite; -using Newtonsoft.Json; -using NUnit.Framework; -using Scalar.FunctionalTests.FileSystemRunners; -using Scalar.FunctionalTests.Should; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Scalar.FunctionalTests.Tools -{ - public static class ScalarHelpers - { - public const string ModifiedPathsNewLine = "\r\n"; - public const string PlaceholderFieldDelimiter = "\0"; - - public static readonly string BackgroundOpsFile = Path.Combine("databases", "BackgroundGitOperations.dat"); - public static readonly string PlaceholderListFile = Path.Combine("databases", "PlaceholderList.dat"); - public static readonly string RepoMetadataName = Path.Combine("databases", "RepoMetadata.dat"); - - private const string ModifedPathsLineAddPrefix = "A "; - private const string ModifedPathsLineDeletePrefix = "D "; - - private const string DiskLayoutMajorVersionKey = "DiskLayoutVersion"; - private const string DiskLayoutMinorVersionKey = "DiskLayoutMinorVersion"; - private const string LocalCacheRootKey = "LocalCacheRoot"; - private const string GitObjectsRootKey = "GitObjectsRoot"; - private const string BlobSizesRootKey = "BlobSizesRoot"; - - public static string ConvertPathToGitFormat(string path) - { - return path.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator); - } - - public static void SaveDiskLayoutVersion(string dotScalarRoot, string majorVersion, string minorVersion) - { - SavePersistedValue(dotScalarRoot, DiskLayoutMajorVersionKey, majorVersion); - SavePersistedValue(dotScalarRoot, DiskLayoutMinorVersionKey, minorVersion); - } - - public static void GetPersistedDiskLayoutVersion(string dotScalarRoot, out string majorVersion, out string minorVersion) - { - majorVersion = GetPersistedValue(dotScalarRoot, DiskLayoutMajorVersionKey); - minorVersion = GetPersistedValue(dotScalarRoot, DiskLayoutMinorVersionKey); - } - - public static void SaveLocalCacheRoot(string dotScalarRoot, string value) - { - SavePersistedValue(dotScalarRoot, LocalCacheRootKey, value); - } - - public static string GetPersistedLocalCacheRoot(string dotScalarRoot) - { - return GetPersistedValue(dotScalarRoot, LocalCacheRootKey); - } - - public static void SaveGitObjectsRoot(string dotScalarRoot, string value) - { - SavePersistedValue(dotScalarRoot, GitObjectsRootKey, value); - } - - public static string GetPersistedGitObjectsRoot(string dotScalarRoot) - { - return GetPersistedValue(dotScalarRoot, GitObjectsRootKey); - } - - public static string GetPersistedBlobSizesRoot(string dotScalarRoot) - { - return GetPersistedValue(dotScalarRoot, BlobSizesRootKey); - } - - public static void SQLiteBlobSizesDatabaseHasEntry(string blobSizesDbPath, string blobSha, long blobSize) - { - RunSqliteCommand(blobSizesDbPath, command => - { - SqliteParameter shaParam = command.CreateParameter(); - shaParam.ParameterName = "@sha"; - command.CommandText = "SELECT size FROM BlobSizes WHERE sha = (@sha)"; - command.Parameters.Add(shaParam); - shaParam.Value = StringToShaBytes(blobSha); - - using (SqliteDataReader reader = command.ExecuteReader()) - { - reader.Read().ShouldBeTrue(); - reader.GetInt64(0).ShouldEqual(blobSize); - } - - return true; - }); - } - - public static string GetAllSQLitePlaceholdersAsString(string placeholdersDbPath) - { - return RunSqliteCommand(placeholdersDbPath, command => - { - command.CommandText = "SELECT path, pathType, sha FROM Placeholder"; - using (SqliteDataReader reader = command.ExecuteReader()) - { - StringBuilder sb = new StringBuilder(); - while (reader.Read()) - { - sb.Append(reader.GetString(0)); - sb.Append(PlaceholderFieldDelimiter); - sb.Append(reader.GetByte(1)); - sb.Append(PlaceholderFieldDelimiter); - if (!reader.IsDBNull(2)) - { - sb.Append(reader.GetString(2)); - sb.Append(PlaceholderFieldDelimiter); - } - - sb.AppendLine(); - } - - return sb.ToString(); - } - }); - } - - public static void AddPlaceholderFolder(string placeholdersDbPath, string path, int pathType) - { - RunSqliteCommand(placeholdersDbPath, command => - { - command.CommandText = "INSERT OR REPLACE INTO Placeholder (path, pathType, sha) VALUES (@path, @pathType, NULL)"; - command.Parameters.AddWithValue("@path", path); - command.Parameters.AddWithValue("@pathType", pathType); - return command.ExecuteNonQuery(); - }); - } - - public static string ReadAllTextFromWriteLockedFile(string filename) - { - // File.ReadAllText and others attempt to open for read and FileShare.None, which always fail on - // the placeholder db and other files that open for write and only share read access - using (StreamReader reader = new StreamReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) - { - return reader.ReadToEnd(); - } - } - - public static string GetInternalParameter(string maintenanceJob = "null", string packfileMaintenanceBatchSize = "null") - { - return $"\"{{\\\"ServiceName\\\":\\\"{ScalarServiceProcess.TestServiceName}\\\"," + - "\\\"StartedByService\\\":false," + - $"\\\"MaintenanceJob\\\":{maintenanceJob}," + - $"\\\"PackfileMaintenanceBatchSize\\\":{packfileMaintenanceBatchSize}}}\""; - } - - private static T RunSqliteCommand(string sqliteDbPath, Func runCommand) - { - string connectionString = $"data source={sqliteDbPath}"; - using (SqliteConnection connection = new SqliteConnection(connectionString)) - { - connection.Open(); - using (SqliteCommand command = connection.CreateCommand()) - { - return runCommand(command); - } - } - } - - private static byte[] StringToShaBytes(string sha) - { - byte[] shaBytes = new byte[20]; - - string upperCaseSha = sha.ToUpper(); - int stringIndex = 0; - for (int i = 0; i < 20; ++i) - { - stringIndex = i * 2; - char firstChar = sha[stringIndex]; - char secondChar = sha[stringIndex + 1]; - shaBytes[i] = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); - } - - return shaBytes; - } - - private static byte CharToByte(char c) - { - if (c >= '0' && c <= '9') - { - return (byte)(c - '0'); - } - - if (c >= 'A' && c <= 'F') - { - return (byte)(10 + (c - 'A')); - } - - Assert.Fail($"Invalid character c: {c}"); - - return 0; - } - - private static string GetPersistedValue(string dotScalarRoot, string key) - { - string metadataPath = Path.Combine(dotScalarRoot, RepoMetadataName); - string json; - using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) - using (StreamReader reader = new StreamReader(fs)) - { - while (!reader.EndOfStream) - { - json = reader.ReadLine(); - json.Substring(0, 2).ShouldEqual("A "); - - KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2)); - if (kvp.Key == key) - { - return kvp.Value; - } - } - } - - return null; - } - - private static void SavePersistedValue(string dotScalarRoot, string key, string value) - { - string metadataPath = Path.Combine(dotScalarRoot, RepoMetadataName); - - Dictionary repoMetadata = new Dictionary(); - string json; - using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) - using (StreamReader reader = new StreamReader(fs)) - { - while (!reader.EndOfStream) - { - json = reader.ReadLine(); - json.Substring(0, 2).ShouldEqual("A "); - - KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2)); - repoMetadata.Add(kvp.Key, kvp.Value); - } - } - - repoMetadata[key] = value; - - string newRepoMetadataContents = string.Empty; - - foreach (KeyValuePair kvp in repoMetadata) - { - newRepoMetadataContents += "A " + JsonConvert.SerializeObject(kvp).Trim() + "\r\n"; - } - - File.WriteAllText(metadataPath, newRepoMetadataContents); - } - } -} +using Microsoft.Data.Sqlite; +using Newtonsoft.Json; +using NUnit.Framework; +using Scalar.FunctionalTests.FileSystemRunners; +using Scalar.FunctionalTests.Should; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Scalar.FunctionalTests.Tools +{ + public static class ScalarHelpers + { + public const string ModifiedPathsNewLine = "\r\n"; + public const string PlaceholderFieldDelimiter = "\0"; + + public static readonly string BackgroundOpsFile = Path.Combine("databases", "BackgroundGitOperations.dat"); + public static readonly string PlaceholderListFile = Path.Combine("databases", "PlaceholderList.dat"); + public static readonly string RepoMetadataName = Path.Combine("databases", "RepoMetadata.dat"); + + private const string ModifedPathsLineAddPrefix = "A "; + private const string ModifedPathsLineDeletePrefix = "D "; + + private const string DiskLayoutMajorVersionKey = "DiskLayoutVersion"; + private const string DiskLayoutMinorVersionKey = "DiskLayoutMinorVersion"; + private const string LocalCacheRootKey = "LocalCacheRoot"; + private const string GitObjectsRootKey = "GitObjectsRoot"; + private const string BlobSizesRootKey = "BlobSizesRoot"; + + public static string ConvertPathToGitFormat(string path) + { + return path.Replace(Path.DirectorySeparatorChar, TestConstants.GitPathSeparator); + } + + public static void SaveDiskLayoutVersion(string dotScalarRoot, string majorVersion, string minorVersion) + { + SavePersistedValue(dotScalarRoot, DiskLayoutMajorVersionKey, majorVersion); + SavePersistedValue(dotScalarRoot, DiskLayoutMinorVersionKey, minorVersion); + } + + public static void GetPersistedDiskLayoutVersion(string dotScalarRoot, out string majorVersion, out string minorVersion) + { + majorVersion = GetPersistedValue(dotScalarRoot, DiskLayoutMajorVersionKey); + minorVersion = GetPersistedValue(dotScalarRoot, DiskLayoutMinorVersionKey); + } + + public static void SaveLocalCacheRoot(string dotScalarRoot, string value) + { + SavePersistedValue(dotScalarRoot, LocalCacheRootKey, value); + } + + public static string GetPersistedLocalCacheRoot(string dotScalarRoot) + { + return GetPersistedValue(dotScalarRoot, LocalCacheRootKey); + } + + public static void SaveGitObjectsRoot(string dotScalarRoot, string value) + { + SavePersistedValue(dotScalarRoot, GitObjectsRootKey, value); + } + + public static string GetPersistedGitObjectsRoot(string dotScalarRoot) + { + return GetPersistedValue(dotScalarRoot, GitObjectsRootKey); + } + + public static string GetPersistedBlobSizesRoot(string dotScalarRoot) + { + return GetPersistedValue(dotScalarRoot, BlobSizesRootKey); + } + + public static void SQLiteBlobSizesDatabaseHasEntry(string blobSizesDbPath, string blobSha, long blobSize) + { + RunSqliteCommand(blobSizesDbPath, command => + { + SqliteParameter shaParam = command.CreateParameter(); + shaParam.ParameterName = "@sha"; + command.CommandText = "SELECT size FROM BlobSizes WHERE sha = (@sha)"; + command.Parameters.Add(shaParam); + shaParam.Value = StringToShaBytes(blobSha); + + using (SqliteDataReader reader = command.ExecuteReader()) + { + reader.Read().ShouldBeTrue(); + reader.GetInt64(0).ShouldEqual(blobSize); + } + + return true; + }); + } + + public static string GetAllSQLitePlaceholdersAsString(string placeholdersDbPath) + { + return RunSqliteCommand(placeholdersDbPath, command => + { + command.CommandText = "SELECT path, pathType, sha FROM Placeholder"; + using (SqliteDataReader reader = command.ExecuteReader()) + { + StringBuilder sb = new StringBuilder(); + while (reader.Read()) + { + sb.Append(reader.GetString(0)); + sb.Append(PlaceholderFieldDelimiter); + sb.Append(reader.GetByte(1)); + sb.Append(PlaceholderFieldDelimiter); + if (!reader.IsDBNull(2)) + { + sb.Append(reader.GetString(2)); + sb.Append(PlaceholderFieldDelimiter); + } + + sb.AppendLine(); + } + + return sb.ToString(); + } + }); + } + + public static void AddPlaceholderFolder(string placeholdersDbPath, string path, int pathType) + { + RunSqliteCommand(placeholdersDbPath, command => + { + command.CommandText = "INSERT OR REPLACE INTO Placeholder (path, pathType, sha) VALUES (@path, @pathType, NULL)"; + command.Parameters.AddWithValue("@path", path); + command.Parameters.AddWithValue("@pathType", pathType); + return command.ExecuteNonQuery(); + }); + } + + public static string ReadAllTextFromWriteLockedFile(string filename) + { + // File.ReadAllText and others attempt to open for read and FileShare.None, which always fail on + // the placeholder db and other files that open for write and only share read access + using (StreamReader reader = new StreamReader(File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) + { + return reader.ReadToEnd(); + } + } + + public static string GetInternalParameter(string maintenanceJob = "null", string packfileMaintenanceBatchSize = "null") + { + return $"\"{{\\\"ServiceName\\\":\\\"{ScalarServiceProcess.TestServiceName}\\\"," + + "\\\"StartedByService\\\":false," + + $"\\\"MaintenanceJob\\\":{maintenanceJob}," + + $"\\\"PackfileMaintenanceBatchSize\\\":{packfileMaintenanceBatchSize}}}\""; + } + + private static T RunSqliteCommand(string sqliteDbPath, Func runCommand) + { + string connectionString = $"data source={sqliteDbPath}"; + using (SqliteConnection connection = new SqliteConnection(connectionString)) + { + connection.Open(); + using (SqliteCommand command = connection.CreateCommand()) + { + return runCommand(command); + } + } + } + + private static byte[] StringToShaBytes(string sha) + { + byte[] shaBytes = new byte[20]; + + string upperCaseSha = sha.ToUpper(); + int stringIndex = 0; + for (int i = 0; i < 20; ++i) + { + stringIndex = i * 2; + char firstChar = sha[stringIndex]; + char secondChar = sha[stringIndex + 1]; + shaBytes[i] = (byte)(CharToByte(firstChar) << 4 | CharToByte(secondChar)); + } + + return shaBytes; + } + + private static byte CharToByte(char c) + { + if (c >= '0' && c <= '9') + { + return (byte)(c - '0'); + } + + if (c >= 'A' && c <= 'F') + { + return (byte)(10 + (c - 'A')); + } + + Assert.Fail($"Invalid character c: {c}"); + + return 0; + } + + private static string GetPersistedValue(string dotScalarRoot, string key) + { + string metadataPath = Path.Combine(dotScalarRoot, RepoMetadataName); + string json; + using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (StreamReader reader = new StreamReader(fs)) + { + while (!reader.EndOfStream) + { + json = reader.ReadLine(); + json.Substring(0, 2).ShouldEqual("A "); + + KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2)); + if (kvp.Key == key) + { + return kvp.Value; + } + } + } + + return null; + } + + private static void SavePersistedValue(string dotScalarRoot, string key, string value) + { + string metadataPath = Path.Combine(dotScalarRoot, RepoMetadataName); + + Dictionary repoMetadata = new Dictionary(); + string json; + using (FileStream fs = new FileStream(metadataPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete)) + using (StreamReader reader = new StreamReader(fs)) + { + while (!reader.EndOfStream) + { + json = reader.ReadLine(); + json.Substring(0, 2).ShouldEqual("A "); + + KeyValuePair kvp = JsonConvert.DeserializeObject>(json.Substring(2)); + repoMetadata.Add(kvp.Key, kvp.Value); + } + } + + repoMetadata[key] = value; + + string newRepoMetadataContents = string.Empty; + + foreach (KeyValuePair kvp in repoMetadata) + { + newRepoMetadataContents += "A " + JsonConvert.SerializeObject(kvp).Trim() + "\r\n"; + } + + File.WriteAllText(metadataPath, newRepoMetadataContents); + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ScalarProcess.cs b/Scalar.FunctionalTests/Tools/ScalarProcess.cs index 14dac1a4ab..752926fceb 100644 --- a/Scalar.FunctionalTests/Tools/ScalarProcess.cs +++ b/Scalar.FunctionalTests/Tools/ScalarProcess.cs @@ -1,254 +1,254 @@ -using Scalar.Tests.Should; -using System; -using System.Diagnostics; -using System.IO; - -namespace Scalar.FunctionalTests.Tools -{ - public class ScalarProcess - { - private const int SuccessExitCode = 0; - private const int ExitCodeShouldNotBeZero = -1; - private const int DoNotCheckExitCode = -2; - - private readonly string pathToScalar; - private readonly string enlistmentRoot; - private readonly string localCacheRoot; - - public ScalarProcess(ScalarFunctionalTestEnlistment enlistment) - : this(ScalarTestConfig.PathToScalar, enlistment.EnlistmentRoot, Path.Combine(enlistment.EnlistmentRoot, ScalarTestConfig.DotScalarRoot)) - { - } - - public ScalarProcess(string pathToScalar, string enlistmentRoot, string localCacheRoot) - { - this.pathToScalar = pathToScalar; - this.enlistmentRoot = enlistmentRoot; - this.localCacheRoot = localCacheRoot; - } - - public void Clone(string repositorySource, string branchToCheckout, bool skipPrefetch) - { - string args = string.Format( - "clone \"{0}\" \"{1}\" --branch \"{2}\" --local-cache-path \"{3}\" {4}", - repositorySource, - this.enlistmentRoot, - branchToCheckout, - this.localCacheRoot, - skipPrefetch ? "--no-prefetch" : string.Empty); - this.CallScalar(args, expectedExitCode: SuccessExitCode); - } - - public void Mount() - { - string output; - this.TryMount(out output).ShouldEqual(true, "Scalar did not mount: " + output); - - // TODO: Re-add this warning after we work out the version detail information - // output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "warning"); - } - - public bool TryMount(out string output) - { - this.IsEnlistmentMounted().ShouldEqual(false, "Scalar is already mounted"); - output = this.CallScalar("mount \"" + this.enlistmentRoot + "\""); - return this.IsEnlistmentMounted(); - } - - public string AddSparseFolders(params string[] folders) - { - return this.SparseCommand(addFolders: true, shouldSucceed: true, folders: folders); - } - - public string AddSparseFolders(bool shouldSucceed, params string[] folders) - { - return this.SparseCommand(addFolders: true, shouldSucceed: shouldSucceed, folders: folders); - } - - public string RemoveSparseFolders(params string[] folders) - { - return this.SparseCommand(addFolders: false, shouldSucceed: true, folders: folders); - } - - public string RemoveSparseFolders(bool shouldSucceed, params string[] folders) - { - return this.SparseCommand(addFolders: false, shouldSucceed: shouldSucceed, folders: folders); - } - - public string SparseCommand(bool addFolders, bool shouldSucceed, params string[] folders) - { - string action = addFolders ? "-a" : "-r"; - string folderList = string.Join(";", folders); - if (folderList.Contains(" ")) - { - folderList = $"\"{folderList}\""; - } - - return this.CallScalar($"sparse {this.enlistmentRoot} {action} {folderList}", expectedExitCode: shouldSucceed ? SuccessExitCode : ExitCodeShouldNotBeZero); - } - - public string[] GetSparseFolders() - { - string output = this.CallScalar($"sparse {this.enlistmentRoot} -l"); - if (output.StartsWith("No folders in sparse list.")) - { - return new string[0]; - } - - return output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - } - - public string Prefetch(string args, bool failOnError, string standardInput = null) - { - return this.CallScalar("prefetch \"" + this.enlistmentRoot + "\" " + args, failOnError ? SuccessExitCode : DoNotCheckExitCode, standardInput: standardInput); - } - - public void Repair(bool confirm) - { - string confirmArg = confirm ? "--confirm " : string.Empty; - this.CallScalar( - "repair " + confirmArg + "\"" + this.enlistmentRoot + "\"", - expectedExitCode: SuccessExitCode); - } - - public string LooseObjectStep() - { - return this.CallScalar( - "dehydrate \"" + this.enlistmentRoot + "\"", - expectedExitCode: SuccessExitCode, - internalParameter: ScalarHelpers.GetInternalParameter("\\\"LooseObjects\\\"")); - } - - public string PackfileMaintenanceStep(long? batchSize) - { - string sizeString = batchSize.HasValue ? $"\\\"{batchSize.Value}\\\"" : "null"; - string internalParameter = ScalarHelpers.GetInternalParameter("\\\"PackfileMaintenance\\\"", sizeString); - return this.CallScalar( - "dehydrate \"" + this.enlistmentRoot + "\"", - expectedExitCode: SuccessExitCode, - internalParameter: internalParameter); - } - - public string PostFetchStep() - { - string internalParameter = ScalarHelpers.GetInternalParameter("\\\"PostFetch\\\""); - return this.CallScalar( - "dehydrate \"" + this.enlistmentRoot + "\"", - expectedExitCode: SuccessExitCode, - internalParameter: internalParameter); - } - - public string Diagnose() - { - return this.CallScalar("diagnose \"" + this.enlistmentRoot + "\""); - } - - public string Status(string trace = null) - { - return this.CallScalar("status " + this.enlistmentRoot, trace: trace); - } - - public string CacheServer(string args) - { - return this.CallScalar("cache-server " + args + " \"" + this.enlistmentRoot + "\""); - } - - public void Unmount() - { - if (this.IsEnlistmentMounted()) - { - string result = this.CallScalar("unmount \"" + this.enlistmentRoot + "\"", expectedExitCode: SuccessExitCode); - this.IsEnlistmentMounted().ShouldEqual(false, "Scalar did not unmount: " + result); - } - } - - public bool IsEnlistmentMounted() - { - string statusResult = this.CallScalar("status \"" + this.enlistmentRoot + "\""); - return statusResult.Contains("Mount status: Ready"); - } - - public string RunServiceVerb(string argument) - { - return this.CallScalar("service " + argument, expectedExitCode: SuccessExitCode); - } - - public string ReadConfig(string key, bool failOnError) - { - return this.CallScalar($"config {key}", failOnError ? SuccessExitCode : DoNotCheckExitCode).TrimEnd('\r', '\n'); - } - - public void WriteConfig(string key, string value) - { - this.CallScalar($"config {key} {value}", expectedExitCode: SuccessExitCode); - } - - public void DeleteConfig(string key) - { - this.CallScalar($"config --delete {key}", expectedExitCode: SuccessExitCode); - } - - /// - /// Invokes a call to scalar using the arguments specified - /// - /// The arguments to use when invoking scalar - /// - /// What the expected exit code should be. - /// >= than 0 to check the exit code explicitly - /// -1 = Fail if the exit code is 0 - /// -2 = Do not check the exit code (Default) - /// - /// What to set the GIT_TRACE environment variable to - /// What to write to the standard input stream - /// The internal parameter to set in the arguments - /// - private string CallScalar(string args, int expectedExitCode = DoNotCheckExitCode, string trace = null, string standardInput = null, string internalParameter = null) - { - ProcessStartInfo processInfo = null; - processInfo = new ProcessStartInfo(this.pathToScalar); - - if (internalParameter == null) - { - internalParameter = ScalarHelpers.GetInternalParameter(); - } - - processInfo.Arguments = args + " " + TestConstants.InternalUseOnlyFlag + " " + internalParameter; - - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - if (standardInput != null) - { - processInfo.RedirectStandardInput = true; - } - - if (trace != null) - { - processInfo.EnvironmentVariables["GIT_TRACE"] = trace; - } - - using (Process process = Process.Start(processInfo)) - { - if (standardInput != null) - { - process.StandardInput.Write(standardInput); - process.StandardInput.Close(); - } - - string result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - if (expectedExitCode >= SuccessExitCode) - { - process.ExitCode.ShouldEqual(expectedExitCode, result); - } - else if (expectedExitCode == ExitCodeShouldNotBeZero) - { - process.ExitCode.ShouldNotEqual(SuccessExitCode, "Exit code should not be zero"); - } - - return result; - } - } - } -} +using Scalar.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; + +namespace Scalar.FunctionalTests.Tools +{ + public class ScalarProcess + { + private const int SuccessExitCode = 0; + private const int ExitCodeShouldNotBeZero = -1; + private const int DoNotCheckExitCode = -2; + + private readonly string pathToScalar; + private readonly string enlistmentRoot; + private readonly string localCacheRoot; + + public ScalarProcess(ScalarFunctionalTestEnlistment enlistment) + : this(ScalarTestConfig.PathToScalar, enlistment.EnlistmentRoot, Path.Combine(enlistment.EnlistmentRoot, ScalarTestConfig.DotScalarRoot)) + { + } + + public ScalarProcess(string pathToScalar, string enlistmentRoot, string localCacheRoot) + { + this.pathToScalar = pathToScalar; + this.enlistmentRoot = enlistmentRoot; + this.localCacheRoot = localCacheRoot; + } + + public void Clone(string repositorySource, string branchToCheckout, bool skipPrefetch) + { + string args = string.Format( + "clone \"{0}\" \"{1}\" --branch \"{2}\" --local-cache-path \"{3}\" {4}", + repositorySource, + this.enlistmentRoot, + branchToCheckout, + this.localCacheRoot, + skipPrefetch ? "--no-prefetch" : string.Empty); + this.CallScalar(args, expectedExitCode: SuccessExitCode); + } + + public void Mount() + { + string output; + this.TryMount(out output).ShouldEqual(true, "Scalar did not mount: " + output); + + // TODO: Re-add this warning after we work out the version detail information + // output.ShouldNotContain(ignoreCase: true, unexpectedSubstrings: "warning"); + } + + public bool TryMount(out string output) + { + this.IsEnlistmentMounted().ShouldEqual(false, "Scalar is already mounted"); + output = this.CallScalar("mount \"" + this.enlistmentRoot + "\""); + return this.IsEnlistmentMounted(); + } + + public string AddSparseFolders(params string[] folders) + { + return this.SparseCommand(addFolders: true, shouldSucceed: true, folders: folders); + } + + public string AddSparseFolders(bool shouldSucceed, params string[] folders) + { + return this.SparseCommand(addFolders: true, shouldSucceed: shouldSucceed, folders: folders); + } + + public string RemoveSparseFolders(params string[] folders) + { + return this.SparseCommand(addFolders: false, shouldSucceed: true, folders: folders); + } + + public string RemoveSparseFolders(bool shouldSucceed, params string[] folders) + { + return this.SparseCommand(addFolders: false, shouldSucceed: shouldSucceed, folders: folders); + } + + public string SparseCommand(bool addFolders, bool shouldSucceed, params string[] folders) + { + string action = addFolders ? "-a" : "-r"; + string folderList = string.Join(";", folders); + if (folderList.Contains(" ")) + { + folderList = $"\"{folderList}\""; + } + + return this.CallScalar($"sparse {this.enlistmentRoot} {action} {folderList}", expectedExitCode: shouldSucceed ? SuccessExitCode : ExitCodeShouldNotBeZero); + } + + public string[] GetSparseFolders() + { + string output = this.CallScalar($"sparse {this.enlistmentRoot} -l"); + if (output.StartsWith("No folders in sparse list.")) + { + return new string[0]; + } + + return output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + } + + public string Prefetch(string args, bool failOnError, string standardInput = null) + { + return this.CallScalar("prefetch \"" + this.enlistmentRoot + "\" " + args, failOnError ? SuccessExitCode : DoNotCheckExitCode, standardInput: standardInput); + } + + public void Repair(bool confirm) + { + string confirmArg = confirm ? "--confirm " : string.Empty; + this.CallScalar( + "repair " + confirmArg + "\"" + this.enlistmentRoot + "\"", + expectedExitCode: SuccessExitCode); + } + + public string LooseObjectStep() + { + return this.CallScalar( + "dehydrate \"" + this.enlistmentRoot + "\"", + expectedExitCode: SuccessExitCode, + internalParameter: ScalarHelpers.GetInternalParameter("\\\"LooseObjects\\\"")); + } + + public string PackfileMaintenanceStep(long? batchSize) + { + string sizeString = batchSize.HasValue ? $"\\\"{batchSize.Value}\\\"" : "null"; + string internalParameter = ScalarHelpers.GetInternalParameter("\\\"PackfileMaintenance\\\"", sizeString); + return this.CallScalar( + "dehydrate \"" + this.enlistmentRoot + "\"", + expectedExitCode: SuccessExitCode, + internalParameter: internalParameter); + } + + public string PostFetchStep() + { + string internalParameter = ScalarHelpers.GetInternalParameter("\\\"PostFetch\\\""); + return this.CallScalar( + "dehydrate \"" + this.enlistmentRoot + "\"", + expectedExitCode: SuccessExitCode, + internalParameter: internalParameter); + } + + public string Diagnose() + { + return this.CallScalar("diagnose \"" + this.enlistmentRoot + "\""); + } + + public string Status(string trace = null) + { + return this.CallScalar("status " + this.enlistmentRoot, trace: trace); + } + + public string CacheServer(string args) + { + return this.CallScalar("cache-server " + args + " \"" + this.enlistmentRoot + "\""); + } + + public void Unmount() + { + if (this.IsEnlistmentMounted()) + { + string result = this.CallScalar("unmount \"" + this.enlistmentRoot + "\"", expectedExitCode: SuccessExitCode); + this.IsEnlistmentMounted().ShouldEqual(false, "Scalar did not unmount: " + result); + } + } + + public bool IsEnlistmentMounted() + { + string statusResult = this.CallScalar("status \"" + this.enlistmentRoot + "\""); + return statusResult.Contains("Mount status: Ready"); + } + + public string RunServiceVerb(string argument) + { + return this.CallScalar("service " + argument, expectedExitCode: SuccessExitCode); + } + + public string ReadConfig(string key, bool failOnError) + { + return this.CallScalar($"config {key}", failOnError ? SuccessExitCode : DoNotCheckExitCode).TrimEnd('\r', '\n'); + } + + public void WriteConfig(string key, string value) + { + this.CallScalar($"config {key} {value}", expectedExitCode: SuccessExitCode); + } + + public void DeleteConfig(string key) + { + this.CallScalar($"config --delete {key}", expectedExitCode: SuccessExitCode); + } + + /// + /// Invokes a call to scalar using the arguments specified + /// + /// The arguments to use when invoking scalar + /// + /// What the expected exit code should be. + /// >= than 0 to check the exit code explicitly + /// -1 = Fail if the exit code is 0 + /// -2 = Do not check the exit code (Default) + /// + /// What to set the GIT_TRACE environment variable to + /// What to write to the standard input stream + /// The internal parameter to set in the arguments + /// + private string CallScalar(string args, int expectedExitCode = DoNotCheckExitCode, string trace = null, string standardInput = null, string internalParameter = null) + { + ProcessStartInfo processInfo = null; + processInfo = new ProcessStartInfo(this.pathToScalar); + + if (internalParameter == null) + { + internalParameter = ScalarHelpers.GetInternalParameter(); + } + + processInfo.Arguments = args + " " + TestConstants.InternalUseOnlyFlag + " " + internalParameter; + + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + if (standardInput != null) + { + processInfo.RedirectStandardInput = true; + } + + if (trace != null) + { + processInfo.EnvironmentVariables["GIT_TRACE"] = trace; + } + + using (Process process = Process.Start(processInfo)) + { + if (standardInput != null) + { + process.StandardInput.Write(standardInput); + process.StandardInput.Close(); + } + + string result = process.StandardOutput.ReadToEnd(); + process.WaitForExit(); + + if (expectedExitCode >= SuccessExitCode) + { + process.ExitCode.ShouldEqual(expectedExitCode, result); + } + else if (expectedExitCode == ExitCodeShouldNotBeZero) + { + process.ExitCode.ShouldNotEqual(SuccessExitCode, "Exit code should not be zero"); + } + + return result; + } + } + } +} diff --git a/Scalar.FunctionalTests/Tools/ScalarServiceProcess.cs b/Scalar.FunctionalTests/Tools/ScalarServiceProcess.cs index c5a18e97d9..39fedd1314 100644 --- a/Scalar.FunctionalTests/Tools/ScalarServiceProcess.cs +++ b/Scalar.FunctionalTests/Tools/ScalarServiceProcess.cs @@ -1,151 +1,151 @@ -using Scalar.Tests.Should; -using System; -using System.Diagnostics; -using System.IO; -using System.Linq; +using Scalar.Tests.Should; +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; using System.Runtime.InteropServices; -using System.ServiceProcess; -using System.Threading; - -namespace Scalar.FunctionalTests.Tools -{ - public static class ScalarServiceProcess - { - private static readonly string ServiceNameArgument = "--servicename=" + TestServiceName; - - public static string TestServiceName - { - get - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - // Service name is used in the lookup of Scalar.Service communication pipes - // by clients for IPC. Custom pipe names can be specified as command line - // args to the Service during its startup. On the Mac, Scalar.Service started - // for testing is the same instance as the one that is installed by the - // Mac installer. Also it is not as easy as in Windows to pass command line - // args (that are specific to testing) to Scalar.Service (Service on Mac is a - // Launch agent and needs its args to be specified in its launchd.plist - // file). So on Mac - during tests Scalar.Service is started without any - // customized pipe name for testing. - return "Scalar.Service"; - } - - return "Test.Scalar.Service"; - } - } - - public static void InstallService() - { - Console.WriteLine("Installing " + TestServiceName); - - UninstallService(); - - // Wait for delete to complete. If the services control panel is open, this will never complete. - while (RunScCommand("query", TestServiceName).ExitCode == 0) - { - Thread.Sleep(1000); - } - - // Install service - string pathToService = GetPathToService(); - Console.WriteLine("Using service executable: " + pathToService); - - File.Exists(pathToService).ShouldBeTrue($"{pathToService} does not exist"); - - string createServiceArguments = string.Format( - "{0} binPath= \"{1}\"", - TestServiceName, - pathToService); - - ProcessResult result = RunScCommand("create", createServiceArguments); - result.ExitCode.ShouldEqual(0, "Failure while running sc create " + createServiceArguments + "\r\n" + result.Output); - - StartService(); - } - - public static void UninstallService() - { - StopService(); - - RunScCommand("delete", TestServiceName); - - // Make sure to delete any test service data state - string serviceData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Scalar", TestServiceName); - DirectoryInfo serviceDataDir = new DirectoryInfo(serviceData); - if (serviceDataDir.Exists) - { - serviceDataDir.Delete(true); - } - } - - public static void StartService() - { - ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName); - testService.ShouldNotBeNull($"{TestServiceName} does not exist as a service"); - - using (ServiceController controller = new ServiceController(TestServiceName)) - { - controller.Start(new[] { ServiceNameArgument }); - controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)); - controller.Status.ShouldEqual(ServiceControllerStatus.Running); - } - } - - public static void StopService() - { - try - { - ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName); - if (testService != null) - { - if (testService.Status == ServiceControllerStatus.Running) - { - testService.Stop(); - } - - testService.WaitForStatus(ServiceControllerStatus.Stopped); - } - } - catch (InvalidOperationException) - { - return; - } - } - - private static ProcessResult RunScCommand(string command, string parameters) - { - ProcessStartInfo processInfo = new ProcessStartInfo("sc"); - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - processInfo.Arguments = command + " " + parameters; - - return ProcessHelper.Run(processInfo); - } - - private static string GetPathToService() - { - if (ScalarTestConfig.TestScalarOnPath) - { - ProcessResult result = ProcessHelper.Run("where", Properties.Settings.Default.PathToScalarService); - result.ExitCode.ShouldEqual(0, $"{nameof(GetPathToService)}: where returned {result.ExitCode} when looking for {Properties.Settings.Default.PathToScalarService}"); - - string firstPath = - string.IsNullOrWhiteSpace(result.Output) - ? null - : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); - - firstPath.ShouldNotBeNull($"{nameof(GetPathToService)}: Failed to find {Properties.Settings.Default.PathToScalarService}"); - return firstPath; - } - else - { - return Path.Combine( - Properties.Settings.Default.CurrentDirectory, - Properties.Settings.Default.PathToScalarService); - } - } - } -} +using System.ServiceProcess; +using System.Threading; + +namespace Scalar.FunctionalTests.Tools +{ + public static class ScalarServiceProcess + { + private static readonly string ServiceNameArgument = "--servicename=" + TestServiceName; + + public static string TestServiceName + { + get + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + // Service name is used in the lookup of Scalar.Service communication pipes + // by clients for IPC. Custom pipe names can be specified as command line + // args to the Service during its startup. On the Mac, Scalar.Service started + // for testing is the same instance as the one that is installed by the + // Mac installer. Also it is not as easy as in Windows to pass command line + // args (that are specific to testing) to Scalar.Service (Service on Mac is a + // Launch agent and needs its args to be specified in its launchd.plist + // file). So on Mac - during tests Scalar.Service is started without any + // customized pipe name for testing. + return "Scalar.Service"; + } + + return "Test.Scalar.Service"; + } + } + + public static void InstallService() + { + Console.WriteLine("Installing " + TestServiceName); + + UninstallService(); + + // Wait for delete to complete. If the services control panel is open, this will never complete. + while (RunScCommand("query", TestServiceName).ExitCode == 0) + { + Thread.Sleep(1000); + } + + // Install service + string pathToService = GetPathToService(); + Console.WriteLine("Using service executable: " + pathToService); + + File.Exists(pathToService).ShouldBeTrue($"{pathToService} does not exist"); + + string createServiceArguments = string.Format( + "{0} binPath= \"{1}\"", + TestServiceName, + pathToService); + + ProcessResult result = RunScCommand("create", createServiceArguments); + result.ExitCode.ShouldEqual(0, "Failure while running sc create " + createServiceArguments + "\r\n" + result.Output); + + StartService(); + } + + public static void UninstallService() + { + StopService(); + + RunScCommand("delete", TestServiceName); + + // Make sure to delete any test service data state + string serviceData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "Scalar", TestServiceName); + DirectoryInfo serviceDataDir = new DirectoryInfo(serviceData); + if (serviceDataDir.Exists) + { + serviceDataDir.Delete(true); + } + } + + public static void StartService() + { + ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName); + testService.ShouldNotBeNull($"{TestServiceName} does not exist as a service"); + + using (ServiceController controller = new ServiceController(TestServiceName)) + { + controller.Start(new[] { ServiceNameArgument }); + controller.WaitForStatus(ServiceControllerStatus.Running, TimeSpan.FromSeconds(10)); + controller.Status.ShouldEqual(ServiceControllerStatus.Running); + } + } + + public static void StopService() + { + try + { + ServiceController testService = ServiceController.GetServices().SingleOrDefault(service => service.ServiceName == TestServiceName); + if (testService != null) + { + if (testService.Status == ServiceControllerStatus.Running) + { + testService.Stop(); + } + + testService.WaitForStatus(ServiceControllerStatus.Stopped); + } + } + catch (InvalidOperationException) + { + return; + } + } + + private static ProcessResult RunScCommand(string command, string parameters) + { + ProcessStartInfo processInfo = new ProcessStartInfo("sc"); + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + processInfo.Arguments = command + " " + parameters; + + return ProcessHelper.Run(processInfo); + } + + private static string GetPathToService() + { + if (ScalarTestConfig.TestScalarOnPath) + { + ProcessResult result = ProcessHelper.Run("where", Properties.Settings.Default.PathToScalarService); + result.ExitCode.ShouldEqual(0, $"{nameof(GetPathToService)}: where returned {result.ExitCode} when looking for {Properties.Settings.Default.PathToScalarService}"); + + string firstPath = + string.IsNullOrWhiteSpace(result.Output) + ? null + : result.Output.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); + + firstPath.ShouldNotBeNull($"{nameof(GetPathToService)}: Failed to find {Properties.Settings.Default.PathToScalarService}"); + return firstPath; + } + else + { + return Path.Combine( + Properties.Settings.Default.CurrentDirectory, + Properties.Settings.Default.PathToScalarService); + } + } + } +} diff --git a/Scalar.FunctionalTests/Tools/TestConstants.cs b/Scalar.FunctionalTests/Tools/TestConstants.cs index 66c0b962f8..31df1233d0 100644 --- a/Scalar.FunctionalTests/Tools/TestConstants.cs +++ b/Scalar.FunctionalTests/Tools/TestConstants.cs @@ -1,42 +1,42 @@ -using System.IO; - -namespace Scalar.FunctionalTests.Tools -{ - public static class TestConstants - { - public const string AllZeroSha = "0000000000000000000000000000000000000000"; - public const string PartialFolderPlaceholderDatabaseValue = " PARTIAL FOLDER"; - public const char GitPathSeparator = '/'; - public const string InternalUseOnlyFlag = "--internal_use_only"; - - public static class DotGit - { - public const string Root = ".git"; - public static readonly string Head = Path.Combine(DotGit.Root, "HEAD"); - - public static class Objects - { - public static readonly string Root = Path.Combine(DotGit.Root, "objects"); - } - - public static class Info - { - public const string Name = "info"; - public const string AlwaysExcludeName = "always_exclude"; - public const string SparseCheckoutName = "sparse-checkout"; - - public static readonly string Root = Path.Combine(DotGit.Root, Info.Name); - public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName); - public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName); - } - } - - public static class Databases - { - public const string Root = "databases"; - public static readonly string BackgroundOpsFile = Path.Combine(Root, "BackgroundGitOperations.dat"); - public static readonly string ModifiedPaths = Path.Combine(Root, "ModifiedPaths.dat"); - public static readonly string Scalar = Path.Combine(Root, "Scalar.sqlite"); - } - } -} +using System.IO; + +namespace Scalar.FunctionalTests.Tools +{ + public static class TestConstants + { + public const string AllZeroSha = "0000000000000000000000000000000000000000"; + public const string PartialFolderPlaceholderDatabaseValue = " PARTIAL FOLDER"; + public const char GitPathSeparator = '/'; + public const string InternalUseOnlyFlag = "--internal_use_only"; + + public static class DotGit + { + public const string Root = ".git"; + public static readonly string Head = Path.Combine(DotGit.Root, "HEAD"); + + public static class Objects + { + public static readonly string Root = Path.Combine(DotGit.Root, "objects"); + } + + public static class Info + { + public const string Name = "info"; + public const string AlwaysExcludeName = "always_exclude"; + public const string SparseCheckoutName = "sparse-checkout"; + + public static readonly string Root = Path.Combine(DotGit.Root, Info.Name); + public static readonly string SparseCheckoutPath = Path.Combine(Info.Root, Info.SparseCheckoutName); + public static readonly string AlwaysExcludePath = Path.Combine(Info.Root, AlwaysExcludeName); + } + } + + public static class Databases + { + public const string Root = "databases"; + public static readonly string BackgroundOpsFile = Path.Combine(Root, "BackgroundGitOperations.dat"); + public static readonly string ModifiedPaths = Path.Combine(Root, "ModifiedPaths.dat"); + public static readonly string Scalar = Path.Combine(Root, "Scalar.sqlite"); + } + } +} diff --git a/Scalar.Installer.Mac/Scalar.Installer.Mac.csproj b/Scalar.Installer.Mac/Scalar.Installer.Mac.csproj index 46e33a58d9..5b2c796cf4 100644 --- a/Scalar.Installer.Mac/Scalar.Installer.Mac.csproj +++ b/Scalar.Installer.Mac/Scalar.Installer.Mac.csproj @@ -1,37 +1,37 @@ - - - - - - Scalar.Installer.Mac - Scalar.Installer.Mac - netcoreapp2.1 - x64 - osx-x64 - - - - $(ScalarVersion) - - - - $(ScalarVersion) - - - - - - - - - - - - - - - - - - - + + + + + + Scalar.Installer.Mac + Scalar.Installer.Mac + netcoreapp2.1 + x64 + osx-x64 + + + + $(ScalarVersion) + + + + $(ScalarVersion) + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.Installer.Windows/Scalar.Installer.Windows.csproj b/Scalar.Installer.Windows/Scalar.Installer.Windows.csproj index 71484d078f..c1da9fe09c 100644 --- a/Scalar.Installer.Windows/Scalar.Installer.Windows.csproj +++ b/Scalar.Installer.Windows/Scalar.Installer.Windows.csproj @@ -1,65 +1,65 @@ - - - - - - - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893} - Library - Properties - Scalar.Installer.Windows - Scalar.Installer.Windows - v4.6.1 - 512 - - - - - true - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - - - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - - - - Microsoft400 - false - - - - - - - - Designer - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - + + + + + + + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893} + Library + Properties + Scalar.Installer.Windows + Scalar.Installer.Windows + v4.6.1 + 512 + + + + + true + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + Microsoft400 + false + + + + + + + + Designer + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + diff --git a/Scalar.Installer.Windows/Setup.iss b/Scalar.Installer.Windows/Setup.iss index abed555d9e..a643e74e04 100644 --- a/Scalar.Installer.Windows/Setup.iss +++ b/Scalar.Installer.Windows/Setup.iss @@ -1,607 +1,607 @@ -; This script requires Inno Setup Compiler 5.5.9 or later to compile -; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php - -; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php - -#define VCRuntimeDir PackagesDir + "\GVFS.VCRuntime.0.2.0-build\lib\x64" -#define ScalarDir BuildOutputDir + "\Scalar.Windows\bin\" + PlatformAndConfiguration -#define ScalarCommonDir BuildOutputDir + "\Scalar.Common\bin\" + PlatformAndConfiguration + "\netstandard2.0" -#define ServiceDir BuildOutputDir + "\Scalar.Service.Windows\bin\" + PlatformAndConfiguration -#define ServiceUIDir BuildOutputDir + "\Scalar.Service.UI\bin\" + PlatformAndConfiguration -#define ScalarMountDir BuildOutputDir + "\Scalar.Mount.Windows\bin\" + PlatformAndConfiguration -#define ReadObjectDir BuildOutputDir + "\Scalar.ReadObjectHook.Windows\bin\" + PlatformAndConfiguration -#define ScalarUpgraderDir BuildOutputDir + "\Scalar.Upgrader\bin\" + PlatformAndConfiguration + "\net461" - -#define MyAppName "Scalar" -#define MyAppInstallerVersion GetFileVersion(ScalarDir + "\Scalar.exe") -#define MyAppPublisher "Microsoft Corporation" -#define MyAppPublisherURL "http://www.microsoft.com" -#define MyAppURL "https://github.com/microsoft/Scalar" -#define MyAppExeName "Scalar.exe" -#define EnvironmentKey "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" -#define FileSystemKey "SYSTEM\CurrentControlSet\Control\FileSystem" - -[Setup] -AppId={{489CA581-F131-4C28-BE04-4FB178933E6D} -AppName={#MyAppName} -AppVersion={#MyAppInstallerVersion} -VersionInfoVersion={#MyAppInstallerVersion} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppPublisherURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -AppCopyright=Copyright � Microsoft 2019 -BackColor=clWhite -BackSolid=yes -DefaultDirName={pf}\{#MyAppName} -OutputBaseFilename=SetupScalar.{#ScalarVersion} -OutputDir=Setup -Compression=lzma2 -InternalCompressLevel=ultra64 -SolidCompression=yes -MinVersion=10.0.14374 -DisableDirPage=yes -DisableReadyPage=yes -SetupIconFile="{#ScalarDir}\GitVirtualFileSystem.ico" -ArchitecturesInstallIn64BitMode=x64 -ArchitecturesAllowed=x64 -WizardImageStretch=no -WindowResizable=no -CloseApplications=yes -ChangesEnvironment=yes -RestartIfNeededByRun=yes - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl"; - -[Types] -Name: "full"; Description: "Full installation"; Flags: iscustom; - -[Components] - -[InstallDelete] -; Delete old dependencies from VS 2015 VC redistributables -Type: files; Name: "{app}\ucrtbase.dll" - -[Files] - -; Scalar.Common Files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarCommonDir}\git2.dll" - -; Scalar.Mount Files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.exe" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.exe.config" - -; Scalar.Upgrader Files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.exe" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.exe.config" - -; Scalar.ReadObjectHook files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectDir}\Scalar.ReadObjectHook.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectDir}\Scalar.ReadObjectHook.exe" - -; Cpp Dependencies -DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_1.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_2.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\vcruntime140.dll" - -; Scalar PDB's -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Common.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Platform.Windows.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.pdb" - -; Scalar.Service.UI Files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.exe" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.exe.config" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\GitVirtualFileSystem.ico" - -; Scalar Files -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\CommandLine.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Common.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Platform.Windows.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Newtonsoft.Json.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.exe.config" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\GitVirtualFileSystem.ico" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.exe" - -; NuGet support DLLs -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Commands.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Common.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Configuration.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Frameworks.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Packaging.Core.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Packaging.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Protocol.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Versioning.dll" - -; .NET Standard Files -; See https://github.com/dotnet/standard/issues/415 for a discussion on why this are copied -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\netstandard.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.Net.Http.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.ValueTuple.dll" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.IO.Compression.dll" - -; Scalar.Service Files and PDB's -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.pdb" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.exe.config" -DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.exe"; AfterInstall: InstallScalarService - -[UninstallDelete] -; Deletes the entire installation directory, including files and subdirectories -Type: filesandordirs; Name: "{app}"; -Type: filesandordirs; Name: "{commonappdata}\Scalar\Scalar.Upgrade"; - -[Registry] -Root: HKLM; Subkey: "{#EnvironmentKey}"; \ - ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{app}"; \ - Check: NeedsAddPath(ExpandConstant('{app}')) - -Root: HKLM; Subkey: "{#FileSystemKey}"; \ - ValueType: dword; ValueName: "NtfsEnableDetailedCleanupResults"; ValueData: "1"; \ - Check: IsWindows10VersionPriorToCreatorsUpdate - -[Code] -var - ExitCode: Integer; - -function NeedsAddPath(Param: string): boolean; -var - OrigPath: string; -begin - if not RegQueryStringValue(HKEY_LOCAL_MACHINE, - '{#EnvironmentKey}', - 'PATH', OrigPath) - then begin - Result := True; - exit; - end; - // look for the path with leading and trailing semicolon - // Pos() returns 0 if not found - Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; -end; - -function IsWindows10VersionPriorToCreatorsUpdate(): Boolean; -var - Version: TWindowsVersion; -begin - GetWindowsVersionEx(Version); - Result := (Version.Major = 10) and (Version.Minor = 0) and (Version.Build < 15063); -end; - -procedure RemovePath(Path: string); -var - Paths: string; - PathMatchIndex: Integer; -begin - if not RegQueryStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then - begin - Log('PATH not found'); - end - else - begin - Log(Format('PATH is [%s]', [Paths])); - - PathMatchIndex := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); - if PathMatchIndex = 0 then - begin - Log(Format('Path [%s] not found in PATH', [Path])); - end - else - begin - Delete(Paths, PathMatchIndex - 1, Length(Path) + 1); - Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths])); - - if RegWriteStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then - begin - Log('PATH written'); - end - else - begin - Log('Error writing PATH'); - end; - end; - end; -end; - -procedure StopService(ServiceName: string); -var - ResultCode: integer; -begin - Log('StopService: stopping: ' + ServiceName); - // ErrorCode 1060 means service not installed, 1062 means service not started - if not Exec(ExpandConstant('{sys}\SC.EXE'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) and (ResultCode <> 1062) then - begin - RaiseException('Fatal: Could not stop service: ' + ServiceName); - end; -end; - -procedure UninstallService(ServiceName: string; ShowProgress: boolean); -var - ResultCode: integer; -begin - if Exec(ExpandConstant('{sys}\SC.EXE'), 'query ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) then - begin - Log('UninstallService: uninstalling service: ' + ServiceName); - if (ShowProgress) then - begin - WizardForm.StatusLabel.Caption := 'Uninstalling service: ' + ServiceName; - WizardForm.ProgressGauge.Style := npbstMarquee; - end; - - try - StopService(ServiceName); - - if not Exec(ExpandConstant('{sys}\SC.EXE'), 'delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then - begin - Log('UninstallService: Could not uninstall service: ' + ServiceName); - RaiseException('Fatal: Could not uninstall service: ' + ServiceName); - end; - - if (ShowProgress) then - begin - WizardForm.StatusLabel.Caption := 'Waiting for pending ' + ServiceName + ' deletion to complete. This may take a while.'; - end; - - finally - if (ShowProgress) then - begin - WizardForm.ProgressGauge.Style := npbstNormal; - end; - end; - - end; -end; - -procedure WriteOnDiskVersion16CapableFile(); -var - FilePath: string; -begin - FilePath := ExpandConstant('{app}\OnDiskVersion16CapableInstallation.dat'); - if not FileExists(FilePath) then - begin - Log('WriteOnDiskVersion16CapableFile: Writing file ' + FilePath); - SaveStringToFile(FilePath, '', False); - end -end; - -procedure InstallScalarService(); -var - ResultCode: integer; - StatusText: string; - InstallSuccessful: Boolean; -begin - InstallSuccessful := False; - - StatusText := WizardForm.StatusLabel.Caption; - WizardForm.StatusLabel.Caption := 'Installing Scalar.Service.'; - WizardForm.ProgressGauge.Style := npbstMarquee; - - try - if Exec(ExpandConstant('{sys}\SC.EXE'), ExpandConstant('create Scalar.Service binPath="{app}\Scalar.Service.exe" start=auto'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then - begin - if Exec(ExpandConstant('{sys}\SC.EXE'), 'failure Scalar.Service reset= 30 actions= restart/10/restart/5000//1', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - if Exec(ExpandConstant('{sys}\SC.EXE'), 'start Scalar.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - InstallSuccessful := True; - end; - end; - end; - - WriteOnDiskVersion16CapableFile(); - finally - WizardForm.StatusLabel.Caption := StatusText; - WizardForm.ProgressGauge.Style := npbstNormal; - end; - - if InstallSuccessful = False then - begin - RaiseException('Fatal: An error occured while installing Scalar.Service.'); - end; -end; - -function DeleteFileIfItExists(FilePath: string) : Boolean; -begin - Result := False; - if FileExists(FilePath) then - begin - Log('DeleteFileIfItExists: Removing ' + FilePath); - if DeleteFile(FilePath) then - begin - if not FileExists(FilePath) then - begin - Result := True; - end - else - begin - Log('DeleteFileIfItExists: File still exists after deleting: ' + FilePath); - end; - end - else - begin - Log('DeleteFileIfItExists: Failed to delete ' + FilePath); - end; - end - else - begin - Log('DeleteFileIfItExists: File does not exist: ' + FilePath); - Result := True; - end; -end; - -function IsScalarRunning(): Boolean; -var - ResultCode: integer; -begin - if Exec('powershell.exe', '-NoProfile "Get-Process scalar,scalar.mount | foreach {exit 10}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then - begin - if ResultCode = 10 then - begin - Result := True; - end; - if ResultCode = 1 then - begin - Result := False; - end; - end; -end; - -function ExecWithResult(Filename, Params, WorkingDir: String; ShowCmd: Integer; - Wait: TExecWait; var ResultCode: Integer; var ResultString: ansiString): Boolean; -var - TempFilename: string; - Command: string; -begin - TempFilename := ExpandConstant('{tmp}\~execwithresult.txt'); - { Exec via cmd and redirect output to file. Must use special string-behavior to work. } - Command := Format('"%s" /S /C ""%s" %s > "%s""', [ExpandConstant('{cmd}'), Filename, Params, TempFilename]); - Result := Exec(ExpandConstant('{cmd}'), Command, WorkingDir, ShowCmd, Wait, ResultCode); - if Result then - begin - LoadStringFromFile(TempFilename, ResultString); - end; - DeleteFile(TempFilename); -end; - -procedure UnmountRepos(); -var - ResultCode: integer; -begin - Exec('scalar.exe', 'service --unmount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; - -procedure MountRepos(); -var - StatusText: string; - MountOutput: ansiString; - ResultCode: integer; - MsgBoxText: string; -begin - StatusText := WizardForm.StatusLabel.Caption; - WizardForm.StatusLabel.Caption := 'Mounting Repos.'; - WizardForm.ProgressGauge.Style := npbstMarquee; - - ExecWithResult(ExpandConstant('{app}') + '\scalar.exe', 'service --mount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, MountOutput); - WizardForm.StatusLabel.Caption := StatusText; - WizardForm.ProgressGauge.Style := npbstNormal; - - if (ResultCode <> 0) then - begin - MsgBoxText := 'Mounting one or more repos failed:' + #13#10 + MountOutput; - SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OK, IDOK); - ExitCode := 17; - end; -end; - -function ConfirmUnmountAll(): Boolean; -var - MsgBoxResult: integer; - Repos: ansiString; - ResultCode: integer; - MsgBoxText: string; -begin - Result := False; - if ExecWithResult('scalar.exe', 'service --list-mounted', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Repos) then - begin - if Repos = '' then - begin - Result := False; - end - else - begin - if ResultCode = 0 then - begin - MsgBoxText := 'The following repos are currently mounted:' + #13#10 + Repos + #13#10 + 'Setup needs to unmount all repos before it can proceed, and those repos will be unavailable while setup is running. Do you want to continue?'; - MsgBoxResult := SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OKCANCEL, IDOK); - if (MsgBoxResult = IDOK) then - begin - Result := True; - end - else - begin - Abort(); - end; - end; - end; - end; -end; - -function EnsureGvfsNotRunning(): Boolean; -var - MsgBoxResult: integer; -begin - MsgBoxResult := IDRETRY; - while (IsScalarRunning()) Do - begin - if(MsgBoxResult = IDRETRY) then - begin - MsgBoxResult := SuppressibleMsgBox('Scalar is currently running. Please close all instances of Scalar before continuing the installation.', mbError, MB_RETRYCANCEL, IDCANCEL); - end; - if(MsgBoxResult = IDCANCEL) then - begin - Result := False; - Abort(); - end; - end; - - Result := True; -end; - -type - UpgradeRing = (urUnconfigured, urNone, urFast, urSlow); - -function GetConfiguredUpgradeRing(): UpgradeRing; -var - ResultCode: integer; - ResultString: ansiString; -begin - Result := urUnconfigured; - if ExecWithResult('scalar.exe', 'config upgrade.ring', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin - if ResultCode = 0 then begin - ResultString := AnsiLowercase(Trim(ResultString)); - Log('GetConfiguredUpgradeRing: upgrade.ring is ' + ResultString); - if CompareText(ResultString, 'none') = 0 then begin - Result := urNone; - end else if CompareText(ResultString, 'fast') = 0 then begin - Result := urFast; - end else if CompareText(ResultString, 'slow') = 0 then begin - Result := urSlow; - end else begin - Log('GetConfiguredUpgradeRing: Unknown upgrade ring: ' + ResultString); - end; - end else begin - Log('GetConfiguredUpgradeRing: Call to scalar config upgrade.ring failed with ' + SysErrorMessage(ResultCode)); - end; - end else begin - Log('GetConfiguredUpgradeRing: Call to scalar config upgrade.ring failed with ' + SysErrorMessage(ResultCode)); - end; -end; - -function IsConfigured(ConfigKey: String): Boolean; -var - ResultCode: integer; - ResultString: ansiString; -begin - Result := False - if ExecWithResult('scalar.exe', Format('config %s', [ConfigKey]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin - ResultString := AnsiLowercase(Trim(ResultString)); - Log(Format('IsConfigured(%s): value is %s', [ConfigKey, ResultString])); - Result := Length(ResultString) > 1 - end -end; - -procedure SetIfNotConfigured(ConfigKey: String; ConfigValue: String); -var - ResultCode: integer; - ResultString: ansiString; -begin - if IsConfigured(ConfigKey) = False then begin - if ExecWithResult('scalar.exe', Format('config %s %s', [ConfigKey, ConfigValue]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin - Log(Format('SetIfNotConfigured: Set %s to %s', [ConfigKey, ConfigValue])); - end else begin - Log(Format('SetIfNotConfigured: Failed to set %s with %s', [ConfigKey, SysErrorMessage(ResultCode)])); - end; - end else begin - Log(Format('SetIfNotConfigured: %s is configured, not overwriting', [ConfigKey])); - end; -end; - -procedure SetNuGetFeedIfNecessary(); -var - ConfiguredRing: UpgradeRing; - RingName: String; - TargetFeed: String; - FeedPackageName: String; -begin - ConfiguredRing := GetConfiguredUpgradeRing(); - if ConfiguredRing = urFast then begin - RingName := 'Fast'; - end else if (ConfiguredRing = urSlow) or (ConfiguredRing = urNone) then begin - RingName := 'Slow'; - end else begin - Log('SetNuGetFeedIfNecessary: No upgrade ring configured. Not configuring NuGet feed.') - exit; - end; - - TargetFeed := Format('https://pkgs.dev.azure.com/microsoft/_packaging/Scalar-%s/nuget/v3/index.json', [RingName]); - FeedPackageName := 'Microsoft.VfsForGitEnvironment'; - - SetIfNotConfigured('upgrade.feedurl', TargetFeed); - SetIfNotConfigured('upgrade.feedpackagename', FeedPackageName); -end; - -// Below are EVENT FUNCTIONS -> The main entry points of InnoSetup into the code region -// Documentation : http://www.jrsoftware.org/ishelp/index.php?topic=scriptevents - -function InitializeUninstall(): Boolean; -begin - UnmountRepos(); - Result := EnsureGvfsNotRunning(); -end; - -// Called just after "install" phase, before "post install" -function NeedRestart(): Boolean; -begin - Result := False; -end; - -function UninstallNeedRestart(): Boolean; -begin - Result := False; -end; - -procedure CurStepChanged(CurStep: TSetupStep); -begin - case CurStep of - ssInstall: - begin - UninstallService('Scalar.Service', True); - end; - ssPostInstall: - begin - if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then - begin - MountRepos(); - end - end; - end; -end; - -function GetCustomSetupExitCode: Integer; -begin - Result := ExitCode; -end; - -procedure CurUninstallStepChanged(CurStep: TUninstallStep); -begin - case CurStep of - usUninstall: - begin - UninstallService('Scalar.Service', False); - RemovePath(ExpandConstant('{app}')); - end; - end; -end; - -function PrepareToInstall(var NeedsRestart: Boolean): String; -begin - NeedsRestart := False; - Result := ''; - SetNuGetFeedIfNecessary(); - if ConfirmUnmountAll() then - begin - if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then - begin - UnmountRepos(); - end - end; - if not EnsureGvfsNotRunning() then - begin - Abort(); - end; - StopService('Scalar.Service'); -end; +; This script requires Inno Setup Compiler 5.5.9 or later to compile +; The Inno Setup Compiler (and IDE) can be found at http://www.jrsoftware.org/isinfo.php + +; General documentation on how to use InnoSetup scripts: http://www.jrsoftware.org/ishelp/index.php + +#define VCRuntimeDir PackagesDir + "\GVFS.VCRuntime.0.2.0-build\lib\x64" +#define ScalarDir BuildOutputDir + "\Scalar.Windows\bin\" + PlatformAndConfiguration +#define ScalarCommonDir BuildOutputDir + "\Scalar.Common\bin\" + PlatformAndConfiguration + "\netstandard2.0" +#define ServiceDir BuildOutputDir + "\Scalar.Service.Windows\bin\" + PlatformAndConfiguration +#define ServiceUIDir BuildOutputDir + "\Scalar.Service.UI\bin\" + PlatformAndConfiguration +#define ScalarMountDir BuildOutputDir + "\Scalar.Mount.Windows\bin\" + PlatformAndConfiguration +#define ReadObjectDir BuildOutputDir + "\Scalar.ReadObjectHook.Windows\bin\" + PlatformAndConfiguration +#define ScalarUpgraderDir BuildOutputDir + "\Scalar.Upgrader\bin\" + PlatformAndConfiguration + "\net461" + +#define MyAppName "Scalar" +#define MyAppInstallerVersion GetFileVersion(ScalarDir + "\Scalar.exe") +#define MyAppPublisher "Microsoft Corporation" +#define MyAppPublisherURL "http://www.microsoft.com" +#define MyAppURL "https://github.com/microsoft/Scalar" +#define MyAppExeName "Scalar.exe" +#define EnvironmentKey "SYSTEM\CurrentControlSet\Control\Session Manager\Environment" +#define FileSystemKey "SYSTEM\CurrentControlSet\Control\FileSystem" + +[Setup] +AppId={{489CA581-F131-4C28-BE04-4FB178933E6D} +AppName={#MyAppName} +AppVersion={#MyAppInstallerVersion} +VersionInfoVersion={#MyAppInstallerVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppPublisherURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +AppCopyright=Copyright � Microsoft 2019 +BackColor=clWhite +BackSolid=yes +DefaultDirName={pf}\{#MyAppName} +OutputBaseFilename=SetupScalar.{#ScalarVersion} +OutputDir=Setup +Compression=lzma2 +InternalCompressLevel=ultra64 +SolidCompression=yes +MinVersion=10.0.14374 +DisableDirPage=yes +DisableReadyPage=yes +SetupIconFile="{#ScalarDir}\GitVirtualFileSystem.ico" +ArchitecturesInstallIn64BitMode=x64 +ArchitecturesAllowed=x64 +WizardImageStretch=no +WindowResizable=no +CloseApplications=yes +ChangesEnvironment=yes +RestartIfNeededByRun=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl"; + +[Types] +Name: "full"; Description: "Full installation"; Flags: iscustom; + +[Components] + +[InstallDelete] +; Delete old dependencies from VS 2015 VC redistributables +Type: files; Name: "{app}\ucrtbase.dll" + +[Files] + +; Scalar.Common Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarCommonDir}\git2.dll" + +; Scalar.Mount Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.exe" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarMountDir}\Scalar.Mount.exe.config" + +; Scalar.Upgrader Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.exe" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarUpgraderDir}\Scalar.Upgrader.exe.config" + +; Scalar.ReadObjectHook files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectDir}\Scalar.ReadObjectHook.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ReadObjectDir}\Scalar.ReadObjectHook.exe" + +; Cpp Dependencies +DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_1.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\msvcp140_2.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#VCRuntimeDir}\vcruntime140.dll" + +; Scalar PDB's +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Common.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Platform.Windows.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.pdb" + +; Scalar.Service.UI Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.exe" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.exe.config" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\Scalar.Service.UI.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceUIDir}\GitVirtualFileSystem.ico" + +; Scalar Files +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\CommandLine.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Common.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.Platform.Windows.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Newtonsoft.Json.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.exe.config" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\GitVirtualFileSystem.ico" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\Scalar.exe" + +; NuGet support DLLs +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Commands.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Common.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Configuration.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Frameworks.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Packaging.Core.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Packaging.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Protocol.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\NuGet.Versioning.dll" + +; .NET Standard Files +; See https://github.com/dotnet/standard/issues/415 for a discussion on why this are copied +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\netstandard.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.Net.Http.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.ValueTuple.dll" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ScalarDir}\System.IO.Compression.dll" + +; Scalar.Service Files and PDB's +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.pdb" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.exe.config" +DestDir: "{app}"; Flags: ignoreversion; Source:"{#ServiceDir}\Scalar.Service.exe"; AfterInstall: InstallScalarService + +[UninstallDelete] +; Deletes the entire installation directory, including files and subdirectories +Type: filesandordirs; Name: "{app}"; +Type: filesandordirs; Name: "{commonappdata}\Scalar\Scalar.Upgrade"; + +[Registry] +Root: HKLM; Subkey: "{#EnvironmentKey}"; \ + ValueType: expandsz; ValueName: "PATH"; ValueData: "{olddata};{app}"; \ + Check: NeedsAddPath(ExpandConstant('{app}')) + +Root: HKLM; Subkey: "{#FileSystemKey}"; \ + ValueType: dword; ValueName: "NtfsEnableDetailedCleanupResults"; ValueData: "1"; \ + Check: IsWindows10VersionPriorToCreatorsUpdate + +[Code] +var + ExitCode: Integer; + +function NeedsAddPath(Param: string): boolean; +var + OrigPath: string; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, + '{#EnvironmentKey}', + 'PATH', OrigPath) + then begin + Result := True; + exit; + end; + // look for the path with leading and trailing semicolon + // Pos() returns 0 if not found + Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0; +end; + +function IsWindows10VersionPriorToCreatorsUpdate(): Boolean; +var + Version: TWindowsVersion; +begin + GetWindowsVersionEx(Version); + Result := (Version.Major = 10) and (Version.Minor = 0) and (Version.Build < 15063); +end; + +procedure RemovePath(Path: string); +var + Paths: string; + PathMatchIndex: Integer; +begin + if not RegQueryStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then + begin + Log('PATH not found'); + end + else + begin + Log(Format('PATH is [%s]', [Paths])); + + PathMatchIndex := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); + if PathMatchIndex = 0 then + begin + Log(Format('Path [%s] not found in PATH', [Path])); + end + else + begin + Delete(Paths, PathMatchIndex - 1, Length(Path) + 1); + Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths])); + + if RegWriteStringValue(HKEY_LOCAL_MACHINE, '{#EnvironmentKey}', 'Path', Paths) then + begin + Log('PATH written'); + end + else + begin + Log('Error writing PATH'); + end; + end; + end; +end; + +procedure StopService(ServiceName: string); +var + ResultCode: integer; +begin + Log('StopService: stopping: ' + ServiceName); + // ErrorCode 1060 means service not installed, 1062 means service not started + if not Exec(ExpandConstant('{sys}\SC.EXE'), 'stop ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) and (ResultCode <> 1062) then + begin + RaiseException('Fatal: Could not stop service: ' + ServiceName); + end; +end; + +procedure UninstallService(ServiceName: string; ShowProgress: boolean); +var + ResultCode: integer; +begin + if Exec(ExpandConstant('{sys}\SC.EXE'), 'query ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode <> 1060) then + begin + Log('UninstallService: uninstalling service: ' + ServiceName); + if (ShowProgress) then + begin + WizardForm.StatusLabel.Caption := 'Uninstalling service: ' + ServiceName; + WizardForm.ProgressGauge.Style := npbstMarquee; + end; + + try + StopService(ServiceName); + + if not Exec(ExpandConstant('{sys}\SC.EXE'), 'delete ' + ServiceName, '', SW_HIDE, ewWaitUntilTerminated, ResultCode) or (ResultCode <> 0) then + begin + Log('UninstallService: Could not uninstall service: ' + ServiceName); + RaiseException('Fatal: Could not uninstall service: ' + ServiceName); + end; + + if (ShowProgress) then + begin + WizardForm.StatusLabel.Caption := 'Waiting for pending ' + ServiceName + ' deletion to complete. This may take a while.'; + end; + + finally + if (ShowProgress) then + begin + WizardForm.ProgressGauge.Style := npbstNormal; + end; + end; + + end; +end; + +procedure WriteOnDiskVersion16CapableFile(); +var + FilePath: string; +begin + FilePath := ExpandConstant('{app}\OnDiskVersion16CapableInstallation.dat'); + if not FileExists(FilePath) then + begin + Log('WriteOnDiskVersion16CapableFile: Writing file ' + FilePath); + SaveStringToFile(FilePath, '', False); + end +end; + +procedure InstallScalarService(); +var + ResultCode: integer; + StatusText: string; + InstallSuccessful: Boolean; +begin + InstallSuccessful := False; + + StatusText := WizardForm.StatusLabel.Caption; + WizardForm.StatusLabel.Caption := 'Installing Scalar.Service.'; + WizardForm.ProgressGauge.Style := npbstMarquee; + + try + if Exec(ExpandConstant('{sys}\SC.EXE'), ExpandConstant('create Scalar.Service binPath="{app}\Scalar.Service.exe" start=auto'), '', SW_HIDE, ewWaitUntilTerminated, ResultCode) and (ResultCode = 0) then + begin + if Exec(ExpandConstant('{sys}\SC.EXE'), 'failure Scalar.Service reset= 30 actions= restart/10/restart/5000//1', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + if Exec(ExpandConstant('{sys}\SC.EXE'), 'start Scalar.Service', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + InstallSuccessful := True; + end; + end; + end; + + WriteOnDiskVersion16CapableFile(); + finally + WizardForm.StatusLabel.Caption := StatusText; + WizardForm.ProgressGauge.Style := npbstNormal; + end; + + if InstallSuccessful = False then + begin + RaiseException('Fatal: An error occured while installing Scalar.Service.'); + end; +end; + +function DeleteFileIfItExists(FilePath: string) : Boolean; +begin + Result := False; + if FileExists(FilePath) then + begin + Log('DeleteFileIfItExists: Removing ' + FilePath); + if DeleteFile(FilePath) then + begin + if not FileExists(FilePath) then + begin + Result := True; + end + else + begin + Log('DeleteFileIfItExists: File still exists after deleting: ' + FilePath); + end; + end + else + begin + Log('DeleteFileIfItExists: Failed to delete ' + FilePath); + end; + end + else + begin + Log('DeleteFileIfItExists: File does not exist: ' + FilePath); + Result := True; + end; +end; + +function IsScalarRunning(): Boolean; +var + ResultCode: integer; +begin + if Exec('powershell.exe', '-NoProfile "Get-Process scalar,scalar.mount | foreach {exit 10}"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + if ResultCode = 10 then + begin + Result := True; + end; + if ResultCode = 1 then + begin + Result := False; + end; + end; +end; + +function ExecWithResult(Filename, Params, WorkingDir: String; ShowCmd: Integer; + Wait: TExecWait; var ResultCode: Integer; var ResultString: ansiString): Boolean; +var + TempFilename: string; + Command: string; +begin + TempFilename := ExpandConstant('{tmp}\~execwithresult.txt'); + { Exec via cmd and redirect output to file. Must use special string-behavior to work. } + Command := Format('"%s" /S /C ""%s" %s > "%s""', [ExpandConstant('{cmd}'), Filename, Params, TempFilename]); + Result := Exec(ExpandConstant('{cmd}'), Command, WorkingDir, ShowCmd, Wait, ResultCode); + if Result then + begin + LoadStringFromFile(TempFilename, ResultString); + end; + DeleteFile(TempFilename); +end; + +procedure UnmountRepos(); +var + ResultCode: integer; +begin + Exec('scalar.exe', 'service --unmount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; + +procedure MountRepos(); +var + StatusText: string; + MountOutput: ansiString; + ResultCode: integer; + MsgBoxText: string; +begin + StatusText := WizardForm.StatusLabel.Caption; + WizardForm.StatusLabel.Caption := 'Mounting Repos.'; + WizardForm.ProgressGauge.Style := npbstMarquee; + + ExecWithResult(ExpandConstant('{app}') + '\scalar.exe', 'service --mount-all', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, MountOutput); + WizardForm.StatusLabel.Caption := StatusText; + WizardForm.ProgressGauge.Style := npbstNormal; + + if (ResultCode <> 0) then + begin + MsgBoxText := 'Mounting one or more repos failed:' + #13#10 + MountOutput; + SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OK, IDOK); + ExitCode := 17; + end; +end; + +function ConfirmUnmountAll(): Boolean; +var + MsgBoxResult: integer; + Repos: ansiString; + ResultCode: integer; + MsgBoxText: string; +begin + Result := False; + if ExecWithResult('scalar.exe', 'service --list-mounted', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Repos) then + begin + if Repos = '' then + begin + Result := False; + end + else + begin + if ResultCode = 0 then + begin + MsgBoxText := 'The following repos are currently mounted:' + #13#10 + Repos + #13#10 + 'Setup needs to unmount all repos before it can proceed, and those repos will be unavailable while setup is running. Do you want to continue?'; + MsgBoxResult := SuppressibleMsgBox(MsgBoxText, mbConfirmation, MB_OKCANCEL, IDOK); + if (MsgBoxResult = IDOK) then + begin + Result := True; + end + else + begin + Abort(); + end; + end; + end; + end; +end; + +function EnsureGvfsNotRunning(): Boolean; +var + MsgBoxResult: integer; +begin + MsgBoxResult := IDRETRY; + while (IsScalarRunning()) Do + begin + if(MsgBoxResult = IDRETRY) then + begin + MsgBoxResult := SuppressibleMsgBox('Scalar is currently running. Please close all instances of Scalar before continuing the installation.', mbError, MB_RETRYCANCEL, IDCANCEL); + end; + if(MsgBoxResult = IDCANCEL) then + begin + Result := False; + Abort(); + end; + end; + + Result := True; +end; + +type + UpgradeRing = (urUnconfigured, urNone, urFast, urSlow); + +function GetConfiguredUpgradeRing(): UpgradeRing; +var + ResultCode: integer; + ResultString: ansiString; +begin + Result := urUnconfigured; + if ExecWithResult('scalar.exe', 'config upgrade.ring', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin + if ResultCode = 0 then begin + ResultString := AnsiLowercase(Trim(ResultString)); + Log('GetConfiguredUpgradeRing: upgrade.ring is ' + ResultString); + if CompareText(ResultString, 'none') = 0 then begin + Result := urNone; + end else if CompareText(ResultString, 'fast') = 0 then begin + Result := urFast; + end else if CompareText(ResultString, 'slow') = 0 then begin + Result := urSlow; + end else begin + Log('GetConfiguredUpgradeRing: Unknown upgrade ring: ' + ResultString); + end; + end else begin + Log('GetConfiguredUpgradeRing: Call to scalar config upgrade.ring failed with ' + SysErrorMessage(ResultCode)); + end; + end else begin + Log('GetConfiguredUpgradeRing: Call to scalar config upgrade.ring failed with ' + SysErrorMessage(ResultCode)); + end; +end; + +function IsConfigured(ConfigKey: String): Boolean; +var + ResultCode: integer; + ResultString: ansiString; +begin + Result := False + if ExecWithResult('scalar.exe', Format('config %s', [ConfigKey]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin + ResultString := AnsiLowercase(Trim(ResultString)); + Log(Format('IsConfigured(%s): value is %s', [ConfigKey, ResultString])); + Result := Length(ResultString) > 1 + end +end; + +procedure SetIfNotConfigured(ConfigKey: String; ConfigValue: String); +var + ResultCode: integer; + ResultString: ansiString; +begin + if IsConfigured(ConfigKey) = False then begin + if ExecWithResult('scalar.exe', Format('config %s %s', [ConfigKey, ConfigValue]), '', SW_HIDE, ewWaitUntilTerminated, ResultCode, ResultString) then begin + Log(Format('SetIfNotConfigured: Set %s to %s', [ConfigKey, ConfigValue])); + end else begin + Log(Format('SetIfNotConfigured: Failed to set %s with %s', [ConfigKey, SysErrorMessage(ResultCode)])); + end; + end else begin + Log(Format('SetIfNotConfigured: %s is configured, not overwriting', [ConfigKey])); + end; +end; + +procedure SetNuGetFeedIfNecessary(); +var + ConfiguredRing: UpgradeRing; + RingName: String; + TargetFeed: String; + FeedPackageName: String; +begin + ConfiguredRing := GetConfiguredUpgradeRing(); + if ConfiguredRing = urFast then begin + RingName := 'Fast'; + end else if (ConfiguredRing = urSlow) or (ConfiguredRing = urNone) then begin + RingName := 'Slow'; + end else begin + Log('SetNuGetFeedIfNecessary: No upgrade ring configured. Not configuring NuGet feed.') + exit; + end; + + TargetFeed := Format('https://pkgs.dev.azure.com/microsoft/_packaging/Scalar-%s/nuget/v3/index.json', [RingName]); + FeedPackageName := 'Microsoft.VfsForGitEnvironment'; + + SetIfNotConfigured('upgrade.feedurl', TargetFeed); + SetIfNotConfigured('upgrade.feedpackagename', FeedPackageName); +end; + +// Below are EVENT FUNCTIONS -> The main entry points of InnoSetup into the code region +// Documentation : http://www.jrsoftware.org/ishelp/index.php?topic=scriptevents + +function InitializeUninstall(): Boolean; +begin + UnmountRepos(); + Result := EnsureGvfsNotRunning(); +end; + +// Called just after "install" phase, before "post install" +function NeedRestart(): Boolean; +begin + Result := False; +end; + +function UninstallNeedRestart(): Boolean; +begin + Result := False; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + case CurStep of + ssInstall: + begin + UninstallService('Scalar.Service', True); + end; + ssPostInstall: + begin + if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then + begin + MountRepos(); + end + end; + end; +end; + +function GetCustomSetupExitCode: Integer; +begin + Result := ExitCode; +end; + +procedure CurUninstallStepChanged(CurStep: TUninstallStep); +begin + case CurStep of + usUninstall: + begin + UninstallService('Scalar.Service', False); + RemovePath(ExpandConstant('{app}')); + end; + end; +end; + +function PrepareToInstall(var NeedsRestart: Boolean): String; +begin + NeedsRestart := False; + Result := ''; + SetNuGetFeedIfNecessary(); + if ConfirmUnmountAll() then + begin + if ExpandConstant('{param:REMOUNTREPOS|true}') = 'true' then + begin + UnmountRepos(); + end + end; + if not EnsureGvfsNotRunning() then + begin + Abort(); + end; + StopService('Scalar.Service'); +end; diff --git a/Scalar.Installer.Windows/packages.config b/Scalar.Installer.Windows/packages.config index f0c6f5ea8a..b27bb19f10 100644 --- a/Scalar.Installer.Windows/packages.config +++ b/Scalar.Installer.Windows/packages.config @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/Scalar.Mount/InProcessMount.cs b/Scalar.Mount/InProcessMount.cs index 612b7d7ed0..0bf44dab22 100644 --- a/Scalar.Mount/InProcessMount.cs +++ b/Scalar.Mount/InProcessMount.cs @@ -1,408 +1,408 @@ -using Newtonsoft.Json; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Maintenance; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading; - -namespace Scalar.Mount -{ - public class InProcessMount - { - private readonly bool showDebugWindow; - - private ScalarEnlistment enlistment; - private ITracer tracer; - private GitMaintenanceScheduler maintenanceScheduler; - - private CacheServerInfo cacheServer; - private RetryConfig retryConfig; - - private ScalarContext context; - private ScalarGitObjects gitObjects; - - private MountState currentState; - private ManualResetEvent unmountEvent; - - public InProcessMount(ITracer tracer, ScalarEnlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig, bool showDebugWindow) - { - this.tracer = tracer; - this.retryConfig = retryConfig; - this.cacheServer = cacheServer; - this.enlistment = enlistment; - this.showDebugWindow = showDebugWindow; - this.unmountEvent = new ManualResetEvent(false); - } - - private enum MountState - { - Invalid = 0, - - Mounting, - Ready, - Unmounting, - MountFailed - } - - public void Mount(EventLevel verbosity, Keywords keywords) - { - this.currentState = MountState.Mounting; - - // We must initialize repo metadata before starting the pipe server so it - // can immediately handle status requests - string error; - if (!RepoMetadata.TryInitialize(this.tracer, this.enlistment.DotScalarRoot, out error)) - { - this.FailMountAndExit("Failed to load repo metadata: " + error); - } - - string gitObjectsRoot; - if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) - { - this.FailMountAndExit("Failed to determine git objects root from repo metadata: " + error); - } - - string localCacheRoot; - if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) - { - this.FailMountAndExit("Failed to determine local cache path from repo metadata: " + error); - } - - string blobSizesRoot; - if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error)) - { - this.FailMountAndExit("Failed to determine blob sizes root from repo metadata: " + error); - } - - this.tracer.RelatedEvent( - EventLevel.Informational, - "CachePathsLoaded", - new EventMetadata - { - { "gitObjectsRoot", gitObjectsRoot }, - { "localCacheRoot", localCacheRoot }, - { "blobSizesRoot", blobSizesRoot }, - }); - - this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); - - using (NamedPipeServer pipeServer = this.StartNamedPipe()) - { - this.tracer.RelatedEvent( - EventLevel.Informational, - $"{nameof(this.Mount)}_StartedNamedPipe", - new EventMetadata { { "NamedPipeName", this.enlistment.NamedPipeName } }); - - this.context = this.CreateContext(); - - if (this.context.Unattended) - { - this.tracer.RelatedEvent(EventLevel.Critical, ScalarConstants.UnattendedEnvironmentVariable, null); - } - - this.ValidateMountPoints(); - - string errorMessage; - if (!HooksInstaller.TryUpdateHooks(this.context, out errorMessage)) - { - this.FailMountAndExit(errorMessage); - } - - ScalarPlatform.Instance.ConfigureVisualStudio(this.enlistment.GitBinPath, this.tracer); - - this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer); - - Console.Title = "Scalar " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.EnlistmentRoot; - - this.tracer.RelatedEvent( - EventLevel.Informational, - "Mount", - new EventMetadata - { - // Use TracingConstants.MessageKey.InfoMessage rather than TracingConstants.MessageKey.CriticalMessage - // as this message should not appear as an error - { TracingConstants.MessageKey.InfoMessage, "Virtual repo is ready" }, - }, - Keywords.Telemetry); - - this.currentState = MountState.Ready; - - this.unmountEvent.WaitOne(); - } - } - - private ScalarContext CreateContext() - { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo gitRepo = this.CreateOrReportAndExit( - () => new GitRepo( - this.tracer, - this.enlistment, - fileSystem), - "Failed to read git repo"); - return new ScalarContext(this.tracer, fileSystem, gitRepo, this.enlistment); - } - - private void ValidateMountPoints() - { - DirectoryInfo workingDirectoryRootInfo = new DirectoryInfo(this.enlistment.WorkingDirectoryBackingRoot); - if (!workingDirectoryRootInfo.Exists) - { - this.FailMountAndExit("Failed to initialize file system callbacks. Directory \"{0}\" must exist.", this.enlistment.WorkingDirectoryBackingRoot); - } - - string dotGitPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Root); - DirectoryInfo dotGitPathInfo = new DirectoryInfo(dotGitPath); - if (!dotGitPathInfo.Exists) - { - this.FailMountAndExit("Failed to mount. Directory \"{0}\" must exist.", dotGitPathInfo); - } - } - - private NamedPipeServer StartNamedPipe() - { - try - { - return NamedPipeServer.StartNewServer(this.enlistment.NamedPipeName, this.tracer, this.HandleRequest); - } - catch (PipeNameLengthException) - { - this.FailMountAndExit("Failed to create mount point. Mount path exceeds the maximum number of allowed characters"); - return null; - } - } - - private void FailMountAndExit(string error, params object[] args) - { - this.currentState = MountState.MountFailed; - - this.tracer.RelatedError(error, args); - if (this.showDebugWindow) - { - Console.WriteLine("\nPress Enter to Exit"); - Console.ReadLine(); - } - - Environment.Exit((int)ReturnCode.GenericError); - } - - private T CreateOrReportAndExit(Func factory, string reportMessage) - { - try - { - return factory(); - } - catch (Exception e) - { - this.FailMountAndExit(reportMessage + " " + e.ToString()); - throw; - } - } - - private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) - { - NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); - - switch (message.Header) - { - case NamedPipeMessages.GetStatus.Request: - this.HandleGetStatusRequest(connection); - break; - - case NamedPipeMessages.Unmount.Request: - this.HandleUnmountRequest(connection); - break; - - case NamedPipeMessages.DownloadObject.DownloadRequest: - this.HandleDownloadObjectRequest(message, connection); - break; - - case NamedPipeMessages.RunPostFetchJob.PostFetchJob: - this.HandlePostFetchJobRequest(message, connection); - break; - - default: - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", "Mount"); - metadata.Add("Header", message.Header); - this.tracer.RelatedError(metadata, "HandleRequest: Unknown request"); - - connection.TrySendResponse(NamedPipeMessages.UnknownRequest); - break; - } - } - - private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) - { - NamedPipeMessages.DownloadObject.Response response; - - NamedPipeMessages.DownloadObject.Request request = new NamedPipeMessages.DownloadObject.Request(message); - string objectSha = request.RequestSha; - if (this.currentState != MountState.Ready) - { - response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.MountNotReadyResult); - } - else - { - if (!SHA1Util.IsValidShaFormat(objectSha)) - { - response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.InvalidSHAResult); - } - else - { - Stopwatch downloadTime = Stopwatch.StartNew(); - if (this.gitObjects.TryDownloadAndSaveObject(objectSha, ScalarGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success) - { - response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); - } - else - { - response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed); - } - - bool isBlob; - this.context.Repository.TryGetIsBlob(objectSha, out isBlob); - this.context.Repository.ScalarLock.Stats.RecordObjectDownload(isBlob, downloadTime.ElapsedMilliseconds); - } - } - - connection.TrySendResponse(response.CreateMessage()); - } - - private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) - { - NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(message); - - this.tracer.RelatedInfo("Received post-fetch job request with body {0}", message.Body); - - NamedPipeMessages.RunPostFetchJob.Response response; - if (this.currentState == MountState.Ready) - { - List packIndexes = JsonConvert.DeserializeObject>(message.Body); - this.maintenanceScheduler.EnqueueOneTimeStep(new PostFetchStep(this.context, packIndexes)); - - response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.QueuedResult); - } - else - { - response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.MountNotReadyResult); - } - - connection.TrySendResponse(response.CreateMessage()); - } - - private void HandleGetStatusRequest(NamedPipeServer.Connection connection) - { - NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response(); - response.EnlistmentRoot = this.enlistment.EnlistmentRoot; - response.LocalCacheRoot = !string.IsNullOrWhiteSpace(this.enlistment.LocalCacheRoot) ? this.enlistment.LocalCacheRoot : this.enlistment.GitObjectsRoot; - response.RepoUrl = this.enlistment.RepoUrl; - response.CacheServer = this.cacheServer.ToString(); - response.DiskLayoutVersion = $"{ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion}.{ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion}"; - - switch (this.currentState) - { - case MountState.Mounting: - response.MountStatus = NamedPipeMessages.GetStatus.Mounting; - break; - - case MountState.Ready: - response.MountStatus = NamedPipeMessages.GetStatus.Ready; - break; - - case MountState.Unmounting: - response.MountStatus = NamedPipeMessages.GetStatus.Unmounting; - break; - - case MountState.MountFailed: - response.MountStatus = NamedPipeMessages.GetStatus.MountFailed; - break; - - default: - response.MountStatus = NamedPipeMessages.UnknownScalarState; - break; - } - - connection.TrySendResponse(response.ToJson()); - } - - private void HandleUnmountRequest(NamedPipeServer.Connection connection) - { - switch (this.currentState) - { - case MountState.Mounting: - connection.TrySendResponse(NamedPipeMessages.Unmount.NotMounted); - break; - - // Even if the previous mount failed, attempt to unmount anyway. Otherwise the user has no - // recourse but to kill the process. - case MountState.MountFailed: - goto case MountState.Ready; - - case MountState.Ready: - this.currentState = MountState.Unmounting; - - connection.TrySendResponse(NamedPipeMessages.Unmount.Acknowledged); - this.UnmountAndStopWorkingDirectoryCallbacks(); - connection.TrySendResponse(NamedPipeMessages.Unmount.Completed); - - this.unmountEvent.Set(); - Environment.Exit((int)ReturnCode.Success); - break; - - case MountState.Unmounting: - connection.TrySendResponse(NamedPipeMessages.Unmount.AlreadyUnmounting); - break; - - default: - connection.TrySendResponse(NamedPipeMessages.UnknownScalarState); - break; - } - } - - private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache) - { - string error; - if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error)) - { - this.FailMountAndExit("Failed to obtain git credentials: " + error); - } - - GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig); - this.gitObjects = new ScalarGitObjects(this.context, objectRequestor); - - this.maintenanceScheduler = this.CreateOrReportAndExit(() => new GitMaintenanceScheduler(this.context, this.gitObjects), "Failed to start maintenance scheduler"); - - int majorVersion; - int minorVersion; - if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) - { - this.FailMountAndExit("Error: {0}", error); - } - - if (majorVersion != ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) - { - this.FailMountAndExit( - "Error: On disk version ({0}) does not match current version ({1})", - majorVersion, - ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion); - } - } - - private void UnmountAndStopWorkingDirectoryCallbacks() - { - if (this.maintenanceScheduler != null) - { - this.maintenanceScheduler.Dispose(); - this.maintenanceScheduler = null; - } - } - } +using Newtonsoft.Json; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Maintenance; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Scalar.Mount +{ + public class InProcessMount + { + private readonly bool showDebugWindow; + + private ScalarEnlistment enlistment; + private ITracer tracer; + private GitMaintenanceScheduler maintenanceScheduler; + + private CacheServerInfo cacheServer; + private RetryConfig retryConfig; + + private ScalarContext context; + private ScalarGitObjects gitObjects; + + private MountState currentState; + private ManualResetEvent unmountEvent; + + public InProcessMount(ITracer tracer, ScalarEnlistment enlistment, CacheServerInfo cacheServer, RetryConfig retryConfig, bool showDebugWindow) + { + this.tracer = tracer; + this.retryConfig = retryConfig; + this.cacheServer = cacheServer; + this.enlistment = enlistment; + this.showDebugWindow = showDebugWindow; + this.unmountEvent = new ManualResetEvent(false); + } + + private enum MountState + { + Invalid = 0, + + Mounting, + Ready, + Unmounting, + MountFailed + } + + public void Mount(EventLevel verbosity, Keywords keywords) + { + this.currentState = MountState.Mounting; + + // We must initialize repo metadata before starting the pipe server so it + // can immediately handle status requests + string error; + if (!RepoMetadata.TryInitialize(this.tracer, this.enlistment.DotScalarRoot, out error)) + { + this.FailMountAndExit("Failed to load repo metadata: " + error); + } + + string gitObjectsRoot; + if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) + { + this.FailMountAndExit("Failed to determine git objects root from repo metadata: " + error); + } + + string localCacheRoot; + if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) + { + this.FailMountAndExit("Failed to determine local cache path from repo metadata: " + error); + } + + string blobSizesRoot; + if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error)) + { + this.FailMountAndExit("Failed to determine blob sizes root from repo metadata: " + error); + } + + this.tracer.RelatedEvent( + EventLevel.Informational, + "CachePathsLoaded", + new EventMetadata + { + { "gitObjectsRoot", gitObjectsRoot }, + { "localCacheRoot", localCacheRoot }, + { "blobSizesRoot", blobSizesRoot }, + }); + + this.enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); + + using (NamedPipeServer pipeServer = this.StartNamedPipe()) + { + this.tracer.RelatedEvent( + EventLevel.Informational, + $"{nameof(this.Mount)}_StartedNamedPipe", + new EventMetadata { { "NamedPipeName", this.enlistment.NamedPipeName } }); + + this.context = this.CreateContext(); + + if (this.context.Unattended) + { + this.tracer.RelatedEvent(EventLevel.Critical, ScalarConstants.UnattendedEnvironmentVariable, null); + } + + this.ValidateMountPoints(); + + string errorMessage; + if (!HooksInstaller.TryUpdateHooks(this.context, out errorMessage)) + { + this.FailMountAndExit(errorMessage); + } + + ScalarPlatform.Instance.ConfigureVisualStudio(this.enlistment.GitBinPath, this.tracer); + + this.MountAndStartWorkingDirectoryCallbacks(this.cacheServer); + + Console.Title = "Scalar " + ProcessHelper.GetCurrentProcessVersion() + " - " + this.enlistment.EnlistmentRoot; + + this.tracer.RelatedEvent( + EventLevel.Informational, + "Mount", + new EventMetadata + { + // Use TracingConstants.MessageKey.InfoMessage rather than TracingConstants.MessageKey.CriticalMessage + // as this message should not appear as an error + { TracingConstants.MessageKey.InfoMessage, "Virtual repo is ready" }, + }, + Keywords.Telemetry); + + this.currentState = MountState.Ready; + + this.unmountEvent.WaitOne(); + } + } + + private ScalarContext CreateContext() + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + GitRepo gitRepo = this.CreateOrReportAndExit( + () => new GitRepo( + this.tracer, + this.enlistment, + fileSystem), + "Failed to read git repo"); + return new ScalarContext(this.tracer, fileSystem, gitRepo, this.enlistment); + } + + private void ValidateMountPoints() + { + DirectoryInfo workingDirectoryRootInfo = new DirectoryInfo(this.enlistment.WorkingDirectoryBackingRoot); + if (!workingDirectoryRootInfo.Exists) + { + this.FailMountAndExit("Failed to initialize file system callbacks. Directory \"{0}\" must exist.", this.enlistment.WorkingDirectoryBackingRoot); + } + + string dotGitPath = Path.Combine(this.enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Root); + DirectoryInfo dotGitPathInfo = new DirectoryInfo(dotGitPath); + if (!dotGitPathInfo.Exists) + { + this.FailMountAndExit("Failed to mount. Directory \"{0}\" must exist.", dotGitPathInfo); + } + } + + private NamedPipeServer StartNamedPipe() + { + try + { + return NamedPipeServer.StartNewServer(this.enlistment.NamedPipeName, this.tracer, this.HandleRequest); + } + catch (PipeNameLengthException) + { + this.FailMountAndExit("Failed to create mount point. Mount path exceeds the maximum number of allowed characters"); + return null; + } + } + + private void FailMountAndExit(string error, params object[] args) + { + this.currentState = MountState.MountFailed; + + this.tracer.RelatedError(error, args); + if (this.showDebugWindow) + { + Console.WriteLine("\nPress Enter to Exit"); + Console.ReadLine(); + } + + Environment.Exit((int)ReturnCode.GenericError); + } + + private T CreateOrReportAndExit(Func factory, string reportMessage) + { + try + { + return factory(); + } + catch (Exception e) + { + this.FailMountAndExit(reportMessage + " " + e.ToString()); + throw; + } + } + + private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) + { + NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); + + switch (message.Header) + { + case NamedPipeMessages.GetStatus.Request: + this.HandleGetStatusRequest(connection); + break; + + case NamedPipeMessages.Unmount.Request: + this.HandleUnmountRequest(connection); + break; + + case NamedPipeMessages.DownloadObject.DownloadRequest: + this.HandleDownloadObjectRequest(message, connection); + break; + + case NamedPipeMessages.RunPostFetchJob.PostFetchJob: + this.HandlePostFetchJobRequest(message, connection); + break; + + default: + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", "Mount"); + metadata.Add("Header", message.Header); + this.tracer.RelatedError(metadata, "HandleRequest: Unknown request"); + + connection.TrySendResponse(NamedPipeMessages.UnknownRequest); + break; + } + } + + private void HandleDownloadObjectRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) + { + NamedPipeMessages.DownloadObject.Response response; + + NamedPipeMessages.DownloadObject.Request request = new NamedPipeMessages.DownloadObject.Request(message); + string objectSha = request.RequestSha; + if (this.currentState != MountState.Ready) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.MountNotReadyResult); + } + else + { + if (!SHA1Util.IsValidShaFormat(objectSha)) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.InvalidSHAResult); + } + else + { + Stopwatch downloadTime = Stopwatch.StartNew(); + if (this.gitObjects.TryDownloadAndSaveObject(objectSha, ScalarGitObjects.RequestSource.NamedPipeMessage) == GitObjects.DownloadAndSaveObjectResult.Success) + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.SuccessResult); + } + else + { + response = new NamedPipeMessages.DownloadObject.Response(NamedPipeMessages.DownloadObject.DownloadFailed); + } + + bool isBlob; + this.context.Repository.TryGetIsBlob(objectSha, out isBlob); + this.context.Repository.ScalarLock.Stats.RecordObjectDownload(isBlob, downloadTime.ElapsedMilliseconds); + } + } + + connection.TrySendResponse(response.CreateMessage()); + } + + private void HandlePostFetchJobRequest(NamedPipeMessages.Message message, NamedPipeServer.Connection connection) + { + NamedPipeMessages.RunPostFetchJob.Request request = new NamedPipeMessages.RunPostFetchJob.Request(message); + + this.tracer.RelatedInfo("Received post-fetch job request with body {0}", message.Body); + + NamedPipeMessages.RunPostFetchJob.Response response; + if (this.currentState == MountState.Ready) + { + List packIndexes = JsonConvert.DeserializeObject>(message.Body); + this.maintenanceScheduler.EnqueueOneTimeStep(new PostFetchStep(this.context, packIndexes)); + + response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.QueuedResult); + } + else + { + response = new NamedPipeMessages.RunPostFetchJob.Response(NamedPipeMessages.RunPostFetchJob.MountNotReadyResult); + } + + connection.TrySendResponse(response.CreateMessage()); + } + + private void HandleGetStatusRequest(NamedPipeServer.Connection connection) + { + NamedPipeMessages.GetStatus.Response response = new NamedPipeMessages.GetStatus.Response(); + response.EnlistmentRoot = this.enlistment.EnlistmentRoot; + response.LocalCacheRoot = !string.IsNullOrWhiteSpace(this.enlistment.LocalCacheRoot) ? this.enlistment.LocalCacheRoot : this.enlistment.GitObjectsRoot; + response.RepoUrl = this.enlistment.RepoUrl; + response.CacheServer = this.cacheServer.ToString(); + response.DiskLayoutVersion = $"{ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion}.{ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMinorVersion}"; + + switch (this.currentState) + { + case MountState.Mounting: + response.MountStatus = NamedPipeMessages.GetStatus.Mounting; + break; + + case MountState.Ready: + response.MountStatus = NamedPipeMessages.GetStatus.Ready; + break; + + case MountState.Unmounting: + response.MountStatus = NamedPipeMessages.GetStatus.Unmounting; + break; + + case MountState.MountFailed: + response.MountStatus = NamedPipeMessages.GetStatus.MountFailed; + break; + + default: + response.MountStatus = NamedPipeMessages.UnknownScalarState; + break; + } + + connection.TrySendResponse(response.ToJson()); + } + + private void HandleUnmountRequest(NamedPipeServer.Connection connection) + { + switch (this.currentState) + { + case MountState.Mounting: + connection.TrySendResponse(NamedPipeMessages.Unmount.NotMounted); + break; + + // Even if the previous mount failed, attempt to unmount anyway. Otherwise the user has no + // recourse but to kill the process. + case MountState.MountFailed: + goto case MountState.Ready; + + case MountState.Ready: + this.currentState = MountState.Unmounting; + + connection.TrySendResponse(NamedPipeMessages.Unmount.Acknowledged); + this.UnmountAndStopWorkingDirectoryCallbacks(); + connection.TrySendResponse(NamedPipeMessages.Unmount.Completed); + + this.unmountEvent.Set(); + Environment.Exit((int)ReturnCode.Success); + break; + + case MountState.Unmounting: + connection.TrySendResponse(NamedPipeMessages.Unmount.AlreadyUnmounting); + break; + + default: + connection.TrySendResponse(NamedPipeMessages.UnknownScalarState); + break; + } + } + + private void MountAndStartWorkingDirectoryCallbacks(CacheServerInfo cache) + { + string error; + if (!this.context.Enlistment.Authentication.TryInitialize(this.context.Tracer, this.context.Enlistment, out error)) + { + this.FailMountAndExit("Failed to obtain git credentials: " + error); + } + + GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(this.context.Tracer, this.context.Enlistment, cache, this.retryConfig); + this.gitObjects = new ScalarGitObjects(this.context, objectRequestor); + + this.maintenanceScheduler = this.CreateOrReportAndExit(() => new GitMaintenanceScheduler(this.context, this.gitObjects), "Failed to start maintenance scheduler"); + + int majorVersion; + int minorVersion; + if (!RepoMetadata.Instance.TryGetOnDiskLayoutVersion(out majorVersion, out minorVersion, out error)) + { + this.FailMountAndExit("Error: {0}", error); + } + + if (majorVersion != ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion) + { + this.FailMountAndExit( + "Error: On disk version ({0}) does not match current version ({1})", + majorVersion, + ScalarPlatform.Instance.DiskLayoutUpgrade.Version.CurrentMajorVersion); + } + } + + private void UnmountAndStopWorkingDirectoryCallbacks() + { + if (this.maintenanceScheduler != null) + { + this.maintenanceScheduler.Dispose(); + this.maintenanceScheduler = null; + } + } + } } diff --git a/Scalar.Mount/InProcessMountVerb.cs b/Scalar.Mount/InProcessMountVerb.cs index fd8d237686..e26151ed54 100644 --- a/Scalar.Mount/InProcessMountVerb.cs +++ b/Scalar.Mount/InProcessMountVerb.cs @@ -1,60 +1,60 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; +using CommandLine; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; using System.ComponentModel; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.Mount -{ - [Verb("mount", HelpText = "Starts the background mount process")] - public class InProcessMountVerb - { - private TextWriter output; - - public InProcessMountVerb() - { - this.output = Console.Out; - this.ReturnCode = ReturnCode.Success; - - this.InitializeDefaultParameterValues(); - } - - public ReturnCode ReturnCode { get; private set; } - - [Option( - 'v', - ScalarConstants.VerbParameters.Mount.Verbosity, - Default = ScalarConstants.VerbParameters.Mount.DefaultVerbosity, - Required = false, - HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] - public string Verbosity { get; set; } - - [Option( - 'k', - ScalarConstants.VerbParameters.Mount.Keywords, - Default = ScalarConstants.VerbParameters.Mount.DefaultKeywords, - Required = false, - HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] - public string KeywordsCsv { get; set; } - - [Option( - 'd', - ScalarConstants.VerbParameters.Mount.DebugWindow, - Default = false, - Required = false, - HelpText = "Show the debug window. By default, all output is written to a log file and no debug window is shown.")] - public bool ShowDebugWindow { get; set; } - - [Option( - 's', - ScalarConstants.VerbParameters.Mount.StartedByService, - Default = "false", - Required = false, - HelpText = "Service initiated mount.")] +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.Mount +{ + [Verb("mount", HelpText = "Starts the background mount process")] + public class InProcessMountVerb + { + private TextWriter output; + + public InProcessMountVerb() + { + this.output = Console.Out; + this.ReturnCode = ReturnCode.Success; + + this.InitializeDefaultParameterValues(); + } + + public ReturnCode ReturnCode { get; private set; } + + [Option( + 'v', + ScalarConstants.VerbParameters.Mount.Verbosity, + Default = ScalarConstants.VerbParameters.Mount.DefaultVerbosity, + Required = false, + HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] + public string Verbosity { get; set; } + + [Option( + 'k', + ScalarConstants.VerbParameters.Mount.Keywords, + Default = ScalarConstants.VerbParameters.Mount.DefaultKeywords, + Required = false, + HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] + public string KeywordsCsv { get; set; } + + [Option( + 'd', + ScalarConstants.VerbParameters.Mount.DebugWindow, + Default = false, + Required = false, + HelpText = "Show the debug window. By default, all output is written to a log file and no debug window is shown.")] + public bool ShowDebugWindow { get; set; } + + [Option( + 's', + ScalarConstants.VerbParameters.Mount.StartedByService, + Default = "false", + Required = false, + HelpText = "Service initiated mount.")] public string StartedByService { get; set; } [Option( @@ -62,165 +62,165 @@ public InProcessMountVerb() ScalarConstants.VerbParameters.Mount.StartedByVerb, Default = false, Required = false, - HelpText = "Verb initiated mount.")] - public bool StartedByVerb { get; set; } - - [Value( - 0, - Required = true, - MetaName = "Enlistment Root Path", - HelpText = "Full or relative path to the Scalar enlistment root")] - public string EnlistmentRootPathParameter { get; set; } - - public void InitializeDefaultParameterValues() - { - this.Verbosity = ScalarConstants.VerbParameters.Mount.DefaultVerbosity; - this.KeywordsCsv = ScalarConstants.VerbParameters.Mount.DefaultKeywords; - } - - public void Execute() - { + HelpText = "Verb initiated mount.")] + public bool StartedByVerb { get; set; } + + [Value( + 0, + Required = true, + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the Scalar enlistment root")] + public string EnlistmentRootPathParameter { get; set; } + + public void InitializeDefaultParameterValues() + { + this.Verbosity = ScalarConstants.VerbParameters.Mount.DefaultVerbosity; + this.KeywordsCsv = ScalarConstants.VerbParameters.Mount.DefaultKeywords; + } + + public void Execute() + { if (this.StartedByVerb) - { - // If this process was started by a verb it means that StartBackgroundScalarProcess was used - // and we should be running in the background. PrepareProcessToRunInBackground will perform - // any platform specific preparation required to run as a background process. + { + // If this process was started by a verb it means that StartBackgroundScalarProcess was used + // and we should be running in the background. PrepareProcessToRunInBackground will perform + // any platform specific preparation required to run as a background process. ScalarPlatform.Instance.PrepareProcessToRunInBackground(); - } - - ScalarEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter); - - EventLevel verbosity; - Keywords keywords; - this.ParseEnumArgs(out verbosity, out keywords); - - JsonTracer tracer = this.CreateTracer(enlistment, verbosity, keywords); - - CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); - - tracer.WriteStartEvent( - enlistment.EnlistmentRoot, - enlistment.RepoUrl, - cacheServer.Url, - new EventMetadata - { - { "IsElevated", ScalarPlatform.Instance.IsElevated() }, - { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, - { nameof(this.StartedByService), this.StartedByService }, - { nameof(this.StartedByVerb), this.StartedByVerb }, - }); - - AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => - { - this.UnhandledScalarExceptionHandler(tracer, sender, e); - }; - - string error; - RetryConfig retryConfig; - if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); - } - - InProcessMount mountHelper = new InProcessMount(tracer, enlistment, cacheServer, retryConfig, this.ShowDebugWindow); - - try - { - mountHelper.Mount(verbosity, keywords); - } - catch (Exception ex) - { - this.ReportErrorAndExit(tracer, "Failed to mount: {0}", ex.Message); - } - } - - private void UnhandledScalarExceptionHandler(ITracer tracer, object sender, UnhandledExceptionEventArgs e) - { - Exception exception = e.ExceptionObject as Exception; - - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", exception.ToString()); - metadata.Add("IsTerminating", e.IsTerminating); - tracer.RelatedError(metadata, "UnhandledScalarExceptionHandler caught unhandled exception"); - } - - private JsonTracer CreateTracer(ScalarEnlistment enlistment, EventLevel verbosity, Keywords keywords) - { - JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "ScalarMount", enlistment.GetEnlistmentId(), enlistment.GetMountId()); - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.MountProcess), - verbosity, - keywords); - if (this.ShowDebugWindow) - { - tracer.AddDiagnosticConsoleEventListener(verbosity, keywords); - } - - return tracer; - } - - private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) - { - if (!Enum.TryParse(this.KeywordsCsv, out keywords)) - { - this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); - } - - if (!Enum.TryParse(this.Verbosity, out verbosity)) - { - this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); - } - } - - private ScalarEnlistment CreateEnlistment(string enlistmentRootPath) - { - string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (string.IsNullOrWhiteSpace(gitBinPath)) - { - this.ReportErrorAndExit("Error: " + ScalarConstants.GitIsNotInstalledError); - } - - ScalarEnlistment enlistment = null; - try - { - enlistment = ScalarEnlistment.CreateFromDirectory(enlistmentRootPath, gitBinPath, authentication: null); - } - catch (InvalidRepoException e) - { - this.ReportErrorAndExit( - "Error: '{0}' is not a valid Scalar enlistment. {1}", - enlistmentRootPath, - e.Message); - } - - return enlistment; - } - - private void ReportErrorAndExit(string error, params object[] args) - { - this.ReportErrorAndExit(null, error, args); - } - - private void ReportErrorAndExit(ITracer tracer, string error, params object[] args) - { - if (tracer != null) - { - tracer.RelatedError(error, args); - } - - if (error != null) - { - this.output.WriteLine(error, args); - } - - if (this.ShowDebugWindow) - { - Console.WriteLine("\nPress Enter to Exit"); - Console.ReadLine(); - } - - this.ReturnCode = ReturnCode.GenericError; - throw new MountAbortedException(this); - } - } -} + } + + ScalarEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter); + + EventLevel verbosity; + Keywords keywords; + this.ParseEnumArgs(out verbosity, out keywords); + + JsonTracer tracer = this.CreateTracer(enlistment, verbosity, keywords); + + CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + cacheServer.Url, + new EventMetadata + { + { "IsElevated", ScalarPlatform.Instance.IsElevated() }, + { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, + { nameof(this.StartedByService), this.StartedByService }, + { nameof(this.StartedByVerb), this.StartedByVerb }, + }); + + AppDomain.CurrentDomain.UnhandledException += (object sender, UnhandledExceptionEventArgs e) => + { + this.UnhandledScalarExceptionHandler(tracer, sender, e); + }; + + string error; + RetryConfig retryConfig; + if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); + } + + InProcessMount mountHelper = new InProcessMount(tracer, enlistment, cacheServer, retryConfig, this.ShowDebugWindow); + + try + { + mountHelper.Mount(verbosity, keywords); + } + catch (Exception ex) + { + this.ReportErrorAndExit(tracer, "Failed to mount: {0}", ex.Message); + } + } + + private void UnhandledScalarExceptionHandler(ITracer tracer, object sender, UnhandledExceptionEventArgs e) + { + Exception exception = e.ExceptionObject as Exception; + + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", exception.ToString()); + metadata.Add("IsTerminating", e.IsTerminating); + tracer.RelatedError(metadata, "UnhandledScalarExceptionHandler caught unhandled exception"); + } + + private JsonTracer CreateTracer(ScalarEnlistment enlistment, EventLevel verbosity, Keywords keywords) + { + JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "ScalarMount", enlistment.GetEnlistmentId(), enlistment.GetMountId()); + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.MountProcess), + verbosity, + keywords); + if (this.ShowDebugWindow) + { + tracer.AddDiagnosticConsoleEventListener(verbosity, keywords); + } + + return tracer; + } + + private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + { + if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + { + this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); + } + + if (!Enum.TryParse(this.Verbosity, out verbosity)) + { + this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); + } + } + + private ScalarEnlistment CreateEnlistment(string enlistmentRootPath) + { + string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + this.ReportErrorAndExit("Error: " + ScalarConstants.GitIsNotInstalledError); + } + + ScalarEnlistment enlistment = null; + try + { + enlistment = ScalarEnlistment.CreateFromDirectory(enlistmentRootPath, gitBinPath, authentication: null); + } + catch (InvalidRepoException e) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid Scalar enlistment. {1}", + enlistmentRootPath, + e.Message); + } + + return enlistment; + } + + private void ReportErrorAndExit(string error, params object[] args) + { + this.ReportErrorAndExit(null, error, args); + } + + private void ReportErrorAndExit(ITracer tracer, string error, params object[] args) + { + if (tracer != null) + { + tracer.RelatedError(error, args); + } + + if (error != null) + { + this.output.WriteLine(error, args); + } + + if (this.ShowDebugWindow) + { + Console.WriteLine("\nPress Enter to Exit"); + Console.ReadLine(); + } + + this.ReturnCode = ReturnCode.GenericError; + throw new MountAbortedException(this); + } + } +} diff --git a/Scalar.Mount/MountAbortedException.cs b/Scalar.Mount/MountAbortedException.cs index 514347b4e6..5a7fb6116e 100644 --- a/Scalar.Mount/MountAbortedException.cs +++ b/Scalar.Mount/MountAbortedException.cs @@ -1,14 +1,14 @@ -using System; - -namespace Scalar.Mount -{ - public class MountAbortedException : Exception - { - public MountAbortedException(InProcessMountVerb verb) - { - this.Verb = verb; - } - - public InProcessMountVerb Verb { get; } - } -} +using System; + +namespace Scalar.Mount +{ + public class MountAbortedException : Exception + { + public MountAbortedException(InProcessMountVerb verb) + { + this.Verb = verb; + } + + public InProcessMountVerb Verb { get; } + } +} diff --git a/Scalar.Mount/Program.cs b/Scalar.Mount/Program.cs index e2b8cdba3f..61fd12fc08 100644 --- a/Scalar.Mount/Program.cs +++ b/Scalar.Mount/Program.cs @@ -1,24 +1,24 @@ -using CommandLine; -using Scalar.PlatformLoader; -using System; - -namespace Scalar.Mount -{ - public class Program - { - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - try - { - Parser.Default.ParseArguments(args) - .WithParsed(mount => mount.Execute()); - } - catch (MountAbortedException e) - { - // Calling Environment.Exit() is required, to force all background threads to exit as well - Environment.Exit((int)e.Verb.ReturnCode); - } - } - } -} +using CommandLine; +using Scalar.PlatformLoader; +using System; + +namespace Scalar.Mount +{ + public class Program + { + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + try + { + Parser.Default.ParseArguments(args) + .WithParsed(mount => mount.Execute()); + } + catch (MountAbortedException e) + { + // Calling Environment.Exit() is required, to force all background threads to exit as well + Environment.Exit((int)e.Verb.ReturnCode); + } + } + } +} diff --git a/Scalar.Mount/Properties/AssemblyInfo.cs b/Scalar.Mount/Properties/AssemblyInfo.cs index 431aaef3eb..1590f8f1f1 100644 --- a/Scalar.Mount/Properties/AssemblyInfo.cs +++ b/Scalar.Mount/Properties/AssemblyInfo.cs @@ -1,22 +1,22 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar.Mount")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar.Mount")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b8c1dfbd-cafd-4f7e-a1a3-e11907b5467b")] +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar.Mount")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar.Mount")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b8c1dfbd-cafd-4f7e-a1a3-e11907b5467b")] diff --git a/Scalar.Mount/Scalar.Mount.Windows.csproj b/Scalar.Mount/Scalar.Mount.Windows.csproj index 8a9fe76c62..23d26bf165 100644 --- a/Scalar.Mount/Scalar.Mount.Windows.csproj +++ b/Scalar.Mount/Scalar.Mount.Windows.csproj @@ -1,123 +1,123 @@ - - - - - - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} - Exe - Properties - Scalar.Mount - Scalar.Mount - v4.6.1 - 512 - true - - - - - x64 - true - full - false - DEBUG;TRACE - prompt - 4 - false - - - x64 - pdbonly - TRACE - true - prompt - 4 - - - - False - ..\..\packages\CommandLineParser.2.1.1-beta\lib\net45\CommandLine.dll - True - - - ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll - - - False - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - True - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll - - - ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll - - - ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll - - - - - - - - - - - - - - PlatformLoader.Windows.cs - - - - - - - - - - - Designer - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - {4ce404e7-d3fc-471c-993c-64615861ea63} - Scalar.Platform.Windows - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - xcopy /Y $(BuildOutputDir)\Scalar.ReadObjectHook.Windows\bin\$(Platform)\$(Configuration)\Scalar.ReadObjectHook.* $(TargetDir) - - - - - - + + + + + + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} + Exe + Properties + Scalar.Mount + Scalar.Mount + v4.6.1 + 512 + true + + + + + x64 + true + full + false + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + TRACE + true + prompt + 4 + + + + False + ..\..\packages\CommandLineParser.2.1.1-beta\lib\net45\CommandLine.dll + True + + + ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll + + + False + ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll + + + ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll + + + ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll + + + + + + + + + + + + + + PlatformLoader.Windows.cs + + + + + + + + + + + Designer + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + {4ce404e7-d3fc-471c-993c-64615861ea63} + Scalar.Platform.Windows + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + xcopy /Y $(BuildOutputDir)\Scalar.ReadObjectHook.Windows\bin\$(Platform)\$(Configuration)\Scalar.ReadObjectHook.* $(TargetDir) + + + + + + diff --git a/Scalar.Mount/app.config b/Scalar.Mount/app.config index 1da8c9b53a..bd27edc04e 100644 --- a/Scalar.Mount/app.config +++ b/Scalar.Mount/app.config @@ -1,6 +1,6 @@ - - - - - - + + + + + + diff --git a/Scalar.Mount/packages.config b/Scalar.Mount/packages.config index fbf8309c0b..a0646c24e1 100644 --- a/Scalar.Mount/packages.config +++ b/Scalar.Mount/packages.config @@ -1,14 +1,14 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/Scalar.NativeHooks.Common/common.h b/Scalar.NativeHooks.Common/common.h index 099127ed8e..2e7960ddb0 100644 --- a/Scalar.NativeHooks.Common/common.h +++ b/Scalar.NativeHooks.Common/common.h @@ -17,32 +17,32 @@ typedef HANDLE PIPE_HANDLE; #if __cplusplus < 201103L #error The hooks require at least C++11 support #endif - -enum ReturnCode -{ - Success = 0, - InvalidArgCount = 1, - GetCurrentDirectoryFailure = 2, - NotInScalarEnlistment = 3, - PipeConnectError = 4, - PipeConnectTimeout = 5, - InvalidSHA = 6, - PipeWriteFailed = 7, - PipeReadFailed = 8, - FailureToDownload = 9, - PathNameError = 10, - - LastError = PathNameError, -}; -void die(int err, const char *fmt, ...) PRINTF_FMT(2,3); -inline void die(int err, const char *fmt, ...) -{ - va_list params; - va_start(params, fmt); - vfprintf(stderr, fmt, params); - va_end(params); - exit(err); +enum ReturnCode +{ + Success = 0, + InvalidArgCount = 1, + GetCurrentDirectoryFailure = 2, + NotInScalarEnlistment = 3, + PipeConnectError = 4, + PipeConnectTimeout = 5, + InvalidSHA = 6, + PipeWriteFailed = 7, + PipeReadFailed = 8, + FailureToDownload = 9, + PathNameError = 10, + + LastError = PathNameError, +}; + +void die(int err, const char *fmt, ...) PRINTF_FMT(2,3); +inline void die(int err, const char *fmt, ...) +{ + va_list params; + va_start(params, fmt); + vfprintf(stderr, fmt, params); + va_end(params); + exit(err); } PATH_STRING GetFinalPathName(const PATH_STRING& path); diff --git a/Scalar.NativeHooks.Common/common.windows.cpp b/Scalar.NativeHooks.Common/common.windows.cpp index f63f1d90f8..6d049709b5 100644 --- a/Scalar.NativeHooks.Common/common.windows.cpp +++ b/Scalar.NativeHooks.Common/common.windows.cpp @@ -1,195 +1,195 @@ -#pragma once -#include "stdafx.h" -#include -#include -#include -#include "common.h" - -PATH_STRING GetFinalPathName(const PATH_STRING& path) -{ - HANDLE fileHandle; - - // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path - // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx, - // we must set this flag to obtain a handle to a directory - fileHandle = CreateFileW( - path.c_str(), - FILE_READ_ATTRIBUTES, - FILE_SHARE_READ | FILE_SHARE_WRITE, - NULL, - OPEN_EXISTING, - FILE_FLAG_BACKUP_SEMANTICS, - NULL); - - if (fileHandle == INVALID_HANDLE_VALUE) - { - die(ReturnCode::PathNameError, "Could not open oppen handle to %ls to determine final path name, Error: %d\n", path.c_str(), GetLastError()); - } - - wchar_t finalPathByHandle[MAX_PATH] = { 0 }; - DWORD finalPathSize = GetFinalPathNameByHandleW(fileHandle, finalPathByHandle, MAX_PATH, FILE_NAME_NORMALIZED); - if (finalPathSize == 0) - { - die(ReturnCode::PathNameError, "Could not get final path name by handle for %ls, Error: %d\n", path.c_str(), GetLastError()); - } - - std::wstring finalPath(finalPathByHandle); - - // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\" - // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx - std::wstring PathPrefix(L"\\\\?\\"); - std::wstring UncPrefix(L"\\\\?\\UNC\\"); - - if (finalPath.compare(0, UncPrefix.length(), UncPrefix) == 0) - { - finalPath = L"\\\\" + finalPath.substr(UncPrefix.length()); - } - else if (finalPath.compare(0, PathPrefix.length(), PathPrefix) == 0) - { - finalPath = finalPath.substr(PathPrefix.length()); - } - - return finalPath; -} - -PATH_STRING GetScalarPipeName(const char *appName) -{ - // The pipe name is built using the path of the Scalar enlistment root. - // Start in the current directory and walk up the directory tree - // until we find a folder that contains the ".scalar" folder - - const size_t dotScalarRelativePathLength = sizeof(L"\\.scalar") / sizeof(wchar_t); - - // TODO 640838: Support paths longer than MAX_PATH - wchar_t enlistmentRoot[MAX_PATH]; - DWORD currentDirResult = GetCurrentDirectoryW(MAX_PATH - dotScalarRelativePathLength, enlistmentRoot); - if (currentDirResult == 0 || currentDirResult > MAX_PATH - dotScalarRelativePathLength) - { - die(ReturnCode::GetCurrentDirectoryFailure, "GetCurrentDirectory failed (%d)\n", GetLastError()); - } - - PATH_STRING finalRootPath(GetFinalPathName(enlistmentRoot)); - errno_t copyResult = wcscpy_s(enlistmentRoot, finalRootPath.c_str()); - if (copyResult != 0) - { - die(ReturnCode::PipeConnectError, "Could not copy finalRootPath: %ls. Error: %d\n", finalRootPath.c_str(), copyResult); - } - - size_t enlistmentRootLength = wcslen(enlistmentRoot); - if ('\\' != enlistmentRoot[enlistmentRootLength - 1]) - { - wcscat_s(enlistmentRoot, L"\\"); - enlistmentRootLength++; - } - - // Walk up enlistmentRoot looking for a folder named .scalar - wchar_t* lastslash = enlistmentRoot + enlistmentRootLength - 1; - WIN32_FIND_DATAW findFileData; - HANDLE dotScalarHandle; - while (1) - { - wcscat_s(lastslash, MAX_PATH - (lastslash - enlistmentRoot), L".scalar"); - dotScalarHandle = FindFirstFileW(enlistmentRoot, &findFileData); - if (dotScalarHandle != INVALID_HANDLE_VALUE) - { - FindClose(dotScalarHandle); - if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) - { - break; - } - } - - lastslash--; - while ((enlistmentRoot != lastslash) && (*lastslash != '\\')) - { - lastslash--; - } - - if (enlistmentRoot == lastslash) - { - die(ReturnCode::NotInScalarEnlistment, "%s must be run from inside a Scalar enlistment\n", appName); - } - - *(lastslash + 1) = 0; - }; - - *(lastslash) = 0; - - PATH_STRING namedPipe(CharUpperW(enlistmentRoot)); - std::replace(namedPipe.begin(), namedPipe.end(), L':', L'_'); - return L"\\\\.\\pipe\\Scalar_" + namedPipe; -} - -PIPE_HANDLE CreatePipeToScalar(const PATH_STRING& pipeName) -{ - PIPE_HANDLE pipeHandle; - while (1) - { - pipeHandle = CreateFileW( - pipeName.c_str(), // pipe name - GENERIC_READ | // read and write access - GENERIC_WRITE, - 0, // no sharing - NULL, // default security attributes - OPEN_EXISTING, // opens existing pipe - 0, // default attributes - NULL); // no template file - - if (pipeHandle != INVALID_HANDLE_VALUE) - { - break; - } - - if (GetLastError() != ERROR_PIPE_BUSY) - { - die(ReturnCode::PipeConnectError, "Could not open pipe: %ls, Error: %d\n", pipeName.c_str(), GetLastError()); - } - - if (!WaitNamedPipeW(pipeName.c_str(), 3000)) - { - die(ReturnCode::PipeConnectTimeout, "Could not open pipe: %ls, Timed out.", pipeName.c_str()); - } - } - - return pipeHandle; -} - -void DisableCRLFTranslationOnStdPipes() -{ - // set the mode to binary so we don't get CRLF translation - _setmode(_fileno(stdin), _O_BINARY); - _setmode(_fileno(stdout), _O_BINARY); -} - -bool WriteToPipe(PIPE_HANDLE pipe, const char* message, unsigned long messageLength, /* out */ unsigned long* bytesWritten, /* out */ int* error) -{ - BOOL success = WriteFile( - pipe, // pipe handle - message, // message - messageLength, // message length - bytesWritten, // bytes written - NULL); // not overlapped - - *error = success ? 0 : GetLastError(); - - return success != FALSE; -} - -bool ReadFromPipe(PIPE_HANDLE pipe, char* buffer, unsigned long bufferLength, /* out */ unsigned long* bytesRead, /* out */ int* error) -{ - *error = 0; - *bytesRead = 0; - BOOL success = ReadFile( - pipe, // pipe handle - buffer, // buffer to receive reply - bufferLength, // size of buffer - bytesRead, // number of bytes read - NULL); // not overlapped - - if (!success) - { - *error = GetLastError(); - } - - return success || (*error == ERROR_MORE_DATA); +#pragma once +#include "stdafx.h" +#include +#include +#include +#include "common.h" + +PATH_STRING GetFinalPathName(const PATH_STRING& path) +{ + HANDLE fileHandle; + + // Using FILE_FLAG_BACKUP_SEMANTICS as it works with file as well as folder path + // According to MSDN, https://msdn.microsoft.com/en-us/library/windows/desktop/aa363858(v=vs.85).aspx, + // we must set this flag to obtain a handle to a directory + fileHandle = CreateFileW( + path.c_str(), + FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE, + NULL, + OPEN_EXISTING, + FILE_FLAG_BACKUP_SEMANTICS, + NULL); + + if (fileHandle == INVALID_HANDLE_VALUE) + { + die(ReturnCode::PathNameError, "Could not open oppen handle to %ls to determine final path name, Error: %d\n", path.c_str(), GetLastError()); + } + + wchar_t finalPathByHandle[MAX_PATH] = { 0 }; + DWORD finalPathSize = GetFinalPathNameByHandleW(fileHandle, finalPathByHandle, MAX_PATH, FILE_NAME_NORMALIZED); + if (finalPathSize == 0) + { + die(ReturnCode::PathNameError, "Could not get final path name by handle for %ls, Error: %d\n", path.c_str(), GetLastError()); + } + + std::wstring finalPath(finalPathByHandle); + + // The remarks section of GetFinalPathNameByHandle mentions the return being prefixed with "\\?\" or "\\?\UNC\" + // More information the prefixes is here http://msdn.microsoft.com/en-us/library/aa365247(v=VS.85).aspx + std::wstring PathPrefix(L"\\\\?\\"); + std::wstring UncPrefix(L"\\\\?\\UNC\\"); + + if (finalPath.compare(0, UncPrefix.length(), UncPrefix) == 0) + { + finalPath = L"\\\\" + finalPath.substr(UncPrefix.length()); + } + else if (finalPath.compare(0, PathPrefix.length(), PathPrefix) == 0) + { + finalPath = finalPath.substr(PathPrefix.length()); + } + + return finalPath; +} + +PATH_STRING GetScalarPipeName(const char *appName) +{ + // The pipe name is built using the path of the Scalar enlistment root. + // Start in the current directory and walk up the directory tree + // until we find a folder that contains the ".scalar" folder + + const size_t dotScalarRelativePathLength = sizeof(L"\\.scalar") / sizeof(wchar_t); + + // TODO 640838: Support paths longer than MAX_PATH + wchar_t enlistmentRoot[MAX_PATH]; + DWORD currentDirResult = GetCurrentDirectoryW(MAX_PATH - dotScalarRelativePathLength, enlistmentRoot); + if (currentDirResult == 0 || currentDirResult > MAX_PATH - dotScalarRelativePathLength) + { + die(ReturnCode::GetCurrentDirectoryFailure, "GetCurrentDirectory failed (%d)\n", GetLastError()); + } + + PATH_STRING finalRootPath(GetFinalPathName(enlistmentRoot)); + errno_t copyResult = wcscpy_s(enlistmentRoot, finalRootPath.c_str()); + if (copyResult != 0) + { + die(ReturnCode::PipeConnectError, "Could not copy finalRootPath: %ls. Error: %d\n", finalRootPath.c_str(), copyResult); + } + + size_t enlistmentRootLength = wcslen(enlistmentRoot); + if ('\\' != enlistmentRoot[enlistmentRootLength - 1]) + { + wcscat_s(enlistmentRoot, L"\\"); + enlistmentRootLength++; + } + + // Walk up enlistmentRoot looking for a folder named .scalar + wchar_t* lastslash = enlistmentRoot + enlistmentRootLength - 1; + WIN32_FIND_DATAW findFileData; + HANDLE dotScalarHandle; + while (1) + { + wcscat_s(lastslash, MAX_PATH - (lastslash - enlistmentRoot), L".scalar"); + dotScalarHandle = FindFirstFileW(enlistmentRoot, &findFileData); + if (dotScalarHandle != INVALID_HANDLE_VALUE) + { + FindClose(dotScalarHandle); + if (findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + break; + } + } + + lastslash--; + while ((enlistmentRoot != lastslash) && (*lastslash != '\\')) + { + lastslash--; + } + + if (enlistmentRoot == lastslash) + { + die(ReturnCode::NotInScalarEnlistment, "%s must be run from inside a Scalar enlistment\n", appName); + } + + *(lastslash + 1) = 0; + }; + + *(lastslash) = 0; + + PATH_STRING namedPipe(CharUpperW(enlistmentRoot)); + std::replace(namedPipe.begin(), namedPipe.end(), L':', L'_'); + return L"\\\\.\\pipe\\Scalar_" + namedPipe; +} + +PIPE_HANDLE CreatePipeToScalar(const PATH_STRING& pipeName) +{ + PIPE_HANDLE pipeHandle; + while (1) + { + pipeHandle = CreateFileW( + pipeName.c_str(), // pipe name + GENERIC_READ | // read and write access + GENERIC_WRITE, + 0, // no sharing + NULL, // default security attributes + OPEN_EXISTING, // opens existing pipe + 0, // default attributes + NULL); // no template file + + if (pipeHandle != INVALID_HANDLE_VALUE) + { + break; + } + + if (GetLastError() != ERROR_PIPE_BUSY) + { + die(ReturnCode::PipeConnectError, "Could not open pipe: %ls, Error: %d\n", pipeName.c_str(), GetLastError()); + } + + if (!WaitNamedPipeW(pipeName.c_str(), 3000)) + { + die(ReturnCode::PipeConnectTimeout, "Could not open pipe: %ls, Timed out.", pipeName.c_str()); + } + } + + return pipeHandle; +} + +void DisableCRLFTranslationOnStdPipes() +{ + // set the mode to binary so we don't get CRLF translation + _setmode(_fileno(stdin), _O_BINARY); + _setmode(_fileno(stdout), _O_BINARY); +} + +bool WriteToPipe(PIPE_HANDLE pipe, const char* message, unsigned long messageLength, /* out */ unsigned long* bytesWritten, /* out */ int* error) +{ + BOOL success = WriteFile( + pipe, // pipe handle + message, // message + messageLength, // message length + bytesWritten, // bytes written + NULL); // not overlapped + + *error = success ? 0 : GetLastError(); + + return success != FALSE; +} + +bool ReadFromPipe(PIPE_HANDLE pipe, char* buffer, unsigned long bufferLength, /* out */ unsigned long* bytesRead, /* out */ int* error) +{ + *error = 0; + *bytesRead = 0; + BOOL success = ReadFile( + pipe, // pipe handle + buffer, // buffer to receive reply + bufferLength, // size of buffer + bytesRead, // number of bytes read + NULL); // not overlapped + + if (!success) + { + *error = GetLastError(); + } + + return success || (*error == ERROR_MORE_DATA); } diff --git a/Scalar.Platform.Mac/DiskLayoutUpgrades/MacDiskLayoutUpgradeData.cs b/Scalar.Platform.Mac/DiskLayoutUpgrades/MacDiskLayoutUpgradeData.cs index ed95eaeb6a..86e65b4ba8 100644 --- a/Scalar.Platform.Mac/DiskLayoutUpgrades/MacDiskLayoutUpgradeData.cs +++ b/Scalar.Platform.Mac/DiskLayoutUpgrades/MacDiskLayoutUpgradeData.cs @@ -1,29 +1,29 @@ -using Scalar.Common; -using Scalar.DiskLayoutUpgrades; - -namespace Scalar.Platform.Mac -{ - public class MacDiskLayoutUpgradeData : IDiskLayoutUpgradeData - { - public DiskLayoutUpgrade[] Upgrades - { - get - { - return new DiskLayoutUpgrade[] - { - }; - } - } - - public DiskLayoutVersion Version => new DiskLayoutVersion( - currentMajorVersion: 0, - currentMinorVersion: 0, - minimumSupportedMajorVersion: 0); - - public bool TryParseLegacyDiskLayoutVersion(string dotScalarPath, out int majorVersion) - { - majorVersion = 0; - return false; - } - } -} +using Scalar.Common; +using Scalar.DiskLayoutUpgrades; + +namespace Scalar.Platform.Mac +{ + public class MacDiskLayoutUpgradeData : IDiskLayoutUpgradeData + { + public DiskLayoutUpgrade[] Upgrades + { + get + { + return new DiskLayoutUpgrade[] + { + }; + } + } + + public DiskLayoutVersion Version => new DiskLayoutVersion( + currentMajorVersion: 0, + currentMinorVersion: 0, + minimumSupportedMajorVersion: 0); + + public bool TryParseLegacyDiskLayoutVersion(string dotScalarPath, out int majorVersion) + { + majorVersion = 0; + return false; + } + } +} diff --git a/Scalar.Platform.Mac/MacDaemonController.cs b/Scalar.Platform.Mac/MacDaemonController.cs index 935cc99a9c..f2368d6487 100644 --- a/Scalar.Platform.Mac/MacDaemonController.cs +++ b/Scalar.Platform.Mac/MacDaemonController.cs @@ -1,74 +1,74 @@ -using Scalar.Common; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.Platform.Mac -{ - /// - /// Class to query the configured services on macOS - /// - public class MacDaemonController - { - private const string LaunchCtlPath = @"/bin/launchctl"; - private const string LaunchCtlArg = @"list"; - - private IProcessRunner processRunner; - - public MacDaemonController(IProcessRunner processRunner) - { - this.processRunner = processRunner; - } - - public bool TryGetDaemons(string currentUser, out List daemons, out string error) - { - // Consider for future improvement: - // Use Launchtl to run Launchctl as the "real" user, so we can get the process list from the user. - ProcessResult result = this.processRunner.Run(LaunchCtlPath, "asuser " + currentUser + " " + LaunchCtlPath + " " + LaunchCtlArg, true); - - if (result.ExitCode != 0) - { - error = result.Output; - daemons = null; - return false; - } - - return this.TryParseOutput(result.Output, out daemons, out error); - } - - private bool TryParseOutput(string output, out List daemonInfos, out string error) - { - daemonInfos = new List(); - - // 1st line is the header, skip it - foreach (string line in output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Skip(1)) - { - // The expected output is a list of tab delimited entried: - // PID\tSTATUS\tLABEL - string[] tokens = line.Split('\t'); - - if (tokens.Length != 3) - { - daemonInfos = null; - error = $"Unexpected number of tokens in line: {line}"; - return false; - } - - string label = tokens[2]; - bool isRunning = int.TryParse(tokens[0], out _); - - daemonInfos.Add(new DaemonInfo() { Name = label, IsRunning = isRunning }); - } - - error = null; - return true; - } - - public class DaemonInfo - { - public string Name { get; set; } - public bool IsRunning { get; set; } - } - } -} +using Scalar.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.Platform.Mac +{ + /// + /// Class to query the configured services on macOS + /// + public class MacDaemonController + { + private const string LaunchCtlPath = @"/bin/launchctl"; + private const string LaunchCtlArg = @"list"; + + private IProcessRunner processRunner; + + public MacDaemonController(IProcessRunner processRunner) + { + this.processRunner = processRunner; + } + + public bool TryGetDaemons(string currentUser, out List daemons, out string error) + { + // Consider for future improvement: + // Use Launchtl to run Launchctl as the "real" user, so we can get the process list from the user. + ProcessResult result = this.processRunner.Run(LaunchCtlPath, "asuser " + currentUser + " " + LaunchCtlPath + " " + LaunchCtlArg, true); + + if (result.ExitCode != 0) + { + error = result.Output; + daemons = null; + return false; + } + + return this.TryParseOutput(result.Output, out daemons, out error); + } + + private bool TryParseOutput(string output, out List daemonInfos, out string error) + { + daemonInfos = new List(); + + // 1st line is the header, skip it + foreach (string line in output.Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries).Skip(1)) + { + // The expected output is a list of tab delimited entried: + // PID\tSTATUS\tLABEL + string[] tokens = line.Split('\t'); + + if (tokens.Length != 3) + { + daemonInfos = null; + error = $"Unexpected number of tokens in line: {line}"; + return false; + } + + string label = tokens[2]; + bool isRunning = int.TryParse(tokens[0], out _); + + daemonInfos.Add(new DaemonInfo() { Name = label, IsRunning = isRunning }); + } + + error = null; + return true; + } + + public class DaemonInfo + { + public string Name { get; set; } + public bool IsRunning { get; set; } + } + } +} diff --git a/Scalar.Platform.Mac/MacFileBasedLock.cs b/Scalar.Platform.Mac/MacFileBasedLock.cs index da7bf82734..1e45e0e731 100644 --- a/Scalar.Platform.Mac/MacFileBasedLock.cs +++ b/Scalar.Platform.Mac/MacFileBasedLock.cs @@ -1,28 +1,28 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; using System.Runtime.InteropServices; -namespace Scalar.Platform.Mac -{ - public class MacFileBasedLock : FileBasedLock - { - private int lockFileDescriptor; - - public MacFileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - : base(fileSystem, tracer, lockPath) - { - this.lockFileDescriptor = NativeMethods.InvalidFileDescriptor; - } - - public override bool TryAcquireLock() - { - if (this.lockFileDescriptor == NativeMethods.InvalidFileDescriptor) +namespace Scalar.Platform.Mac +{ + public class MacFileBasedLock : FileBasedLock + { + private int lockFileDescriptor; + + public MacFileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + : base(fileSystem, tracer, lockPath) + { + this.lockFileDescriptor = NativeMethods.InvalidFileDescriptor; + } + + public override bool TryAcquireLock() + { + if (this.lockFileDescriptor == NativeMethods.InvalidFileDescriptor) { this.FileSystem.CreateDirectory(Path.GetDirectoryName(this.LockPath)); @@ -33,91 +33,91 @@ public override bool TryAcquireLock() if (this.lockFileDescriptor == NativeMethods.InvalidFileDescriptor) { - int errno = Marshal.GetLastWin32Error(); - EventMetadata metadata = this.CreateEventMetadata(errno); - this.Tracer.RelatedWarning( - metadata, - $"{nameof(MacFileBasedLock)}.{nameof(this.TryAcquireLock)}: Failed to open lock file"); + int errno = Marshal.GetLastWin32Error(); + EventMetadata metadata = this.CreateEventMetadata(errno); + this.Tracer.RelatedWarning( + metadata, + $"{nameof(MacFileBasedLock)}.{nameof(this.TryAcquireLock)}: Failed to open lock file"); return false; - } - } - - if (NativeMethods.FLock(this.lockFileDescriptor, NativeMethods.LockEx | NativeMethods.LockNb) != 0) - { - int errno = Marshal.GetLastWin32Error(); - if (errno != NativeMethods.EIntr && errno != NativeMethods.EWouldBlock) - { - EventMetadata metadata = this.CreateEventMetadata(errno); - this.Tracer.RelatedWarning( - metadata, - $"{nameof(MacFileBasedLock)}.{nameof(this.TryAcquireLock)}: Unexpected error when locking file"); - } - - return false; - } - - return true; - } - - public override void Dispose() - { - if (this.lockFileDescriptor != NativeMethods.InvalidFileDescriptor) - { - if (NativeMethods.Close(this.lockFileDescriptor) != 0) - { - // Failures of close() are logged for diagnostic purposes only. - // It's possible that errors from a previous operation (e.g. write(2)) - // are only reported in close(). We should *not* retry the close() if - // it fails since it may cause a re-used file descriptor from another - // thread to be closed. - - int errno = Marshal.GetLastWin32Error(); - EventMetadata metadata = this.CreateEventMetadata(errno); - this.Tracer.RelatedWarning( - metadata, - $"{nameof(MacFileBasedLock)}.{nameof(this.Dispose)}: Error when closing lock fd"); - } - - this.lockFileDescriptor = NativeMethods.InvalidFileDescriptor; - } - } - - private EventMetadata CreateEventMetadata(int errno = 0) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(MacFileBasedLock)); - metadata.Add(nameof(this.LockPath), this.LockPath); - if (errno != 0) - { - metadata.Add(nameof(errno), errno); - } - - return metadata; - } - - private static class NativeMethods - { - // #define O_WRONLY 0x0001 /* open for writing only */ - public const int OpenWriteOnly = 0x0001; - - // #define O_CREAT 0x0200 /* create if nonexistant */ - public const int OpenCreate = 0x0200; - - // #define EINTR 4 /* Interrupted system call */ - public const int EIntr = 4; - - // #define EAGAIN 35 /* Resource temporarily unavailable */ - // #define EWOULDBLOCK EAGAIN /* Operation would block */ - public const int EWouldBlock = 35; - - public const int LockSh = 1; // #define LOCK_SH 1 /* shared lock */ - public const int LockEx = 2; // #define LOCK_EX 2 /* exclusive lock */ - public const int LockNb = 4; // #define LOCK_NB 4 /* don't block when locking */ - public const int LockUn = 8; // #define LOCK_UN 8 /* unlock */ - - public const int InvalidFileDescriptor = -1; - + } + } + + if (NativeMethods.FLock(this.lockFileDescriptor, NativeMethods.LockEx | NativeMethods.LockNb) != 0) + { + int errno = Marshal.GetLastWin32Error(); + if (errno != NativeMethods.EIntr && errno != NativeMethods.EWouldBlock) + { + EventMetadata metadata = this.CreateEventMetadata(errno); + this.Tracer.RelatedWarning( + metadata, + $"{nameof(MacFileBasedLock)}.{nameof(this.TryAcquireLock)}: Unexpected error when locking file"); + } + + return false; + } + + return true; + } + + public override void Dispose() + { + if (this.lockFileDescriptor != NativeMethods.InvalidFileDescriptor) + { + if (NativeMethods.Close(this.lockFileDescriptor) != 0) + { + // Failures of close() are logged for diagnostic purposes only. + // It's possible that errors from a previous operation (e.g. write(2)) + // are only reported in close(). We should *not* retry the close() if + // it fails since it may cause a re-used file descriptor from another + // thread to be closed. + + int errno = Marshal.GetLastWin32Error(); + EventMetadata metadata = this.CreateEventMetadata(errno); + this.Tracer.RelatedWarning( + metadata, + $"{nameof(MacFileBasedLock)}.{nameof(this.Dispose)}: Error when closing lock fd"); + } + + this.lockFileDescriptor = NativeMethods.InvalidFileDescriptor; + } + } + + private EventMetadata CreateEventMetadata(int errno = 0) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(MacFileBasedLock)); + metadata.Add(nameof(this.LockPath), this.LockPath); + if (errno != 0) + { + metadata.Add(nameof(errno), errno); + } + + return metadata; + } + + private static class NativeMethods + { + // #define O_WRONLY 0x0001 /* open for writing only */ + public const int OpenWriteOnly = 0x0001; + + // #define O_CREAT 0x0200 /* create if nonexistant */ + public const int OpenCreate = 0x0200; + + // #define EINTR 4 /* Interrupted system call */ + public const int EIntr = 4; + + // #define EAGAIN 35 /* Resource temporarily unavailable */ + // #define EWOULDBLOCK EAGAIN /* Operation would block */ + public const int EWouldBlock = 35; + + public const int LockSh = 1; // #define LOCK_SH 1 /* shared lock */ + public const int LockEx = 2; // #define LOCK_EX 2 /* exclusive lock */ + public const int LockNb = 4; // #define LOCK_NB 4 /* don't block when locking */ + public const int LockUn = 8; // #define LOCK_UN 8 /* unlock */ + + public const int InvalidFileDescriptor = -1; + public static readonly ushort FileMode644 = Convert.ToUInt16("644", 8); [DllImport("libc", EntryPoint = "open", SetLastError = true)] @@ -127,7 +127,7 @@ private static class NativeMethods public static extern int Close(int fd); [DllImport("libc", EntryPoint = "flock", SetLastError = true)] - public static extern int FLock(int fd, int operation); - } - } -} + public static extern int FLock(int fd, int operation); + } + } +} diff --git a/Scalar.Platform.Mac/MacFileSystem.cs b/Scalar.Platform.Mac/MacFileSystem.cs index 03388efdb8..ea76adaec6 100644 --- a/Scalar.Platform.Mac/MacFileSystem.cs +++ b/Scalar.Platform.Mac/MacFileSystem.cs @@ -1,185 +1,185 @@ -using Scalar.Common; -using Scalar.Platform.POSIX; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.Platform.Mac -{ - public class MacFileSystem : POSIXFileSystem - { - public override void ChangeMode(string path, ushort mode) - { - Chmod(path, mode); - } - - public override bool HydrateFile(string fileName, byte[] buffer) - { - return NativeFileReader.TryReadFirstByteOfFile(fileName, buffer); - } - - public override bool IsExecutable(string fileName) - { - NativeStat.StatBuffer statBuffer = this.StatFile(fileName); - return NativeStat.IsExecutable(statBuffer.Mode); - } - - public override bool IsSocket(string fileName) - { - NativeStat.StatBuffer statBuffer = this.StatFile(fileName); - return NativeStat.IsSock(statBuffer.Mode); - } - - public override bool IsFileSystemSupported(string path, out string error) - { - string lowerCaseFile = Guid.NewGuid().ToString().ToLower(); - string upperCaseFile = lowerCaseFile.ToUpper(); - error = null; - - try - { - File.Create(Path.Combine(path, lowerCaseFile)); - if (File.Exists(Path.Combine(path, upperCaseFile))) - { - File.Delete(lowerCaseFile); - return true; - } - - File.Delete(lowerCaseFile); - error = "Scalar does not support case sensitive filesystems"; - return false; - } - catch (Exception ex) - { - error = $"Exception when performing {nameof(MacFileSystem)}.{nameof(this.IsFileSystemSupported)}: {ex.ToString()}"; - return false; - } - } - - [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] - private static extern int Chmod(string pathname, ushort mode); - - private NativeStat.StatBuffer StatFile(string fileName) - { - if (NativeStat.Stat(fileName, out NativeStat.StatBuffer statBuffer) != 0) - { - NativeMethods.ThrowLastWin32Exception($"Failed to stat {fileName}"); - } - - return statBuffer; - } - - private static class NativeStat - { - // #define S_IFMT 0170000 /* [XSI] type of file mask */ - private static readonly ushort IFMT = Convert.ToUInt16("170000", 8); - - // #define S_IFSOCK 0140000 /* [XSI] socket */ - private static readonly ushort IFSOCK = Convert.ToUInt16("0140000", 8); - - // #define S_IXUSR 0000100 /* [XSI] X for owner */ - private static readonly ushort IXUSR = Convert.ToUInt16("100", 8); - - // #define S_IXGRP 0000010 /* [XSI] X for group */ - private static readonly ushort IXGRP = Convert.ToUInt16("10", 8); - - // #define S_IXOTH 0000001 /* [XSI] X for other */ - private static readonly ushort IXOTH = Convert.ToUInt16("1", 8); - - public static bool IsSock(ushort mode) - { - // #define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) /* socket */ - return (mode & IFMT) == IFSOCK; - } - - public static bool IsExecutable(ushort mode) - { - return (mode & (IXUSR | IXGRP | IXOTH)) != 0; - } - - [DllImport("libc", EntryPoint = "stat$INODE64", SetLastError = true)] - public static extern int Stat(string path, [Out] out StatBuffer statBuffer); - - [StructLayout(LayoutKind.Sequential)] - public struct TimeSpec - { - public long Sec; - public long Nsec; - } - - [StructLayout(LayoutKind.Sequential)] - public struct StatBuffer - { - public int Dev; /* ID of device containing file */ - public ushort Mode; /* Mode of file (see below) */ - public ushort NLink; /* Number of hard links */ - public ulong Ino; /* File serial number */ - public uint UID; /* User ID of the file */ - public uint GID; /* Group ID of the file */ - public int RDev; /* Device ID */ - - public TimeSpec ATimespec; /* time of last access */ - public TimeSpec MTimespec; /* time of last data modification */ - public TimeSpec CTimespec; /* time of last status change */ - public TimeSpec BirthTimespec; /* time of file creation(birth) */ - - public long Size; /* file size, in bytes */ - public long Blocks; /* blocks allocated for file */ - public int BlkSize; /* optimal blocksize for I/O */ - public uint Glags; /* user defined flags for file */ - public uint Gen; /* file generation number */ - public int LSpare; /* RESERVED: DO NOT USE! */ - - [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] - public long[] QSpare; /* RESERVED: DO NOT USE! */ - } - } - - private static class NativeFileReader - { - // #define O_RDONLY 0x0000 /* open for reading only */ - private const int ReadOnly = 0x0000; - - internal static bool TryReadFirstByteOfFile(string fileName, byte[] buffer) - { - int fileDescriptor = -1; - bool readStatus = false; - try - { - fileDescriptor = Open(fileName, ReadOnly); - if (fileDescriptor != -1) - { - readStatus = TryReadOneByte(fileDescriptor, buffer); - } - } - finally - { - Close(fileDescriptor); - } - - return readStatus; - } - - [DllImport("libc", EntryPoint = "open", SetLastError = true)] - private static extern int Open(string path, int flag); - - [DllImport("libc", EntryPoint = "close", SetLastError = true)] - private static extern int Close(int fd); - - [DllImport("libc", EntryPoint = "read", SetLastError = true)] - private static extern int Read(int fd, [Out] byte[] buf, int count); - - private static bool TryReadOneByte(int fileDescriptor, byte[] buffer) - { - int numBytes = Read(fileDescriptor, buffer, 1); - - if (numBytes == -1) - { - return false; - } - - return true; - } - } - } -} +using Scalar.Common; +using Scalar.Platform.POSIX; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.Platform.Mac +{ + public class MacFileSystem : POSIXFileSystem + { + public override void ChangeMode(string path, ushort mode) + { + Chmod(path, mode); + } + + public override bool HydrateFile(string fileName, byte[] buffer) + { + return NativeFileReader.TryReadFirstByteOfFile(fileName, buffer); + } + + public override bool IsExecutable(string fileName) + { + NativeStat.StatBuffer statBuffer = this.StatFile(fileName); + return NativeStat.IsExecutable(statBuffer.Mode); + } + + public override bool IsSocket(string fileName) + { + NativeStat.StatBuffer statBuffer = this.StatFile(fileName); + return NativeStat.IsSock(statBuffer.Mode); + } + + public override bool IsFileSystemSupported(string path, out string error) + { + string lowerCaseFile = Guid.NewGuid().ToString().ToLower(); + string upperCaseFile = lowerCaseFile.ToUpper(); + error = null; + + try + { + File.Create(Path.Combine(path, lowerCaseFile)); + if (File.Exists(Path.Combine(path, upperCaseFile))) + { + File.Delete(lowerCaseFile); + return true; + } + + File.Delete(lowerCaseFile); + error = "Scalar does not support case sensitive filesystems"; + return false; + } + catch (Exception ex) + { + error = $"Exception when performing {nameof(MacFileSystem)}.{nameof(this.IsFileSystemSupported)}: {ex.ToString()}"; + return false; + } + } + + [DllImport("libc", EntryPoint = "chmod", SetLastError = true)] + private static extern int Chmod(string pathname, ushort mode); + + private NativeStat.StatBuffer StatFile(string fileName) + { + if (NativeStat.Stat(fileName, out NativeStat.StatBuffer statBuffer) != 0) + { + NativeMethods.ThrowLastWin32Exception($"Failed to stat {fileName}"); + } + + return statBuffer; + } + + private static class NativeStat + { + // #define S_IFMT 0170000 /* [XSI] type of file mask */ + private static readonly ushort IFMT = Convert.ToUInt16("170000", 8); + + // #define S_IFSOCK 0140000 /* [XSI] socket */ + private static readonly ushort IFSOCK = Convert.ToUInt16("0140000", 8); + + // #define S_IXUSR 0000100 /* [XSI] X for owner */ + private static readonly ushort IXUSR = Convert.ToUInt16("100", 8); + + // #define S_IXGRP 0000010 /* [XSI] X for group */ + private static readonly ushort IXGRP = Convert.ToUInt16("10", 8); + + // #define S_IXOTH 0000001 /* [XSI] X for other */ + private static readonly ushort IXOTH = Convert.ToUInt16("1", 8); + + public static bool IsSock(ushort mode) + { + // #define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) /* socket */ + return (mode & IFMT) == IFSOCK; + } + + public static bool IsExecutable(ushort mode) + { + return (mode & (IXUSR | IXGRP | IXOTH)) != 0; + } + + [DllImport("libc", EntryPoint = "stat$INODE64", SetLastError = true)] + public static extern int Stat(string path, [Out] out StatBuffer statBuffer); + + [StructLayout(LayoutKind.Sequential)] + public struct TimeSpec + { + public long Sec; + public long Nsec; + } + + [StructLayout(LayoutKind.Sequential)] + public struct StatBuffer + { + public int Dev; /* ID of device containing file */ + public ushort Mode; /* Mode of file (see below) */ + public ushort NLink; /* Number of hard links */ + public ulong Ino; /* File serial number */ + public uint UID; /* User ID of the file */ + public uint GID; /* Group ID of the file */ + public int RDev; /* Device ID */ + + public TimeSpec ATimespec; /* time of last access */ + public TimeSpec MTimespec; /* time of last data modification */ + public TimeSpec CTimespec; /* time of last status change */ + public TimeSpec BirthTimespec; /* time of file creation(birth) */ + + public long Size; /* file size, in bytes */ + public long Blocks; /* blocks allocated for file */ + public int BlkSize; /* optimal blocksize for I/O */ + public uint Glags; /* user defined flags for file */ + public uint Gen; /* file generation number */ + public int LSpare; /* RESERVED: DO NOT USE! */ + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + public long[] QSpare; /* RESERVED: DO NOT USE! */ + } + } + + private static class NativeFileReader + { + // #define O_RDONLY 0x0000 /* open for reading only */ + private const int ReadOnly = 0x0000; + + internal static bool TryReadFirstByteOfFile(string fileName, byte[] buffer) + { + int fileDescriptor = -1; + bool readStatus = false; + try + { + fileDescriptor = Open(fileName, ReadOnly); + if (fileDescriptor != -1) + { + readStatus = TryReadOneByte(fileDescriptor, buffer); + } + } + finally + { + Close(fileDescriptor); + } + + return readStatus; + } + + [DllImport("libc", EntryPoint = "open", SetLastError = true)] + private static extern int Open(string path, int flag); + + [DllImport("libc", EntryPoint = "close", SetLastError = true)] + private static extern int Close(int fd); + + [DllImport("libc", EntryPoint = "read", SetLastError = true)] + private static extern int Read(int fd, [Out] byte[] buf, int count); + + private static bool TryReadOneByte(int fileDescriptor, byte[] buffer) + { + int numBytes = Read(fileDescriptor, buffer, 1); + + if (numBytes == -1) + { + return false; + } + + return true; + } + } + } +} diff --git a/Scalar.Platform.Mac/MacPlatform.Shared.cs b/Scalar.Platform.Mac/MacPlatform.Shared.cs index 498870f6b7..b9ca0c31a3 100644 --- a/Scalar.Platform.Mac/MacPlatform.Shared.cs +++ b/Scalar.Platform.Mac/MacPlatform.Shared.cs @@ -1,26 +1,26 @@ -using System; -using System.IO; +using System; +using System.IO; using Scalar.Common; -using Scalar.Platform.POSIX; - -namespace Scalar.Platform.Mac -{ - public partial class MacPlatform - { - public const string DotScalarRoot = ".scalar"; - - public static string GetDataRootForScalarImplementation() - { - return Path.Combine( - Environment.GetEnvironmentVariable("HOME"), - "Library", - "Application Support", - "Scalar"); - } - - public static string GetDataRootForScalarComponentImplementation(string componentName) - { - return Path.Combine(GetDataRootForScalarImplementation(), componentName); +using Scalar.Platform.POSIX; + +namespace Scalar.Platform.Mac +{ + public partial class MacPlatform + { + public const string DotScalarRoot = ".scalar"; + + public static string GetDataRootForScalarImplementation() + { + return Path.Combine( + Environment.GetEnvironmentVariable("HOME"), + "Library", + "Application Support", + "Scalar"); + } + + public static string GetDataRootForScalarComponentImplementation(string componentName) + { + return Path.Combine(GetDataRootForScalarImplementation(), componentName); } public static bool TryGetScalarEnlistmentRootImplementation(string directory, out string enlistmentRoot, out string errorMessage) @@ -38,14 +38,14 @@ public static string GetUpgradeNonProtectedDirectoryImplementation() return Path.Combine(GetDataRootForScalarImplementation(), ProductUpgraderInfo.UpgradeDirectoryName); } - public static string GetNamedPipeNameImplementation(string enlistmentRoot) - { - return POSIXPlatform.GetNamedPipeNameImplementation(enlistmentRoot, DotScalarRoot); - } + public static string GetNamedPipeNameImplementation(string enlistmentRoot) + { + return POSIXPlatform.GetNamedPipeNameImplementation(enlistmentRoot, DotScalarRoot); + } - private string GetUpgradeNonProtectedDataDirectory() - { + private string GetUpgradeNonProtectedDataDirectory() + { return GetUpgradeNonProtectedDirectoryImplementation(); - } - } -} + } + } +} diff --git a/Scalar.Platform.Mac/MacPlatform.cs b/Scalar.Platform.Mac/MacPlatform.cs index f6ac50ec28..a8ca413a6b 100644 --- a/Scalar.Platform.Mac/MacPlatform.cs +++ b/Scalar.Platform.Mac/MacPlatform.cs @@ -1,74 +1,74 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using Scalar.Platform.POSIX; -using System.Collections.Generic; -using System.IO; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using Scalar.Platform.POSIX; +using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Xml; -using System.Xml.Linq; -using System.Xml.XPath; - -namespace Scalar.Platform.Mac -{ - public partial class MacPlatform : POSIXPlatform - { - private const string UpgradeProtectedDataDirectory = "/usr/local/scalar_upgrader"; - - public MacPlatform() : base( - underConstruction: new UnderConstructionFlags( - supportsScalarUpgrade: true, +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; + +namespace Scalar.Platform.Mac +{ + public partial class MacPlatform : POSIXPlatform + { + private const string UpgradeProtectedDataDirectory = "/usr/local/scalar_upgrader"; + + public MacPlatform() : base( + underConstruction: new UnderConstructionFlags( + supportsScalarUpgrade: true, supportsScalarConfig: true, - supportsNuGetEncryption: false)) - { - } - - public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new MacDiskLayoutUpgradeData(); - public override string Name { get => "macOS"; } - public override ScalarPlatformConstants Constants { get; } = new MacPlatformConstants(); - public override IPlatformFileSystem FileSystem { get; } = new MacFileSystem(); - - public override string ScalarConfigPath - { - get - { - return Path.Combine(this.Constants.ScalarBinDirectoryPath, LocalScalarConfig.FileName); - } - } - - public override string GetOSVersionInformation() - { - ProcessResult result = ProcessHelper.Run("sw_vers", args: string.Empty, redirectOutput: true); - return string.IsNullOrWhiteSpace(result.Output) ? result.Errors : result.Output; - } - - public override string GetDataRootForScalar() - { - return MacPlatform.GetDataRootForScalarImplementation(); - } - - public override string GetDataRootForScalarComponent(string componentName) - { - return MacPlatform.GetDataRootForScalarComponentImplementation(componentName); - } - - public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) - { - return MacPlatform.TryGetScalarEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage); - } - - public override string GetNamedPipeName(string enlistmentRoot) - { - return MacPlatform.GetNamedPipeNameImplementation(enlistmentRoot); - } - - public override FileBasedLock CreateFileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - { - return new MacFileBasedLock(fileSystem, tracer, lockPath); - } + supportsNuGetEncryption: false)) + { + } + + public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new MacDiskLayoutUpgradeData(); + public override string Name { get => "macOS"; } + public override ScalarPlatformConstants Constants { get; } = new MacPlatformConstants(); + public override IPlatformFileSystem FileSystem { get; } = new MacFileSystem(); + + public override string ScalarConfigPath + { + get + { + return Path.Combine(this.Constants.ScalarBinDirectoryPath, LocalScalarConfig.FileName); + } + } + + public override string GetOSVersionInformation() + { + ProcessResult result = ProcessHelper.Run("sw_vers", args: string.Empty, redirectOutput: true); + return string.IsNullOrWhiteSpace(result.Output) ? result.Errors : result.Output; + } + + public override string GetDataRootForScalar() + { + return MacPlatform.GetDataRootForScalarImplementation(); + } + + public override string GetDataRootForScalarComponent(string componentName) + { + return MacPlatform.GetDataRootForScalarComponentImplementation(componentName); + } + + public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) + { + return MacPlatform.TryGetScalarEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage); + } + + public override string GetNamedPipeName(string enlistmentRoot) + { + return MacPlatform.GetNamedPipeNameImplementation(enlistmentRoot); + } + + public override FileBasedLock CreateFileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + { + return new MacFileBasedLock(fileSystem, tracer, lockPath); + } public override string GetUpgradeProtectedDataDirectory() { @@ -89,33 +89,33 @@ public override string GetUpgradeLogDirectoryParentDirectory() { return this.GetUpgradeNonProtectedDataDirectory(); } - - public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) - { - // DiskUtil will return disk statistics in xml format - ProcessResult processResult = ProcessHelper.Run("diskutil", "info -plist /", true); - Dictionary result = new Dictionary(); - if (string.IsNullOrEmpty(processResult.Output)) - { - result.Add("DiskUtilError", processResult.Errors); - return result; - } - - try - { - // Parse the XML looking for FilesystemType - XDocument xmlDoc = XDocument.Parse(processResult.Output); - XElement filesystemTypeValue = xmlDoc.XPathSelectElement("plist/dict/key[text()=\"FilesystemType\"]")?.NextNode as XElement; - result.Add("FileSystemType", filesystemTypeValue != null ? filesystemTypeValue.Value : "Not Found"); - } - catch (XmlException ex) - { - result.Add("DiskUtilError", ex.ToString()); - } - - return result; - } - + + public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) + { + // DiskUtil will return disk statistics in xml format + ProcessResult processResult = ProcessHelper.Run("diskutil", "info -plist /", true); + Dictionary result = new Dictionary(); + if (string.IsNullOrEmpty(processResult.Output)) + { + result.Add("DiskUtilError", processResult.Errors); + return result; + } + + try + { + // Parse the XML looking for FilesystemType + XDocument xmlDoc = XDocument.Parse(processResult.Output); + XElement filesystemTypeValue = xmlDoc.XPathSelectElement("plist/dict/key[text()=\"FilesystemType\"]")?.NextNode as XElement; + result.Add("FileSystemType", filesystemTypeValue != null ? filesystemTypeValue.Value : "Not Found"); + } + catch (XmlException ex) + { + result.Add("DiskUtilError", ex.ToString()); + } + + return result; + } + public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInteractions( PhysicalFileSystem fileSystem, ITracer tracer) @@ -123,8 +123,8 @@ public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInt return new MacProductUpgraderPlatformStrategy(fileSystem, tracer); } - public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) - { + public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) + { string currentUser = this.GetCurrentUser(); MacDaemonController macDaemonController = new MacDaemonController(new ProcessRunnerImpl()); List daemons; @@ -135,39 +135,39 @@ public override void IsServiceInstalledAndRunning(string name, out bool installe } MacDaemonController.DaemonInfo scalarService = daemons.FirstOrDefault(sc => string.Equals(sc.Name, "org.scalar.service")); - installed = scalarService != null; - running = installed && scalarService.IsRunning; - } - - public class MacPlatformConstants : POSIXPlatformConstants - { - public override string InstallerExtension - { - get { return ".dmg"; } - } - - public override string WorkingDirectoryBackingRootPath - { - get { return ScalarConstants.WorkingDirectoryRootName; } - } - - public override string DotScalarRoot - { - get { return MacPlatform.DotScalarRoot; } - } - - public override string ScalarBinDirectoryPath - { - get { return Path.Combine("/usr", "local", this.ScalarBinDirectoryName); } - } - - public override string ScalarBinDirectoryName - { - get { return "scalar"; } - } - - // Documented here (in the addressing section): https://www.unix.com/man-page/mojave/4/unix/ - public override int MaxPipePathLength => 104; - } - } -} + installed = scalarService != null; + running = installed && scalarService.IsRunning; + } + + public class MacPlatformConstants : POSIXPlatformConstants + { + public override string InstallerExtension + { + get { return ".dmg"; } + } + + public override string WorkingDirectoryBackingRootPath + { + get { return ScalarConstants.WorkingDirectoryRootName; } + } + + public override string DotScalarRoot + { + get { return MacPlatform.DotScalarRoot; } + } + + public override string ScalarBinDirectoryPath + { + get { return Path.Combine("/usr", "local", this.ScalarBinDirectoryName); } + } + + public override string ScalarBinDirectoryName + { + get { return "scalar"; } + } + + // Documented here (in the addressing section): https://www.unix.com/man-page/mojave/4/unix/ + public override int MaxPipePathLength => 104; + } + } +} diff --git a/Scalar.Platform.Mac/MacProductUpgraderPlatformStrategy.cs b/Scalar.Platform.Mac/MacProductUpgraderPlatformStrategy.cs index 2fdaf8a98d..fb483ec568 100644 --- a/Scalar.Platform.Mac/MacProductUpgraderPlatformStrategy.cs +++ b/Scalar.Platform.Mac/MacProductUpgraderPlatformStrategy.cs @@ -1,62 +1,62 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; - -namespace Scalar.Platform.Mac -{ - public class MacProductUpgraderPlatformStrategy : ProductUpgraderPlatformStrategy - { - public MacProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) - : base(fileSystem, tracer) - { - } - - public override bool TryPrepareLogDirectory(out string error) - { - error = null; - return true; - } - - public override bool TryPrepareApplicationDirectory(out string error) - { - string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); - - Exception deleteDirectoryException; - if (this.FileSystem.DirectoryExists(upgradeApplicationDirectory) && - !this.FileSystem.TryDeleteDirectory(upgradeApplicationDirectory, out deleteDirectoryException)) - { - error = $"Failed to delete {upgradeApplicationDirectory} - {deleteDirectoryException.Message}"; - - this.TraceException(deleteDirectoryException, nameof(this.TryPrepareApplicationDirectory), $"Error deleting {upgradeApplicationDirectory}."); - return false; - } - - this.FileSystem.CreateDirectory(upgradeApplicationDirectory); - - error = null; - return true; - } - - public override bool TryPrepareDownloadDirectory(out string error) - { - string directory = ProductUpgraderInfo.GetAssetDownloadsPath(); - - Exception deleteDirectoryException; - if (this.FileSystem.DirectoryExists(directory) && - !this.FileSystem.TryDeleteDirectory(directory, out deleteDirectoryException)) - { - error = $"Failed to delete {directory} - {deleteDirectoryException.Message}"; - - this.TraceException(deleteDirectoryException, nameof(this.TryPrepareDownloadDirectory), $"Error deleting {directory}."); - return false; - } - - this.FileSystem.CreateDirectory(directory); - - error = null; - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; + +namespace Scalar.Platform.Mac +{ + public class MacProductUpgraderPlatformStrategy : ProductUpgraderPlatformStrategy + { + public MacProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) + : base(fileSystem, tracer) + { + } + + public override bool TryPrepareLogDirectory(out string error) + { + error = null; + return true; + } + + public override bool TryPrepareApplicationDirectory(out string error) + { + string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); + + Exception deleteDirectoryException; + if (this.FileSystem.DirectoryExists(upgradeApplicationDirectory) && + !this.FileSystem.TryDeleteDirectory(upgradeApplicationDirectory, out deleteDirectoryException)) + { + error = $"Failed to delete {upgradeApplicationDirectory} - {deleteDirectoryException.Message}"; + + this.TraceException(deleteDirectoryException, nameof(this.TryPrepareApplicationDirectory), $"Error deleting {upgradeApplicationDirectory}."); + return false; + } + + this.FileSystem.CreateDirectory(upgradeApplicationDirectory); + + error = null; + return true; + } + + public override bool TryPrepareDownloadDirectory(out string error) + { + string directory = ProductUpgraderInfo.GetAssetDownloadsPath(); + + Exception deleteDirectoryException; + if (this.FileSystem.DirectoryExists(directory) && + !this.FileSystem.TryDeleteDirectory(directory, out deleteDirectoryException)) + { + error = $"Failed to delete {directory} - {deleteDirectoryException.Message}"; + + this.TraceException(deleteDirectoryException, nameof(this.TryPrepareDownloadDirectory), $"Error deleting {directory}."); + return false; + } + + this.FileSystem.CreateDirectory(directory); + + error = null; + return true; + } + } +} diff --git a/Scalar.Platform.Mac/Scalar.Platform.Mac.csproj b/Scalar.Platform.Mac/Scalar.Platform.Mac.csproj index 127ecd5297..d0eeac107b 100644 --- a/Scalar.Platform.Mac/Scalar.Platform.Mac.csproj +++ b/Scalar.Platform.Mac/Scalar.Platform.Mac.csproj @@ -1,32 +1,32 @@ - - - - netcoreapp2.1;netstandard2.0 - x64 - true - true - - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - $(ScalarVersion) - - - - - - - - - - - all - - - + + + + netcoreapp2.1;netstandard2.0 + x64 + true + true + + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + $(ScalarVersion) + + + + + + + + + + + all + + + diff --git a/Scalar.Platform.POSIX/POSIXFileSystem.Shared.cs b/Scalar.Platform.POSIX/POSIXFileSystem.Shared.cs index fb90f96880..f6eb6c75dd 100644 --- a/Scalar.Platform.POSIX/POSIXFileSystem.Shared.cs +++ b/Scalar.Platform.POSIX/POSIXFileSystem.Shared.cs @@ -1,13 +1,13 @@ -namespace Scalar.Platform.POSIX -{ - 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) - errorMessage = null; - normalizedPath = path; - return true; - } - } -} +namespace Scalar.Platform.POSIX +{ + 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) + errorMessage = null; + normalizedPath = path; + return true; + } + } +} diff --git a/Scalar.Platform.POSIX/POSIXFileSystem.cs b/Scalar.Platform.POSIX/POSIXFileSystem.cs index 4626c3db11..6b24148d09 100644 --- a/Scalar.Platform.POSIX/POSIXFileSystem.cs +++ b/Scalar.Platform.POSIX/POSIXFileSystem.cs @@ -1,76 +1,76 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.Platform.POSIX -{ - public abstract partial class POSIXFileSystem : IPlatformFileSystem +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.Platform.POSIX +{ + public abstract partial class POSIXFileSystem : IPlatformFileSystem { // https://github.com/dotnet/corefx/blob/103639b6ff5aa6ab6097f70732530e411817f09b/src/Common/src/CoreLib/Interop/Unix/System.Native/Interop.OpenFlags.cs#L12 - [Flags] - public enum OpenFlags - { - // Access modes (mutually exclusive) - O_RDONLY = 0x0000, - O_WRONLY = 0x0001, - O_RDWR = 0x0002, - - // Flags (combinable) - O_CLOEXEC = 0x0010, - O_CREAT = 0x0020, - O_EXCL = 0x0040, - O_TRUNC = 0x0080, - O_SYNC = 0x0100, - } - - public bool SupportsFileMode { get; } = true; - - public void FlushFileBuffers(string path) - { - // TODO(#1057): Use native API to flush file - } - - public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - if (Rename(sourceFileName, destinationFilename) != 0) - { - NativeMethods.ThrowLastWin32Exception($"Failed to rename {sourceFileName} to {destinationFilename}"); - } - } - - public abstract void ChangeMode(string path, ushort mode); - - public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) - { - return POSIXFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); - } - - public abstract bool HydrateFile(string fileName, byte[] buffer); - - public abstract bool IsExecutable(string fileName); - - public abstract bool IsSocket(string fileName); - - public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) - { - throw new NotImplementedException(); - } - - public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) - { - throw new NotImplementedException(); - } - - public virtual bool IsFileSystemSupported(string path, out string error) - { - error = null; - return true; - } + [Flags] + public enum OpenFlags + { + // Access modes (mutually exclusive) + O_RDONLY = 0x0000, + O_WRONLY = 0x0001, + O_RDWR = 0x0002, + + // Flags (combinable) + O_CLOEXEC = 0x0010, + O_CREAT = 0x0020, + O_EXCL = 0x0040, + O_TRUNC = 0x0080, + O_SYNC = 0x0100, + } + + public bool SupportsFileMode { get; } = true; + + public void FlushFileBuffers(string path) + { + // TODO(#1057): Use native API to flush file + } + + public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + if (Rename(sourceFileName, destinationFilename) != 0) + { + NativeMethods.ThrowLastWin32Exception($"Failed to rename {sourceFileName} to {destinationFilename}"); + } + } + + public abstract void ChangeMode(string path, ushort mode); + + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) + { + return POSIXFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); + } + + public abstract bool HydrateFile(string fileName, byte[] buffer); + + public abstract bool IsExecutable(string fileName); + + public abstract bool IsSocket(string fileName); + + public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) + { + throw new NotImplementedException(); + } + + public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) + { + throw new NotImplementedException(); + } + + public virtual bool IsFileSystemSupported(string path, out string error) + { + error = null; + return true; + } - [DllImport("libc", EntryPoint = "rename", SetLastError = true)] - private static extern int Rename(string oldPath, string newPath); - } -} + [DllImport("libc", EntryPoint = "rename", SetLastError = true)] + private static extern int Rename(string oldPath, string newPath); + } +} diff --git a/Scalar.Platform.POSIX/POSIXGitInstallation.cs b/Scalar.Platform.POSIX/POSIXGitInstallation.cs index efb9e78cce..4cd9738e87 100644 --- a/Scalar.Platform.POSIX/POSIXGitInstallation.cs +++ b/Scalar.Platform.POSIX/POSIXGitInstallation.cs @@ -1,32 +1,32 @@ -using Scalar.Common; -using Scalar.Common.Git; -using System.IO; - -namespace Scalar.Platform.POSIX -{ - public class POSIXGitInstallation : IGitInstallation - { - private const string GitProcessName = "git"; - - public bool GitExists(string gitBinPath) - { - if (!string.IsNullOrWhiteSpace(gitBinPath)) - { - return File.Exists(gitBinPath); - } - - return this.GetInstalledGitBinPath() != null; - } - - public string GetInstalledGitBinPath() - { - ProcessResult result = ProcessHelper.Run("which", args: "git", redirectOutput: true); - if (result.ExitCode != 0) - { - return null; - } - - return result.Output.Trim(); - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using System.IO; + +namespace Scalar.Platform.POSIX +{ + public class POSIXGitInstallation : IGitInstallation + { + private const string GitProcessName = "git"; + + public bool GitExists(string gitBinPath) + { + if (!string.IsNullOrWhiteSpace(gitBinPath)) + { + return File.Exists(gitBinPath); + } + + return this.GetInstalledGitBinPath() != null; + } + + public string GetInstalledGitBinPath() + { + ProcessResult result = ProcessHelper.Run("which", args: "git", redirectOutput: true); + if (result.ExitCode != 0) + { + return null; + } + + return result.Output.Trim(); + } + } +} diff --git a/Scalar.Platform.POSIX/POSIXPlatform.Shared.cs b/Scalar.Platform.POSIX/POSIXPlatform.Shared.cs index a85216f4c3..2560783b7b 100644 --- a/Scalar.Platform.POSIX/POSIXPlatform.Shared.cs +++ b/Scalar.Platform.POSIX/POSIXPlatform.Shared.cs @@ -1,66 +1,66 @@ -using Scalar.Common; +using Scalar.Common; using System; using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; - -namespace Scalar.Platform.POSIX -{ - public abstract partial class POSIXPlatform - { - public static bool IsElevatedImplementation() - { - int euid = GetEuid(); - return euid == 0; - } - - public static bool IsProcessActiveImplementation(int processId) - { - try +using System.IO; +using System.Runtime.InteropServices; + +namespace Scalar.Platform.POSIX +{ + public abstract partial class POSIXPlatform + { + public static bool IsElevatedImplementation() + { + int euid = GetEuid(); + return euid == 0; + } + + public static bool IsProcessActiveImplementation(int processId) + { + try { Process process = Process.GetProcessById(processId); - } - catch (ArgumentException) + } + catch (ArgumentException) { return false; - } - - return true; - } - - public static string GetNamedPipeNameImplementation(string enlistmentRoot, string dotScalarRoot) - { - // Pipes are stored as files on POSIX, use a rooted pipe name to keep full control of the location of the file - return Path.Combine(enlistmentRoot, dotScalarRoot, "Scalar_NetCorePipe"); - } - - public static bool IsConsoleOutputRedirectedToFileImplementation() - { - // TODO(#1355): Implement proper check - return false; - } - - public static bool TryGetScalarEnlistmentRootImplementation(string directory, string dotScalarRoot, out string enlistmentRoot, out string errorMessage) - { - enlistmentRoot = null; - - string finalDirectory; - if (!POSIXFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage)) - { - return false; - } - - enlistmentRoot = Paths.GetRoot(finalDirectory, dotScalarRoot); - if (enlistmentRoot == null) - { - errorMessage = $"Failed to find the root directory for {dotScalarRoot} in {finalDirectory}"; - return false; - } - - return true; - } - - [DllImport("libc", EntryPoint = "geteuid", SetLastError = true)] - private static extern int GetEuid(); - } -} + } + + return true; + } + + public static string GetNamedPipeNameImplementation(string enlistmentRoot, string dotScalarRoot) + { + // Pipes are stored as files on POSIX, use a rooted pipe name to keep full control of the location of the file + return Path.Combine(enlistmentRoot, dotScalarRoot, "Scalar_NetCorePipe"); + } + + public static bool IsConsoleOutputRedirectedToFileImplementation() + { + // TODO(#1355): Implement proper check + return false; + } + + public static bool TryGetScalarEnlistmentRootImplementation(string directory, string dotScalarRoot, out string enlistmentRoot, out string errorMessage) + { + enlistmentRoot = null; + + string finalDirectory; + if (!POSIXFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage)) + { + return false; + } + + enlistmentRoot = Paths.GetRoot(finalDirectory, dotScalarRoot); + if (enlistmentRoot == null) + { + errorMessage = $"Failed to find the root directory for {dotScalarRoot} in {finalDirectory}"; + return false; + } + + return true; + } + + [DllImport("libc", EntryPoint = "geteuid", SetLastError = true)] + private static extern int GetEuid(); + } +} diff --git a/Scalar.Platform.POSIX/POSIXPlatform.cs b/Scalar.Platform.POSIX/POSIXPlatform.cs index c03ed2fc28..2572eea681 100644 --- a/Scalar.Platform.POSIX/POSIXPlatform.cs +++ b/Scalar.Platform.POSIX/POSIXPlatform.cs @@ -1,105 +1,105 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; -using System.IO; -using System.IO.Pipes; +using System.IO; +using System.IO.Pipes; using System.Linq; -using System.Runtime.InteropServices; -using System.Security; - -namespace Scalar.Platform.POSIX -{ - public abstract partial class POSIXPlatform : ScalarPlatform +using System.Runtime.InteropServices; +using System.Security; + +namespace Scalar.Platform.POSIX +{ + public abstract partial class POSIXPlatform : ScalarPlatform { - private const int StdInFileNo = 0; // STDIN_FILENO -> standard input file descriptor - private const int StdOutFileNo = 1; // STDOUT_FILENO -> standard output file descriptor - private const int StdErrFileNo = 2; // STDERR_FILENO -> standard error file descriptor - - protected POSIXPlatform() : this( - underConstruction: new UnderConstructionFlags( - supportsScalarUpgrade: false, + private const int StdInFileNo = 0; // STDIN_FILENO -> standard input file descriptor + private const int StdOutFileNo = 1; // STDOUT_FILENO -> standard output file descriptor + private const int StdErrFileNo = 2; // STDERR_FILENO -> standard error file descriptor + + protected POSIXPlatform() : this( + underConstruction: new UnderConstructionFlags( + supportsScalarUpgrade: false, supportsScalarConfig: false, - supportsNuGetEncryption: false)) - { - } - - protected POSIXPlatform(UnderConstructionFlags underConstruction) - : base(underConstruction) - { - } - - public override IGitInstallation GitInstallation { get; } = new POSIXGitInstallation(); - - public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) - { - } - - public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) - { - throw new NotImplementedException(); - } - - public override bool IsProcessActive(int processId) - { - return POSIXPlatform.IsProcessActiveImplementation(processId); - } - - public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) - { - throw new NotImplementedException(); - } - - public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) - { - string programArguments = string.Empty; - try - { - programArguments = string.Join(" ", args.Select(arg => arg.Contains(' ') ? "\"" + arg + "\"" : arg)); - ProcessStartInfo processInfo = new ProcessStartInfo(programName, programArguments); - - // Redirecting stdin/out/err ensures that all standard input/output file descriptors are properly closed - // by dup2 before execve is called for the child process - // (see https://github.com/dotnet/corefx/blob/b10e8d67b260e26f2e47750cf96669e6f48e774d/src/Native/Unix/System.Native/pal_process.c#L381) - // - // Testing has shown that without redirecting stdin/err/out code like this: - // - // string result = process.StandardOutput.ReadToEnd(); - // process.WaitForExit(); - // - // That waits on a `scalar` verb to exit can hang in the WaitForExit() call because the chuild process has inheritied - // standard input/output handle(s), and redirecting those streams before spawing the process appears to be the only - // way to ensure they're properly closed. - // - // Note that this approach requires that the child process know that it needs to redirect its standard input/output to /dev/null and - // so this method can only be used with Scalar processes that are aware they're being launched in the background - processInfo.RedirectStandardError = true; - processInfo.RedirectStandardInput = true; - processInfo.RedirectStandardOutput = true; - - Process executingProcess = new Process(); - executingProcess.StartInfo = processInfo; - executingProcess.Start(); - } - catch (Exception ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(programName), programName); - metadata.Add(nameof(programArguments), programArguments); - metadata.Add("Exception", ex.ToString()); - tracer.RelatedError(metadata, "Failed to start background process."); - throw; - } - } - + supportsNuGetEncryption: false)) + { + } + + protected POSIXPlatform(UnderConstructionFlags underConstruction) + : base(underConstruction) + { + } + + public override IGitInstallation GitInstallation { get; } = new POSIXGitInstallation(); + + public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) + { + } + + public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) + { + throw new NotImplementedException(); + } + + public override bool IsProcessActive(int processId) + { + return POSIXPlatform.IsProcessActiveImplementation(processId); + } + + public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) + { + throw new NotImplementedException(); + } + + public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) + { + string programArguments = string.Empty; + try + { + programArguments = string.Join(" ", args.Select(arg => arg.Contains(' ') ? "\"" + arg + "\"" : arg)); + ProcessStartInfo processInfo = new ProcessStartInfo(programName, programArguments); + + // Redirecting stdin/out/err ensures that all standard input/output file descriptors are properly closed + // by dup2 before execve is called for the child process + // (see https://github.com/dotnet/corefx/blob/b10e8d67b260e26f2e47750cf96669e6f48e774d/src/Native/Unix/System.Native/pal_process.c#L381) + // + // Testing has shown that without redirecting stdin/err/out code like this: + // + // string result = process.StandardOutput.ReadToEnd(); + // process.WaitForExit(); + // + // That waits on a `scalar` verb to exit can hang in the WaitForExit() call because the chuild process has inheritied + // standard input/output handle(s), and redirecting those streams before spawing the process appears to be the only + // way to ensure they're properly closed. + // + // Note that this approach requires that the child process know that it needs to redirect its standard input/output to /dev/null and + // so this method can only be used with Scalar processes that are aware they're being launched in the background + processInfo.RedirectStandardError = true; + processInfo.RedirectStandardInput = true; + processInfo.RedirectStandardOutput = true; + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + executingProcess.Start(); + } + catch (Exception ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(programName), programName); + metadata.Add(nameof(programArguments), programArguments); + metadata.Add("Exception", ex.ToString()); + tracer.RelatedError(metadata, "Failed to start background process."); + throw; + } + } + public override void PrepareProcessToRunInBackground() { int devNullIn = Open("/dev/null", (int)POSIXFileSystem.OpenFlags.O_RDONLY); if (devNullIn == -1) - { + { throw new Win32Exception(Marshal.GetLastWin32Error(), "Unable to open /dev/null for reading"); } @@ -113,159 +113,159 @@ public override void PrepareProcessToRunInBackground() if (Dup2(devNullIn, StdInFileNo) == -1 || Dup2(devNullOut, StdOutFileNo) == -1 || Dup2(devNullOut, StdErrFileNo) == -1) - { + { throw new Win32Exception(Marshal.GetLastWin32Error(), "Error redirecting stdout/stdin/stderr to /dev/null"); - } - - Close(devNullIn); + } + + Close(devNullIn); Close(devNullOut); // Become session leader of a new session if (SetSid() == -1) - { + { throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to become session leader"); } - } - - public override NamedPipeServerStream CreatePipeByName(string pipeName) - { - NamedPipeServerStream pipe = new NamedPipeServerStream( - pipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, - PipeOptions.WriteThrough | PipeOptions.Asynchronous, - 0, // default inBufferSize - 0); // default outBufferSize) - - return pipe; - } - - public override string GetCurrentUser() - { - return Getuid().ToString(); - } - - public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) - { - // There are no separate User and Session Ids on POSIX platforms. - return sessionId.ToString(); - } - - public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) - { - // TODO(#1356): Collect disk information - Dictionary result = new Dictionary(); - result.Add("GetPhysicalDiskInfo", "Not yet implemented on POSIX"); - return result; - } - - public override void InitializeEnlistmentACLs(string enlistmentPath) - { - } - - public override string GetScalarServiceNamedPipeName(string serviceName) - { - // Pipes are stored as files on POSIX, use a rooted pipe name - // in the same location as the service to keep full control of the location of the file - return this.GetDataRootForScalarComponent(serviceName) + ".pipe"; - } - - public override bool IsConsoleOutputRedirectedToFile() - { - return POSIXPlatform.IsConsoleOutputRedirectedToFileImplementation(); - } - - public override bool IsElevated() - { - return POSIXPlatform.IsElevatedImplementation(); - } - - public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError) - { - string homeDirectory; - + } + + public override NamedPipeServerStream CreatePipeByName(string pipeName) + { + NamedPipeServerStream pipe = new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.WriteThrough | PipeOptions.Asynchronous, + 0, // default inBufferSize + 0); // default outBufferSize) + + return pipe; + } + + public override string GetCurrentUser() + { + return Getuid().ToString(); + } + + public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) + { + // There are no separate User and Session Ids on POSIX platforms. + return sessionId.ToString(); + } + + public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) + { + // TODO(#1356): Collect disk information + Dictionary result = new Dictionary(); + result.Add("GetPhysicalDiskInfo", "Not yet implemented on POSIX"); + return result; + } + + public override void InitializeEnlistmentACLs(string enlistmentPath) + { + } + + public override string GetScalarServiceNamedPipeName(string serviceName) + { + // Pipes are stored as files on POSIX, use a rooted pipe name + // in the same location as the service to keep full control of the location of the file + return this.GetDataRootForScalarComponent(serviceName) + ".pipe"; + } + + public override bool IsConsoleOutputRedirectedToFile() + { + return POSIXPlatform.IsConsoleOutputRedirectedToFileImplementation(); + } + + public override bool IsElevated() + { + return POSIXPlatform.IsElevatedImplementation(); + } + + public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError) + { + string homeDirectory; + try - { + { homeDirectory = Environment.GetEnvironmentVariable("HOME"); - } - catch (SecurityException e) - { - localCacheRoot = null; - localCacheRootError = $"Failed to read $HOME, insufficient permission: {e.Message}"; + } + catch (SecurityException e) + { + localCacheRoot = null; + localCacheRootError = $"Failed to read $HOME, insufficient permission: {e.Message}"; + return false; + } + + if (string.IsNullOrEmpty(homeDirectory)) + { + localCacheRoot = null; + localCacheRootError = "$HOME empty or not found"; + return false; + } + + try + { + localCacheRoot = Path.Combine(homeDirectory, ScalarConstants.DefaultScalarCacheFolderName); + localCacheRootError = null; + return true; + } + catch (ArgumentException e) + { + localCacheRoot = null; + localCacheRootError = $"Failed to build local cache path using $HOME('{homeDirectory}'): {e.Message}"; return false; - } - - if (string.IsNullOrEmpty(homeDirectory)) - { - localCacheRoot = null; - localCacheRootError = "$HOME empty or not found"; - return false; - } - - try - { - localCacheRoot = Path.Combine(homeDirectory, ScalarConstants.DefaultScalarCacheFolderName); - localCacheRootError = null; - return true; - } - catch (ArgumentException e) - { - localCacheRoot = null; - localCacheRootError = $"Failed to build local cache path using $HOME('{homeDirectory}'): {e.Message}"; - return false; - } - } - - public override bool TryKillProcessTree(int processId, out int exitCode, out string error) - { - ProcessResult result = ProcessHelper.Run("pkill", $"-P {processId}"); - error = result.Errors; - exitCode = result.ExitCode; - return result.ExitCode == 0; - } - - [DllImport("libc", EntryPoint = "getuid", SetLastError = true)] + } + } + + public override bool TryKillProcessTree(int processId, out int exitCode, out string error) + { + ProcessResult result = ProcessHelper.Run("pkill", $"-P {processId}"); + error = result.Errors; + exitCode = result.ExitCode; + return result.ExitCode == 0; + } + + [DllImport("libc", EntryPoint = "getuid", SetLastError = true)] private static extern uint Getuid(); - [DllImport("libc", EntryPoint = "setsid", SetLastError = true)] - private static extern int SetSid(); - - [DllImport("libc", EntryPoint = "open", SetLastError = true)] - private static extern int Open(string path, int flag); - - [DllImport("libc", EntryPoint = "close", SetLastError = true)] - private static extern int Close(int filedes); - - [DllImport("libc", EntryPoint = "dup2", SetLastError = true)] - private static extern int Dup2(int oldfd, int newfd); - - public abstract class POSIXPlatformConstants : ScalarPlatformConstants - { - public override string ExecutableExtension - { - get { return string.Empty; } - } - + [DllImport("libc", EntryPoint = "setsid", SetLastError = true)] + private static extern int SetSid(); + + [DllImport("libc", EntryPoint = "open", SetLastError = true)] + private static extern int Open(string path, int flag); + + [DllImport("libc", EntryPoint = "close", SetLastError = true)] + private static extern int Close(int filedes); + + [DllImport("libc", EntryPoint = "dup2", SetLastError = true)] + private static extern int Dup2(int oldfd, int newfd); + + public abstract class POSIXPlatformConstants : ScalarPlatformConstants + { + public override string ExecutableExtension + { + get { return string.Empty; } + } + public override string ScalarExecutableName { get { return "scalar"; } - } - - public override string ProgramLocaterCommand - { - get { return "which"; } - } - - public override HashSet UpgradeBlockingProcesses - { - get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar.Mount", "git", "wish" }; } - } - - public override bool SupportsUpgradeWhileRunning => true; - - // Documented here (in the addressing section): https://www.unix.com/man-page/linux/7/unix/ - public override int MaxPipePathLength => 108; - } - } -} + } + + public override string ProgramLocaterCommand + { + get { return "which"; } + } + + public override HashSet UpgradeBlockingProcesses + { + get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar.Mount", "git", "wish" }; } + } + + public override bool SupportsUpgradeWhileRunning => true; + + // Documented here (in the addressing section): https://www.unix.com/man-page/linux/7/unix/ + public override int MaxPipePathLength => 108; + } + } +} diff --git a/Scalar.Platform.Windows/CurrentUser.cs b/Scalar.Platform.Windows/CurrentUser.cs index 708724920d..a47529b11a 100644 --- a/Scalar.Platform.Windows/CurrentUser.cs +++ b/Scalar.Platform.Windows/CurrentUser.cs @@ -1,377 +1,377 @@ -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Runtime.InteropServices; -using System.Security.Principal; - -namespace Scalar.Platform.Windows -{ - public class CurrentUser : IDisposable - { - private const int TokenPrimary = 1; - - private const uint DuplicateTokenFlags = (uint)(TokenRights.Query | TokenRights.AssignPrimary | TokenRights.Duplicate | TokenRights.Default | TokenRights.SessionId); - - private const int StartInfoUseStdHandles = 0x00000100; - private const uint HandleFlagInherit = 1; - - private readonly ITracer tracer; - private readonly IntPtr token; - - public CurrentUser(ITracer tracer, int sessionId) - { - this.tracer = tracer; - this.token = GetCurrentUserToken(this.tracer, sessionId); - if (this.token != IntPtr.Zero) - { - this.Identity = new WindowsIdentity(this.token); - } - else - { - this.Identity = null; - } - } - - private enum TokenRights : uint - { - StandardRightsRequired = 0x000F0000, - StandardRightsRead = 0x00020000, - AssignPrimary = 0x0001, - Duplicate = 0x0002, - TokenImpersonate = 0x0004, - Query = 0x0008, - QuerySource = 0x0010, - AdjustPrivileges = 0x0020, - AdjustGroups = 0x0040, - Default = 0x0080, - SessionId = 0x0100, - Read = (StandardRightsRead | Query), - AllAccess = (StandardRightsRequired | AssignPrimary | - Duplicate | TokenImpersonate | Query | QuerySource | - AdjustPrivileges | AdjustGroups | Default | - SessionId), - } - - private enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - private enum WaitForObjectResults : uint - { - WaitSuccess = 0, - WaitAbandoned = 0x80, - WaitTimeout = 0x102, - WaitFailed = 0xFFFFFFFF - } - - private enum ConnectionState - { - Active, - Connected, - ConnectQuery, - Shadowing, - Disconnected, - Idle, - Listening, - Reset, - Down, - Initializing - } - - [Flags] - private enum CreateProcessFlags : uint - { - CREATE_BREAKAWAY_FROM_JOB = 0x01000000, - CREATE_DEFAULT_ERROR_MODE = 0x04000000, - CREATE_NEW_CONSOLE = 0x00000010, - CREATE_NEW_PROCESS_GROUP = 0x00000200, - CREATE_NO_WINDOW = 0x08000000, - CREATE_PROTECTED_PROCESS = 0x00040000, - CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, - CREATE_SEPARATE_WOW_VDM = 0x00000800, - CREATE_SHARED_WOW_VDM = 0x00001000, - CREATE_SUSPENDED = 0x00000004, - CREATE_UNICODE_ENVIRONMENT = 0x00000400, - DEBUG_ONLY_THIS_PROCESS = 0x00000002, - DEBUG_PROCESS = 0x00000001, - DETACHED_PROCESS = 0x00000008, - EXTENDED_STARTUPINFO_PRESENT = 0x00080000, - INHERIT_PARENT_AFFINITY = 0x00010000 - } - - public WindowsIdentity Identity { get; } - - /// - /// Launches a process for the current user. - /// This code will only work when running in a windows service running as LocalSystem - /// with the SE_TCB_NAME privilege. - /// - /// True on successful process start - public bool RunAs(string processName, string args) - { - IntPtr environment = IntPtr.Zero; - IntPtr duplicate = IntPtr.Zero; - if (this.token == IntPtr.Zero) - { - return false; - } - - try - { - if (DuplicateTokenEx( - this.token, - DuplicateTokenFlags, - IntPtr.Zero, - SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, - TokenPrimary, - out duplicate)) - { - if (CreateEnvironmentBlock(ref environment, duplicate, false)) - { - STARTUP_INFO info = new STARTUP_INFO(); - info.Length = Marshal.SizeOf(typeof(STARTUP_INFO)); - - PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION(); - if (CreateProcessAsUser( - duplicate, - null, - string.Format("\"{0}\" {1}", processName, args), - IntPtr.Zero, - IntPtr.Zero, - inheritHandles: false, - creationFlags: CreateProcessFlags.CREATE_NO_WINDOW | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT, - environment: environment, - currentDirectory: null, - startupInfo: ref info, - processInformation: out procInfo)) - { - try - { - this.tracer.RelatedInfo("Started process '{0} {1}' with Id {2}", processName, args, procInfo.ProcessId); - } - finally - { - CloseHandle(procInfo.ProcessHandle); - CloseHandle(procInfo.ThreadHandle); - } - - return true; - } - else - { - TraceWin32Error(this.tracer, "Unable to start process."); - } - } - else - { - TraceWin32Error(this.tracer, "Unable to set child process environment block."); - } - } - else - { - TraceWin32Error(this.tracer, "Unable to duplicate user token."); - } - } - finally - { - if (environment != IntPtr.Zero) - { - DestroyEnvironmentBlock(environment); - } - - if (duplicate != IntPtr.Zero) - { - CloseHandle(duplicate); - } - } - - return false; - } - - public void Dispose() - { - if (this.token != IntPtr.Zero) - { - CloseHandle(this.token); - } - } - - private static void TraceWin32Error(ITracer tracer, string preface) - { - Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error()); - tracer.RelatedError(preface + " Exception: " + e.Message); - } - - private static IntPtr GetCurrentUserToken(ITracer tracer, int sessionId) - { - IntPtr output = IntPtr.Zero; - if (WTSQueryUserToken((uint)sessionId, out output)) - { - return output; - } - else - { - TraceWin32Error(tracer, string.Format("Unable to query user token from session '{0}'.", sessionId)); - } - - return IntPtr.Zero; - } - - private static List ListSessions(ITracer tracer) - { - IntPtr sessionInfo = IntPtr.Zero; - IntPtr server = IntPtr.Zero; - List output = new List(); - - try - { - int count = 0; - int retval = WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref sessionInfo, ref count); - if (retval != 0) - { - int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); - long current = sessionInfo.ToInt64(); - - for (int i = 0; i < count; i++) - { - WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO)); - current += dataSize; - - output.Add(si); - } - } - else - { - TraceWin32Error(tracer, "Unable to enumerate sessions on the current host."); - } - } - catch (Exception exception) - { - output.Clear(); - tracer.RelatedError(exception.ToString()); - } - finally - { - if (sessionInfo != IntPtr.Zero) - { - WTSFreeMemory(sessionInfo); - } - } - - return output; - } - - [DllImport("kernel32.dll")] - private static extern bool CloseHandle(IntPtr handle); - - [DllImport("kernel32.dll")] - private static extern WaitForObjectResults WaitForSingleObject(IntPtr handle, uint timeout = uint.MaxValue); - - [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern int WTSEnumerateSessions(IntPtr server, int reserved, int version, ref IntPtr sessionInfo, ref int count); - - [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUserW", SetLastError = true, CharSet = CharSet.Auto)] - private static extern bool CreateProcessAsUser( - IntPtr token, - string applicationName, - string commandLine, - IntPtr processAttributes, - IntPtr threadAttributes, - bool inheritHandles, - CreateProcessFlags creationFlags, - IntPtr environment, - string currentDirectory, - ref STARTUP_INFO startupInfo, - out PROCESS_INFORMATION processInformation); - - [DllImport("wtsapi32.dll")] - private static extern void WTSFreeMemory(IntPtr memory); - - [DllImport("wtsapi32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token); - - [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern bool DuplicateTokenEx( - IntPtr existingToken, - uint desiredAccess, - IntPtr tokenAttributes, - SECURITY_IMPERSONATION_LEVEL impersonationLevel, - int tokenType, - out IntPtr newToken); - - [DllImport("userenv.dll", SetLastError = true)] - private static extern bool CreateEnvironmentBlock(ref IntPtr environment, IntPtr token, bool inherit); - - [DllImport("userenv.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool DestroyEnvironmentBlock(IntPtr environment); - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - private struct STARTUP_INFO - { - public int Length; - public string Reserved; - public string DesktopName; - public string Title; - public int WindowX; - public int WindowY; - public int WindowWidth; - public int WindowHeight; - public int ConsoleBufferWidth; - public int ConsoleBufferHeight; - public int ConsoleColors; - public int Flags; - public short ShowWindow; - public short Reserved2; - public IntPtr Reserved3; - public IntPtr StdInput; - public IntPtr StdOutput; - public IntPtr StdError; - } - - [StructLayout(LayoutKind.Sequential)] - private struct SECURITY_ATTRIBUTES - { - public int Length; - public IntPtr SecurityDescriptor; - public bool InheritHandle; - } - - [StructLayoutAttribute(LayoutKind.Sequential)] - private struct SECURITY_DESCRIPTOR - { - public byte Revision; - public byte Size; - public short Control; - public IntPtr Owner; - public IntPtr Group; - public IntPtr Sacl; - public IntPtr Dacl; - } - - [StructLayout(LayoutKind.Sequential)] - private struct PROCESS_INFORMATION - { - public IntPtr ProcessHandle; - public IntPtr ThreadHandle; - public int ProcessId; - public int ThreadId; - } - - [StructLayout(LayoutKind.Sequential)] - private struct WTS_SESSION_INFO - { - public int SessionID; - - [MarshalAs(UnmanagedType.LPTStr)] - public string WinStationName; - public ConnectionState State; - } - } +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace Scalar.Platform.Windows +{ + public class CurrentUser : IDisposable + { + private const int TokenPrimary = 1; + + private const uint DuplicateTokenFlags = (uint)(TokenRights.Query | TokenRights.AssignPrimary | TokenRights.Duplicate | TokenRights.Default | TokenRights.SessionId); + + private const int StartInfoUseStdHandles = 0x00000100; + private const uint HandleFlagInherit = 1; + + private readonly ITracer tracer; + private readonly IntPtr token; + + public CurrentUser(ITracer tracer, int sessionId) + { + this.tracer = tracer; + this.token = GetCurrentUserToken(this.tracer, sessionId); + if (this.token != IntPtr.Zero) + { + this.Identity = new WindowsIdentity(this.token); + } + else + { + this.Identity = null; + } + } + + private enum TokenRights : uint + { + StandardRightsRequired = 0x000F0000, + StandardRightsRead = 0x00020000, + AssignPrimary = 0x0001, + Duplicate = 0x0002, + TokenImpersonate = 0x0004, + Query = 0x0008, + QuerySource = 0x0010, + AdjustPrivileges = 0x0020, + AdjustGroups = 0x0040, + Default = 0x0080, + SessionId = 0x0100, + Read = (StandardRightsRead | Query), + AllAccess = (StandardRightsRequired | AssignPrimary | + Duplicate | TokenImpersonate | Query | QuerySource | + AdjustPrivileges | AdjustGroups | Default | + SessionId), + } + + private enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + private enum WaitForObjectResults : uint + { + WaitSuccess = 0, + WaitAbandoned = 0x80, + WaitTimeout = 0x102, + WaitFailed = 0xFFFFFFFF + } + + private enum ConnectionState + { + Active, + Connected, + ConnectQuery, + Shadowing, + Disconnected, + Idle, + Listening, + Reset, + Down, + Initializing + } + + [Flags] + private enum CreateProcessFlags : uint + { + CREATE_BREAKAWAY_FROM_JOB = 0x01000000, + CREATE_DEFAULT_ERROR_MODE = 0x04000000, + CREATE_NEW_CONSOLE = 0x00000010, + CREATE_NEW_PROCESS_GROUP = 0x00000200, + CREATE_NO_WINDOW = 0x08000000, + CREATE_PROTECTED_PROCESS = 0x00040000, + CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, + CREATE_SEPARATE_WOW_VDM = 0x00000800, + CREATE_SHARED_WOW_VDM = 0x00001000, + CREATE_SUSPENDED = 0x00000004, + CREATE_UNICODE_ENVIRONMENT = 0x00000400, + DEBUG_ONLY_THIS_PROCESS = 0x00000002, + DEBUG_PROCESS = 0x00000001, + DETACHED_PROCESS = 0x00000008, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000, + INHERIT_PARENT_AFFINITY = 0x00010000 + } + + public WindowsIdentity Identity { get; } + + /// + /// Launches a process for the current user. + /// This code will only work when running in a windows service running as LocalSystem + /// with the SE_TCB_NAME privilege. + /// + /// True on successful process start + public bool RunAs(string processName, string args) + { + IntPtr environment = IntPtr.Zero; + IntPtr duplicate = IntPtr.Zero; + if (this.token == IntPtr.Zero) + { + return false; + } + + try + { + if (DuplicateTokenEx( + this.token, + DuplicateTokenFlags, + IntPtr.Zero, + SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, + TokenPrimary, + out duplicate)) + { + if (CreateEnvironmentBlock(ref environment, duplicate, false)) + { + STARTUP_INFO info = new STARTUP_INFO(); + info.Length = Marshal.SizeOf(typeof(STARTUP_INFO)); + + PROCESS_INFORMATION procInfo = new PROCESS_INFORMATION(); + if (CreateProcessAsUser( + duplicate, + null, + string.Format("\"{0}\" {1}", processName, args), + IntPtr.Zero, + IntPtr.Zero, + inheritHandles: false, + creationFlags: CreateProcessFlags.CREATE_NO_WINDOW | CreateProcessFlags.CREATE_UNICODE_ENVIRONMENT, + environment: environment, + currentDirectory: null, + startupInfo: ref info, + processInformation: out procInfo)) + { + try + { + this.tracer.RelatedInfo("Started process '{0} {1}' with Id {2}", processName, args, procInfo.ProcessId); + } + finally + { + CloseHandle(procInfo.ProcessHandle); + CloseHandle(procInfo.ThreadHandle); + } + + return true; + } + else + { + TraceWin32Error(this.tracer, "Unable to start process."); + } + } + else + { + TraceWin32Error(this.tracer, "Unable to set child process environment block."); + } + } + else + { + TraceWin32Error(this.tracer, "Unable to duplicate user token."); + } + } + finally + { + if (environment != IntPtr.Zero) + { + DestroyEnvironmentBlock(environment); + } + + if (duplicate != IntPtr.Zero) + { + CloseHandle(duplicate); + } + } + + return false; + } + + public void Dispose() + { + if (this.token != IntPtr.Zero) + { + CloseHandle(this.token); + } + } + + private static void TraceWin32Error(ITracer tracer, string preface) + { + Win32Exception e = new Win32Exception(Marshal.GetLastWin32Error()); + tracer.RelatedError(preface + " Exception: " + e.Message); + } + + private static IntPtr GetCurrentUserToken(ITracer tracer, int sessionId) + { + IntPtr output = IntPtr.Zero; + if (WTSQueryUserToken((uint)sessionId, out output)) + { + return output; + } + else + { + TraceWin32Error(tracer, string.Format("Unable to query user token from session '{0}'.", sessionId)); + } + + return IntPtr.Zero; + } + + private static List ListSessions(ITracer tracer) + { + IntPtr sessionInfo = IntPtr.Zero; + IntPtr server = IntPtr.Zero; + List output = new List(); + + try + { + int count = 0; + int retval = WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref sessionInfo, ref count); + if (retval != 0) + { + int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); + long current = sessionInfo.ToInt64(); + + for (int i = 0; i < count; i++) + { + WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((IntPtr)current, typeof(WTS_SESSION_INFO)); + current += dataSize; + + output.Add(si); + } + } + else + { + TraceWin32Error(tracer, "Unable to enumerate sessions on the current host."); + } + } + catch (Exception exception) + { + output.Clear(); + tracer.RelatedError(exception.ToString()); + } + finally + { + if (sessionInfo != IntPtr.Zero) + { + WTSFreeMemory(sessionInfo); + } + } + + return output; + } + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle(IntPtr handle); + + [DllImport("kernel32.dll")] + private static extern WaitForObjectResults WaitForSingleObject(IntPtr handle, uint timeout = uint.MaxValue); + + [DllImport("wtsapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern int WTSEnumerateSessions(IntPtr server, int reserved, int version, ref IntPtr sessionInfo, ref int count); + + [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUserW", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateProcessAsUser( + IntPtr token, + string applicationName, + string commandLine, + IntPtr processAttributes, + IntPtr threadAttributes, + bool inheritHandles, + CreateProcessFlags creationFlags, + IntPtr environment, + string currentDirectory, + ref STARTUP_INFO startupInfo, + out PROCESS_INFORMATION processInformation); + + [DllImport("wtsapi32.dll")] + private static extern void WTSFreeMemory(IntPtr memory); + + [DllImport("wtsapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr token); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool DuplicateTokenEx( + IntPtr existingToken, + uint desiredAccess, + IntPtr tokenAttributes, + SECURITY_IMPERSONATION_LEVEL impersonationLevel, + int tokenType, + out IntPtr newToken); + + [DllImport("userenv.dll", SetLastError = true)] + private static extern bool CreateEnvironmentBlock(ref IntPtr environment, IntPtr token, bool inherit); + + [DllImport("userenv.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool DestroyEnvironmentBlock(IntPtr environment); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUP_INFO + { + public int Length; + public string Reserved; + public string DesktopName; + public string Title; + public int WindowX; + public int WindowY; + public int WindowWidth; + public int WindowHeight; + public int ConsoleBufferWidth; + public int ConsoleBufferHeight; + public int ConsoleColors; + public int Flags; + public short ShowWindow; + public short Reserved2; + public IntPtr Reserved3; + public IntPtr StdInput; + public IntPtr StdOutput; + public IntPtr StdError; + } + + [StructLayout(LayoutKind.Sequential)] + private struct SECURITY_ATTRIBUTES + { + public int Length; + public IntPtr SecurityDescriptor; + public bool InheritHandle; + } + + [StructLayoutAttribute(LayoutKind.Sequential)] + private struct SECURITY_DESCRIPTOR + { + public byte Revision; + public byte Size; + public short Control; + public IntPtr Owner; + public IntPtr Group; + public IntPtr Sacl; + public IntPtr Dacl; + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public IntPtr ProcessHandle; + public IntPtr ThreadHandle; + public int ProcessId; + public int ThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public int SessionID; + + [MarshalAs(UnmanagedType.LPTStr)] + public string WinStationName; + public ConnectionState State; + } + } } diff --git a/Scalar.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs b/Scalar.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs index 35b68159d6..0c22b5b484 100644 --- a/Scalar.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs +++ b/Scalar.Platform.Windows/DiskLayoutUpgrades/WindowsDiskLayoutUpgradeData.cs @@ -1,23 +1,23 @@ -using Scalar.Common; -using Scalar.DiskLayoutUpgrades; - -namespace Scalar.Platform.Windows.DiskLayoutUpgrades -{ - public class WindowsDiskLayoutUpgradeData : IDiskLayoutUpgradeData - { - public DiskLayoutUpgrade[] Upgrades - { - get - { - return new DiskLayoutUpgrade[] - { - }; - } - } - - public DiskLayoutVersion Version => new DiskLayoutVersion( - currentMajorVersion: 0, - currentMinorVersion: 0, - minimumSupportedMajorVersion: 0); - } -} +using Scalar.Common; +using Scalar.DiskLayoutUpgrades; + +namespace Scalar.Platform.Windows.DiskLayoutUpgrades +{ + public class WindowsDiskLayoutUpgradeData : IDiskLayoutUpgradeData + { + public DiskLayoutUpgrade[] Upgrades + { + get + { + return new DiskLayoutUpgrade[] + { + }; + } + } + + public DiskLayoutVersion Version => new DiskLayoutVersion( + currentMajorVersion: 0, + currentMinorVersion: 0, + minimumSupportedMajorVersion: 0); + } +} diff --git a/Scalar.Platform.Windows/Properties/AssemblyInfo.cs b/Scalar.Platform.Windows/Properties/AssemblyInfo.cs index 48c54b63d5..fa7187c1e1 100644 --- a/Scalar.Platform.Windows/Properties/AssemblyInfo.cs +++ b/Scalar.Platform.Windows/Properties/AssemblyInfo.cs @@ -1,22 +1,22 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar.Platform.Windows")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar.Platform.Windows")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("4ce404e7-d3fc-471c-993c-64615861ea63")] +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar.Platform.Windows")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar.Platform.Windows")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("4ce404e7-d3fc-471c-993c-64615861ea63")] diff --git a/Scalar.Platform.Windows/Scalar.Platform.Windows.csproj b/Scalar.Platform.Windows/Scalar.Platform.Windows.csproj index d811ce6194..e958b24e47 100644 --- a/Scalar.Platform.Windows/Scalar.Platform.Windows.csproj +++ b/Scalar.Platform.Windows/Scalar.Platform.Windows.csproj @@ -1,86 +1,86 @@ - - - - - - {4CE404E7-D3FC-471C-993C-64615861EA63} - Library - Properties - Scalar.Platform.Windows - Scalar.Platform.Windows - v4.6.1 - 512 - - - - - true - DEBUG;TRACE - full - x64 - prompt - - - TRACE - true - pdbonly - x64 - prompt - - - - - - - ..\..\packages\System.Management.Automation.dll.10.0.10586.0\lib\net40\System.Management.Automation.dll - - - - - - - - - - - - - - - - - - - - - - - - - Designer - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - + + + + + + {4CE404E7-D3FC-471C-993C-64615861EA63} + Library + Properties + Scalar.Platform.Windows + Scalar.Platform.Windows + v4.6.1 + 512 + + + + + true + DEBUG;TRACE + full + x64 + prompt + + + TRACE + true + pdbonly + x64 + prompt + + + + + + + ..\..\packages\System.Management.Automation.dll.10.0.10586.0\lib\net40\System.Management.Automation.dll + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + diff --git a/Scalar.Platform.Windows/WindowsFileBasedLock.cs b/Scalar.Platform.Windows/WindowsFileBasedLock.cs index d06740e86c..81a8d792b5 100644 --- a/Scalar.Platform.Windows/WindowsFileBasedLock.cs +++ b/Scalar.Platform.Windows/WindowsFileBasedLock.cs @@ -1,135 +1,135 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.ComponentModel; -using System.IO; -using System.Text; - -namespace Scalar.Platform.Windows -{ - public class WindowsFileBasedLock : FileBasedLock - { - private const int HResultErrorSharingViolation = -2147024864; // -2147024864 = 0x80070020 = ERROR_SHARING_VIOLATION - private const int HResultErrorFileExists = -2147024816; // -2147024816 = 0x80070050 = ERROR_FILE_EXISTS - private const int DefaultStreamWriterBufferSize = 1024; // Copied from: http://referencesource.microsoft.com/#mscorlib/system/io/streamwriter.cs,5516ce201dc06b5f - private const string EtwArea = nameof(WindowsFileBasedLock); - private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); // Default encoding used by StreamWriter - - private readonly object deleteOnCloseStreamLock = new object(); - private Stream deleteOnCloseStream; - - /// - /// FileBasedLock constructor - /// - /// Path to lock file - /// Text to write in lock file - /// - /// Scalar keeps an exclusive write handle open to lock files that it creates with FileBasedLock. This means that - /// FileBasedLock still ensures exclusivity when the lock file is used only for coordination between multiple Scalar processes. - /// - public WindowsFileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - : base(fileSystem, tracer, lockPath) - { - } - - public override bool TryAcquireLock() - { - try - { - lock (this.deleteOnCloseStreamLock) - { - if (this.deleteOnCloseStream != null) - { - throw new InvalidOperationException("Lock has already been acquired"); - } - - this.FileSystem.CreateDirectory(Path.GetDirectoryName(this.LockPath)); - - this.deleteOnCloseStream = this.FileSystem.OpenFileStream( - this.LockPath, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.Read, - FileOptions.DeleteOnClose, - callFlushFileBuffers: false); - - return true; - } - } - catch (IOException e) - { - // HResultErrorFileExists is expected when the lock file exists - // HResultErrorSharingViolation is expected when the lock file exists andanother Scalar process has acquired the lock file - if (e.HResult != HResultErrorFileExists && e.HResult != HResultErrorSharingViolation) - { - EventMetadata metadata = this.CreateLockMetadata(e); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: IOException caught while trying to acquire lock"); - } - - this.DisposeStream(); - return false; - } - catch (UnauthorizedAccessException e) - { - EventMetadata metadata = this.CreateLockMetadata(e); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: UnauthorizedAccessException caught while trying to acquire lock"); - - this.DisposeStream(); - return false; - } - catch (Win32Exception e) - { - EventMetadata metadata = this.CreateLockMetadata(e); - this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: Win32Exception caught while trying to acquire lock"); - - this.DisposeStream(); - return false; - } - catch (Exception e) - { - EventMetadata metadata = this.CreateLockMetadata(e); - this.Tracer.RelatedError(metadata, $"{nameof(this.TryAcquireLock)}: Unhandled exception caught while trying to acquire lock"); - - this.DisposeStream(); - throw; - } - } - - public override void Dispose() - { - this.DisposeStream(); - } - - private EventMetadata CreateLockMetadata(Exception exception = null) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add(nameof(this.LockPath), this.LockPath); - if (exception != null) - { - metadata.Add("Exception", exception.ToString()); - } - - return metadata; - } - - private bool DisposeStream() - { - lock (this.deleteOnCloseStreamLock) - { - if (this.deleteOnCloseStream != null) - { - this.deleteOnCloseStream.Dispose(); - this.deleteOnCloseStream = null; - return true; - } - } - - return false; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.ComponentModel; +using System.IO; +using System.Text; + +namespace Scalar.Platform.Windows +{ + public class WindowsFileBasedLock : FileBasedLock + { + private const int HResultErrorSharingViolation = -2147024864; // -2147024864 = 0x80070020 = ERROR_SHARING_VIOLATION + private const int HResultErrorFileExists = -2147024816; // -2147024816 = 0x80070050 = ERROR_FILE_EXISTS + private const int DefaultStreamWriterBufferSize = 1024; // Copied from: http://referencesource.microsoft.com/#mscorlib/system/io/streamwriter.cs,5516ce201dc06b5f + private const string EtwArea = nameof(WindowsFileBasedLock); + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); // Default encoding used by StreamWriter + + private readonly object deleteOnCloseStreamLock = new object(); + private Stream deleteOnCloseStream; + + /// + /// FileBasedLock constructor + /// + /// Path to lock file + /// Text to write in lock file + /// + /// Scalar keeps an exclusive write handle open to lock files that it creates with FileBasedLock. This means that + /// FileBasedLock still ensures exclusivity when the lock file is used only for coordination between multiple Scalar processes. + /// + public WindowsFileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + : base(fileSystem, tracer, lockPath) + { + } + + public override bool TryAcquireLock() + { + try + { + lock (this.deleteOnCloseStreamLock) + { + if (this.deleteOnCloseStream != null) + { + throw new InvalidOperationException("Lock has already been acquired"); + } + + this.FileSystem.CreateDirectory(Path.GetDirectoryName(this.LockPath)); + + this.deleteOnCloseStream = this.FileSystem.OpenFileStream( + this.LockPath, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.Read, + FileOptions.DeleteOnClose, + callFlushFileBuffers: false); + + return true; + } + } + catch (IOException e) + { + // HResultErrorFileExists is expected when the lock file exists + // HResultErrorSharingViolation is expected when the lock file exists andanother Scalar process has acquired the lock file + if (e.HResult != HResultErrorFileExists && e.HResult != HResultErrorSharingViolation) + { + EventMetadata metadata = this.CreateLockMetadata(e); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: IOException caught while trying to acquire lock"); + } + + this.DisposeStream(); + return false; + } + catch (UnauthorizedAccessException e) + { + EventMetadata metadata = this.CreateLockMetadata(e); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: UnauthorizedAccessException caught while trying to acquire lock"); + + this.DisposeStream(); + return false; + } + catch (Win32Exception e) + { + EventMetadata metadata = this.CreateLockMetadata(e); + this.Tracer.RelatedWarning(metadata, $"{nameof(this.TryAcquireLock)}: Win32Exception caught while trying to acquire lock"); + + this.DisposeStream(); + return false; + } + catch (Exception e) + { + EventMetadata metadata = this.CreateLockMetadata(e); + this.Tracer.RelatedError(metadata, $"{nameof(this.TryAcquireLock)}: Unhandled exception caught while trying to acquire lock"); + + this.DisposeStream(); + throw; + } + } + + public override void Dispose() + { + this.DisposeStream(); + } + + private EventMetadata CreateLockMetadata(Exception exception = null) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add(nameof(this.LockPath), this.LockPath); + if (exception != null) + { + metadata.Add("Exception", exception.ToString()); + } + + return metadata; + } + + private bool DisposeStream() + { + lock (this.deleteOnCloseStreamLock) + { + if (this.deleteOnCloseStream != null) + { + this.deleteOnCloseStream.Dispose(); + this.deleteOnCloseStream = null; + return true; + } + } + + return false; + } + } +} diff --git a/Scalar.Platform.Windows/WindowsFileSystem.Shared.cs b/Scalar.Platform.Windows/WindowsFileSystem.Shared.cs index 6dbf3bef96..8c91d012a4 100644 --- a/Scalar.Platform.Windows/WindowsFileSystem.Shared.cs +++ b/Scalar.Platform.Windows/WindowsFileSystem.Shared.cs @@ -1,48 +1,48 @@ -using Scalar.Common; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; - -namespace Scalar.Platform.Windows -{ - public partial class WindowsFileSystem - { - public static bool TryGetNormalizedPathImplementation(string path, out string normalizedPath, out string errorMessage) - { - normalizedPath = null; - errorMessage = null; - try - { - // The folder might not be on disk yet, walk up the path until we find a folder that's on disk - Stack removedPathParts = new Stack(); - string parentPath = path; - while (!string.IsNullOrWhiteSpace(parentPath) && !Directory.Exists(parentPath)) - { - removedPathParts.Push(Path.GetFileName(parentPath)); - parentPath = Path.GetDirectoryName(parentPath); - } - - if (string.IsNullOrWhiteSpace(parentPath)) - { - errorMessage = "Could not get path root. Specified path does not exist and unable to find ancestor of path on disk"; - return false; - } - - normalizedPath = NativeMethods.GetFinalPathName(parentPath); - - // normalizedPath now consists of all parts of the path currently on disk, re-add any parts of the path that were popped off - while (removedPathParts.Count > 0) - { - normalizedPath = Path.Combine(normalizedPath, removedPathParts.Pop()); - } - } - catch (Win32Exception e) - { - errorMessage = "Could not get path root. Failed to determine volume: " + e.Message; - return false; - } - - return true; - } - } -} +using Scalar.Common; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; + +namespace Scalar.Platform.Windows +{ + public partial class WindowsFileSystem + { + public static bool TryGetNormalizedPathImplementation(string path, out string normalizedPath, out string errorMessage) + { + normalizedPath = null; + errorMessage = null; + try + { + // The folder might not be on disk yet, walk up the path until we find a folder that's on disk + Stack removedPathParts = new Stack(); + string parentPath = path; + while (!string.IsNullOrWhiteSpace(parentPath) && !Directory.Exists(parentPath)) + { + removedPathParts.Push(Path.GetFileName(parentPath)); + parentPath = Path.GetDirectoryName(parentPath); + } + + if (string.IsNullOrWhiteSpace(parentPath)) + { + errorMessage = "Could not get path root. Specified path does not exist and unable to find ancestor of path on disk"; + return false; + } + + normalizedPath = NativeMethods.GetFinalPathName(parentPath); + + // normalizedPath now consists of all parts of the path currently on disk, re-add any parts of the path that were popped off + while (removedPathParts.Count > 0) + { + normalizedPath = Path.Combine(normalizedPath, removedPathParts.Pop()); + } + } + catch (Win32Exception e) + { + errorMessage = "Could not get path root. Failed to determine volume: " + e.Message; + return false; + } + + return true; + } + } +} diff --git a/Scalar.Platform.Windows/WindowsFileSystem.cs b/Scalar.Platform.Windows/WindowsFileSystem.cs index a922b57be7..23253059d5 100644 --- a/Scalar.Platform.Windows/WindowsFileSystem.cs +++ b/Scalar.Platform.Windows/WindowsFileSystem.cs @@ -1,243 +1,243 @@ -using Microsoft.Win32.SafeHandles; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Security.AccessControl; -using System.Security.Principal; - -namespace Scalar.Platform.Windows -{ - public partial class WindowsFileSystem : IPlatformFileSystem - { - public bool SupportsFileMode { get; } = false; - - /// - /// Adds a new FileSystemAccessRule granting read (and optionally modify) access for all users. - /// - /// DirectorySecurity to which a FileSystemAccessRule will be added. - /// - /// True if all users should be given modify access, false if users should only be allowed read access - /// - public static void AddUsersAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity, bool grantUsersModifyPermissions) - { - SecurityIdentifier allUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); - FileSystemRights rights = FileSystemRights.Read; - if (grantUsersModifyPermissions) - { - rights = rights | FileSystemRights.Modify; - } - - // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files - // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children - // AccessControlType.Allow -> Rule is used to allow access to an object - directorySecurity.AddAccessRule( - new FileSystemAccessRule( - allUsers, - rights, - InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, - PropagationFlags.None, - AccessControlType.Allow)); - } - - /// - /// Adds a new FileSystemAccessRule granting read/exceute/modify/delete access for administrators. - /// - /// DirectorySecurity to which a FileSystemAccessRule will be added. - public static void AddAdminAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity) - { - SecurityIdentifier administratorUsers = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); - - // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files - // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children - // AccessControlType.Allow -> Rule is used to allow access to an object - directorySecurity.AddAccessRule( - new FileSystemAccessRule( - administratorUsers, - FileSystemRights.ReadAndExecute | FileSystemRights.Modify | FileSystemRights.Delete, - InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, - PropagationFlags.None, - AccessControlType.Allow)); - } - - /// - /// Removes all FileSystemAccessRules from specified DirectorySecurity - /// - /// DirectorySecurity from which to remove FileSystemAccessRules - public static void RemoveAllFileSystemAccessRulesFromDirectorySecurity(DirectorySecurity directorySecurity) - { - AuthorizationRuleCollection currentRules = directorySecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(NTAccount)); - foreach (AuthorizationRule authorizationRule in currentRules) - { - FileSystemAccessRule fileSystemRule = authorizationRule as FileSystemAccessRule; - if (fileSystemRule != null) - { - directorySecurity.RemoveAccessRule(fileSystemRule); - } - } - } - - public void FlushFileBuffers(string path) - { - NativeMethods.FlushFileBuffers(path); - } - - public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - NativeMethods.MoveFile( - sourceFileName, - destinationFilename, - NativeMethods.MoveFileFlags.MoveFileReplaceExisting); - } - - public void ChangeMode(string path, ushort mode) - { - } - - public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) - { - return WindowsFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); +using Microsoft.Win32.SafeHandles; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using System.Security.Principal; + +namespace Scalar.Platform.Windows +{ + public partial class WindowsFileSystem : IPlatformFileSystem + { + public bool SupportsFileMode { get; } = false; + + /// + /// Adds a new FileSystemAccessRule granting read (and optionally modify) access for all users. + /// + /// DirectorySecurity to which a FileSystemAccessRule will be added. + /// + /// True if all users should be given modify access, false if users should only be allowed read access + /// + public static void AddUsersAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity, bool grantUsersModifyPermissions) + { + SecurityIdentifier allUsers = new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null); + FileSystemRights rights = FileSystemRights.Read; + if (grantUsersModifyPermissions) + { + rights = rights | FileSystemRights.Modify; + } + + // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files + // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children + // AccessControlType.Allow -> Rule is used to allow access to an object + directorySecurity.AddAccessRule( + new FileSystemAccessRule( + allUsers, + rights, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.None, + AccessControlType.Allow)); + } + + /// + /// Adds a new FileSystemAccessRule granting read/exceute/modify/delete access for administrators. + /// + /// DirectorySecurity to which a FileSystemAccessRule will be added. + public static void AddAdminAccessRulesToDirectorySecurity(DirectorySecurity directorySecurity) + { + SecurityIdentifier administratorUsers = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null); + + // InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit -> ACE is inherited by child directories and files + // PropagationFlags.None -> Standard propagation rules, settings are applied to the directory and its children + // AccessControlType.Allow -> Rule is used to allow access to an object + directorySecurity.AddAccessRule( + new FileSystemAccessRule( + administratorUsers, + FileSystemRights.ReadAndExecute | FileSystemRights.Modify | FileSystemRights.Delete, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.None, + AccessControlType.Allow)); + } + + /// + /// Removes all FileSystemAccessRules from specified DirectorySecurity + /// + /// DirectorySecurity from which to remove FileSystemAccessRules + public static void RemoveAllFileSystemAccessRulesFromDirectorySecurity(DirectorySecurity directorySecurity) + { + AuthorizationRuleCollection currentRules = directorySecurity.GetAccessRules(includeExplicit: true, includeInherited: true, targetType: typeof(NTAccount)); + foreach (AuthorizationRule authorizationRule in currentRules) + { + FileSystemAccessRule fileSystemRule = authorizationRule as FileSystemAccessRule; + if (fileSystemRule != null) + { + directorySecurity.RemoveAccessRule(fileSystemRule); + } + } + } + + public void FlushFileBuffers(string path) + { + NativeMethods.FlushFileBuffers(path); + } + + public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + NativeMethods.MoveFile( + sourceFileName, + destinationFilename, + NativeMethods.MoveFileFlags.MoveFileReplaceExisting); } - public bool HydrateFile(string fileName, byte[] buffer) - { - return NativeFileReader.TryReadFirstByteOfFile(fileName, buffer); - } - - public bool IsExecutable(string fileName) - { - string fileExtension = Path.GetExtension(fileName); - return string.Equals(fileExtension, ".exe", StringComparison.OrdinalIgnoreCase); - } - - public bool IsSocket(string fileName) - { - return false; - } - - public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) - { - try - { - DirectorySecurity directorySecurity = new DirectorySecurity(); - - // Protect the access rules from inheritance and remove any inherited rules - directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); - - // Add new ACLs for users and admins. Users will be granted write permissions. - AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: true); - AddAdminAccessRulesToDirectorySecurity(directorySecurity); - - Directory.CreateDirectory(directoryPath, directorySecurity); - } - catch (Exception e) when (e is IOException || - e is UnauthorizedAccessException || - e is PathTooLongException || - e is DirectoryNotFoundException) - { - error = $"Exception while creating directory `{directoryPath}`: {e.Message}"; - return false; - } - - error = null; - return true; - } - - public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) - { - try - { - DirectorySecurity directorySecurity; - if (Directory.Exists(directoryPath)) - { - directorySecurity = Directory.GetAccessControl(directoryPath); - } - else - { - directorySecurity = new DirectorySecurity(); - } - - // Protect the access rules from inheritance and remove any inherited rules - directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); - - // Remove any existing ACLs and add new ACLs for users and admins - RemoveAllFileSystemAccessRulesFromDirectorySecurity(directorySecurity); - AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: false); - AddAdminAccessRulesToDirectorySecurity(directorySecurity); - - Directory.CreateDirectory(directoryPath, directorySecurity); - - // Ensure the ACLs are set correctly if the directory already existed - Directory.SetAccessControl(directoryPath, directorySecurity); - } - catch (Exception e) when (e is IOException || e is SystemException) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", e.ToString()); - tracer.RelatedError(metadata, $"{nameof(this.TryCreateOrUpdateDirectoryToAdminModifyPermissions)}: Exception while creating/configuring directory"); - - error = e.Message; - return false; - } - - error = null; - return true; - } - - public bool IsFileSystemSupported(string path, out string error) - { - error = null; - return true; - } - - private class NativeFileReader - { - private const uint GenericRead = 0x80000000; + public void ChangeMode(string path, ushort mode) + { + } + + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) + { + return WindowsFileSystem.TryGetNormalizedPathImplementation(path, out normalizedPath, out errorMessage); + } + + public bool HydrateFile(string fileName, byte[] buffer) + { + return NativeFileReader.TryReadFirstByteOfFile(fileName, buffer); + } + + public bool IsExecutable(string fileName) + { + string fileExtension = Path.GetExtension(fileName); + return string.Equals(fileExtension, ".exe", StringComparison.OrdinalIgnoreCase); + } + + public bool IsSocket(string fileName) + { + return false; + } + + public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) + { + try + { + DirectorySecurity directorySecurity = new DirectorySecurity(); + + // Protect the access rules from inheritance and remove any inherited rules + directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); + + // Add new ACLs for users and admins. Users will be granted write permissions. + AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: true); + AddAdminAccessRulesToDirectorySecurity(directorySecurity); + + Directory.CreateDirectory(directoryPath, directorySecurity); + } + catch (Exception e) when (e is IOException || + e is UnauthorizedAccessException || + e is PathTooLongException || + e is DirectoryNotFoundException) + { + error = $"Exception while creating directory `{directoryPath}`: {e.Message}"; + return false; + } + + error = null; + return true; + } + + public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) + { + try + { + DirectorySecurity directorySecurity; + if (Directory.Exists(directoryPath)) + { + directorySecurity = Directory.GetAccessControl(directoryPath); + } + else + { + directorySecurity = new DirectorySecurity(); + } + + // Protect the access rules from inheritance and remove any inherited rules + directorySecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); + + // Remove any existing ACLs and add new ACLs for users and admins + RemoveAllFileSystemAccessRulesFromDirectorySecurity(directorySecurity); + AddUsersAccessRulesToDirectorySecurity(directorySecurity, grantUsersModifyPermissions: false); + AddAdminAccessRulesToDirectorySecurity(directorySecurity); + + Directory.CreateDirectory(directoryPath, directorySecurity); + + // Ensure the ACLs are set correctly if the directory already existed + Directory.SetAccessControl(directoryPath, directorySecurity); + } + catch (Exception e) when (e is IOException || e is SystemException) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + tracer.RelatedError(metadata, $"{nameof(this.TryCreateOrUpdateDirectoryToAdminModifyPermissions)}: Exception while creating/configuring directory"); + + error = e.Message; + return false; + } + + error = null; + return true; + } + + public bool IsFileSystemSupported(string path, out string error) + { + error = null; + return true; + } + + private class NativeFileReader + { + private const uint GenericRead = 0x80000000; private const uint OpenExisting = 3; - - public static bool TryReadFirstByteOfFile(string fileName, byte[] buffer) - { - using (SafeFileHandle handle = Open(fileName)) - { - if (!handle.IsInvalid) - { - return ReadOneByte(handle, buffer); - } - } - - return false; - } - - private static SafeFileHandle Open(string fileName) - { - return CreateFile(fileName, GenericRead, (uint)(FileShare.ReadWrite | FileShare.Delete), 0, OpenExisting, 0, 0); - } - - private static bool ReadOneByte(SafeFileHandle handle, byte[] buffer) - { - int bytesRead = 0; - return ReadFile(handle, buffer, 1, ref bytesRead, 0); - } - - [DllImport("kernel32", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Unicode)] - private static extern SafeFileHandle CreateFile( - string fileName, - uint desiredAccess, - uint shareMode, - uint securityAttributes, - uint creationDisposition, - uint flagsAndAttributes, - int hemplateFile); - - [DllImport("kernel32", SetLastError = true)] - private static extern bool ReadFile( - SafeFileHandle file, - [Out] byte[] buffer, - int numberOfBytesToRead, - ref int numberOfBytesRead, - int overlapped); - } - } -} + + public static bool TryReadFirstByteOfFile(string fileName, byte[] buffer) + { + using (SafeFileHandle handle = Open(fileName)) + { + if (!handle.IsInvalid) + { + return ReadOneByte(handle, buffer); + } + } + + return false; + } + + private static SafeFileHandle Open(string fileName) + { + return CreateFile(fileName, GenericRead, (uint)(FileShare.ReadWrite | FileShare.Delete), 0, OpenExisting, 0, 0); + } + + private static bool ReadOneByte(SafeFileHandle handle, byte[] buffer) + { + int bytesRead = 0; + return ReadFile(handle, buffer, 1, ref bytesRead, 0); + } + + [DllImport("kernel32", SetLastError = true, ThrowOnUnmappableChar = true, CharSet = CharSet.Unicode)] + private static extern SafeFileHandle CreateFile( + string fileName, + uint desiredAccess, + uint shareMode, + uint securityAttributes, + uint creationDisposition, + uint flagsAndAttributes, + int hemplateFile); + + [DllImport("kernel32", SetLastError = true)] + private static extern bool ReadFile( + SafeFileHandle file, + [Out] byte[] buffer, + int numberOfBytesToRead, + ref int numberOfBytesRead, + int overlapped); + } + } +} diff --git a/Scalar.Platform.Windows/WindowsGitInstallation.cs b/Scalar.Platform.Windows/WindowsGitInstallation.cs index c9221c8392..d50e6ebe45 100644 --- a/Scalar.Platform.Windows/WindowsGitInstallation.cs +++ b/Scalar.Platform.Windows/WindowsGitInstallation.cs @@ -1,39 +1,39 @@ -using Scalar.Common; -using Scalar.Common.Git; -using System.IO; - -namespace Scalar.Platform.Windows -{ - public class WindowsGitInstallation : IGitInstallation - { - private const string GitProcessName = "git.exe"; - private const string GitBinRelativePath = "cmd\\git.exe"; - private const string GitInstallationRegistryKey = "SOFTWARE\\GitForWindows"; - private const string GitInstallationRegistryInstallPathValue = "InstallPath"; - - public bool GitExists(string gitBinPath) - { - if (!string.IsNullOrWhiteSpace(gitBinPath)) - { - return File.Exists(gitBinPath); - } - - return ProcessHelper.GetProgramLocation(ScalarPlatform.Instance.Constants.ProgramLocaterCommand, GitProcessName) != null; - } - - public string GetInstalledGitBinPath() - { - string gitBinPath = WindowsPlatform.GetStringFromRegistry(GitInstallationRegistryKey, GitInstallationRegistryInstallPathValue); - if (!string.IsNullOrWhiteSpace(gitBinPath)) - { - gitBinPath = Path.Combine(gitBinPath, GitBinRelativePath); - if (File.Exists(gitBinPath)) - { - return gitBinPath; - } - } - - return null; - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using System.IO; + +namespace Scalar.Platform.Windows +{ + public class WindowsGitInstallation : IGitInstallation + { + private const string GitProcessName = "git.exe"; + private const string GitBinRelativePath = "cmd\\git.exe"; + private const string GitInstallationRegistryKey = "SOFTWARE\\GitForWindows"; + private const string GitInstallationRegistryInstallPathValue = "InstallPath"; + + public bool GitExists(string gitBinPath) + { + if (!string.IsNullOrWhiteSpace(gitBinPath)) + { + return File.Exists(gitBinPath); + } + + return ProcessHelper.GetProgramLocation(ScalarPlatform.Instance.Constants.ProgramLocaterCommand, GitProcessName) != null; + } + + public string GetInstalledGitBinPath() + { + string gitBinPath = WindowsPlatform.GetStringFromRegistry(GitInstallationRegistryKey, GitInstallationRegistryInstallPathValue); + if (!string.IsNullOrWhiteSpace(gitBinPath)) + { + gitBinPath = Path.Combine(gitBinPath, GitBinRelativePath); + if (File.Exists(gitBinPath)) + { + return gitBinPath; + } + } + + return null; + } + } +} diff --git a/Scalar.Platform.Windows/WindowsPhysicalDiskInfo.cs b/Scalar.Platform.Windows/WindowsPhysicalDiskInfo.cs index 7385ef742e..7bb2e494a8 100644 --- a/Scalar.Platform.Windows/WindowsPhysicalDiskInfo.cs +++ b/Scalar.Platform.Windows/WindowsPhysicalDiskInfo.cs @@ -1,204 +1,204 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Management; - -namespace Scalar.Platform.Windows -{ - public class WindowsPhysicalDiskInfo - { - private static readonly Dictionary MapBusType = new Dictionary() - { - { 0, "unknwon" }, - { 1, "SCSI" }, - { 2, "ATAPI" }, - { 3, "ATA" }, - { 4, "1394" }, - { 5, "SSA" }, - { 6, "FibreChannel" }, - { 7, "USB" }, - { 8, "RAID" }, - { 9, "iSCSI" }, - { 10, "SAS" }, - { 11, "SATA" }, - { 12, "SD" }, - { 13, "MMC" }, - { 14, "Virtual" }, - { 15, "FileBackedVirtual" }, - { 16, "StorageSpaces" }, - { 17, "NVMe" }, - }; - - private static readonly Dictionary MapMediaType = new Dictionary() - { - { 0, "unspecified" }, - { 3, "HDD" }, - { 4, "SSD" }, - { 5, "SCM" }, - }; - - private static readonly Dictionary MapDriveType = new Dictionary() - { - { 0, "unknown" }, - { 1, "InvalidRootPath" }, - { 2, "Removable" }, - { 3, "Fixed" }, - { 4, "Remote" }, - { 5, "CDROM" }, - { 6, "RAMDisk" }, - }; - - /// - /// Get the properties of the drive/volume/partition/physical disk associated - /// the given pathname. For example, whether the drive is an SSD or HDD. - /// - /// A dictionary of platform-specific keywords and values. - public static Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) - { - // Use the WMI APIs to get details about the physical disk associated with the given path. - // Some of these fields are avilable using normal classes, such as System.IO.DriveInfo: - // https://msdn.microsoft.com/en-us/library/system.io.driveinfo(v=vs.110).aspx - // - // But the lower-level fields, such as the BusType and SpindleSpeed, are not. - // - // MSFT_Partition: - // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830524(v=vs.85).aspx - // - // MSFT_Disk: - // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830493(v=vs.85).aspx - // - // MSFT_Volume: - // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830604(v=vs.85).aspx - // - // MSFT_PhysicalDisk: - // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830532(v=vs.85) - // - // An overview of these "classes" can be found here: - // https://msdn.microsoft.com/en-us/library/hh830612.aspx - // - // The map variables defined above are based on property values documented in one of the above APIs. - // There are helper functions below to convert from ManagementBaseObject values into the map values. - // These do not do strict validation because the OS can add new values at any time. For example, the - // integer code for NVMe bus drives was recently added. If an unrecognized value is received, the - // raw integer value is used untranslated. - // - // They are accessed via a generic WQL language that is similar to SQL. See here for an example: - // https://blogs.technet.microsoft.com/josebda/2014/08/11/sample-c-code-for-using-the-latest-wmi-classes-to-manage-windows-storage/ - - Dictionary result = new Dictionary(); - - try - { - char driveLetter = PathToDriveLetter(path); - result.Add("DriveLetter", driveLetter.ToString()); - - ManagementScope scope = new ManagementScope(@"\\.\root\microsoft\windows\storage"); - scope.Connect(); - - DiskSizeStatistics(scope, driveLetter, ref result); - - if (sizeStatsOnly) - { - return result; - } - - DiskTypeInfo(scope, driveLetter, ref result); - } - catch (Exception e) - { - result.Add("Error", e.Message); - } - - return result; - } - - private static void DiskSizeStatistics(ManagementScope scope, char driveLetter, ref Dictionary result) - { - string queryVolumeString = $"SELECT DriveType,FileSystem,FileSystemLabel,Size,SizeRemaining FROM MSFT_Volume WHERE DriveLetter=\"{driveLetter}\""; - ManagementBaseObject mbo = GetFirstRecord(scope, queryVolumeString); - if (mbo != null) - { - result.Add("VolumeDriveType", GetMapValue(MapDriveType, FetchValue(mbo, "DriveType"))); - result.Add("VolumeFileSystem", FetchValue(mbo, "FileSystem")); - result.Add("VolumeFileSystemLabel", FetchValue(mbo, "FileSystemLabel")); - result.Add("VolumeSize", FetchValue(mbo, "Size")); - result.Add("VolumeSizeRemaining", FetchValue(mbo, "SizeRemaining")); - } - } - - private static void DiskTypeInfo(ManagementScope scope, char driveLetter, ref Dictionary result) - { - string queryPartitionString = $"SELECT DiskNumber FROM MSFT_Partition WHERE DriveLetter=\"{driveLetter}\""; - ManagementBaseObject mbo = GetFirstRecord(scope, queryPartitionString); - if (mbo != null) - { - string diskNumber = FetchValue(mbo, "DiskNumber"); - result.Add("DiskNumber", diskNumber); - - if (diskNumber.Length > 0) - { - string queryDiskString = $"SELECT Model,IsBoot,IsSystem,SerialNumber FROM MSFT_Disk WHERE Number=\"{diskNumber}\""; - mbo = GetFirstRecord(scope, queryDiskString); - if (mbo != null) - { - result.Add("DiskModel", FetchValue(mbo, "Model")); - result.Add("DiskIsSystem", FetchValue(mbo, "IsSystem")); - result.Add("DiskIsBoot", FetchValue(mbo, "IsBoot")); - result.Add("DiskSerialNumber", FetchValue(mbo, "SerialNumber")); - } - - string queryPhysicalDiskString = $"SELECT MediaType,BusType,SpindleSpeed FROM MSFT_PhysicalDisk WHERE DeviceId=\"{diskNumber}\""; - mbo = GetFirstRecord(scope, queryPhysicalDiskString); - if (mbo != null) - { - result.Add("PhysicalMediaType", GetMapValue(MapMediaType, FetchValue(mbo, "MediaType"))); - result.Add("PhysicalBusType", GetMapValue(MapBusType, FetchValue(mbo, "BusType"))); - result.Add("PhysicalSpindleSpeed", FetchValue(mbo, "SpindleSpeed")); - } - } - } - } - - private static string FetchValue(ManagementBaseObject mbo, string key) - { - return (mbo[key] != null) ? mbo[key].ToString().Trim() : string.Empty; - } - - private static string GetMapValue(Dictionary map, string rawValue) - { - return int.TryParse(rawValue, out int key) && map.Keys.Contains(key) ? map[key] : rawValue; - } - - private static char PathToDriveLetter(string path) - { - FileInfo fi = new FileInfo(path); - string drive = Path.GetPathRoot(fi.FullName); - if ((drive.Length == 3) && (drive[1] == ':') && (drive[2] == '\\')) - { - if ((drive[0] >= 'A') && (drive[0] <= 'Z')) - { - return drive[0]; - } - - if ((drive[0] >= 'a') && (drive[0] <= 'z')) - { - return char.ToUpper(drive[0]); - } - } - - // A bogus path or a UNC path. This should not happen since the path should already - // have been validated. - throw new ArgumentException($"Could not map path '{path}' to a drive letter."); - } - - private static ManagementBaseObject GetFirstRecord(ManagementScope scope, string queryString) - { - ObjectQuery q = new ObjectQuery(queryString); - ManagementObjectSearcher s = new ManagementObjectSearcher(scope, q); - - // Only return the first result. (There should only be one row returned for each of these queries.) - return s.Get().Cast().FirstOrDefault(); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Management; + +namespace Scalar.Platform.Windows +{ + public class WindowsPhysicalDiskInfo + { + private static readonly Dictionary MapBusType = new Dictionary() + { + { 0, "unknwon" }, + { 1, "SCSI" }, + { 2, "ATAPI" }, + { 3, "ATA" }, + { 4, "1394" }, + { 5, "SSA" }, + { 6, "FibreChannel" }, + { 7, "USB" }, + { 8, "RAID" }, + { 9, "iSCSI" }, + { 10, "SAS" }, + { 11, "SATA" }, + { 12, "SD" }, + { 13, "MMC" }, + { 14, "Virtual" }, + { 15, "FileBackedVirtual" }, + { 16, "StorageSpaces" }, + { 17, "NVMe" }, + }; + + private static readonly Dictionary MapMediaType = new Dictionary() + { + { 0, "unspecified" }, + { 3, "HDD" }, + { 4, "SSD" }, + { 5, "SCM" }, + }; + + private static readonly Dictionary MapDriveType = new Dictionary() + { + { 0, "unknown" }, + { 1, "InvalidRootPath" }, + { 2, "Removable" }, + { 3, "Fixed" }, + { 4, "Remote" }, + { 5, "CDROM" }, + { 6, "RAMDisk" }, + }; + + /// + /// Get the properties of the drive/volume/partition/physical disk associated + /// the given pathname. For example, whether the drive is an SSD or HDD. + /// + /// A dictionary of platform-specific keywords and values. + public static Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) + { + // Use the WMI APIs to get details about the physical disk associated with the given path. + // Some of these fields are avilable using normal classes, such as System.IO.DriveInfo: + // https://msdn.microsoft.com/en-us/library/system.io.driveinfo(v=vs.110).aspx + // + // But the lower-level fields, such as the BusType and SpindleSpeed, are not. + // + // MSFT_Partition: + // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830524(v=vs.85).aspx + // + // MSFT_Disk: + // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830493(v=vs.85).aspx + // + // MSFT_Volume: + // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830604(v=vs.85).aspx + // + // MSFT_PhysicalDisk: + // https://msdn.microsoft.com/en-us/library/windows/desktop/hh830532(v=vs.85) + // + // An overview of these "classes" can be found here: + // https://msdn.microsoft.com/en-us/library/hh830612.aspx + // + // The map variables defined above are based on property values documented in one of the above APIs. + // There are helper functions below to convert from ManagementBaseObject values into the map values. + // These do not do strict validation because the OS can add new values at any time. For example, the + // integer code for NVMe bus drives was recently added. If an unrecognized value is received, the + // raw integer value is used untranslated. + // + // They are accessed via a generic WQL language that is similar to SQL. See here for an example: + // https://blogs.technet.microsoft.com/josebda/2014/08/11/sample-c-code-for-using-the-latest-wmi-classes-to-manage-windows-storage/ + + Dictionary result = new Dictionary(); + + try + { + char driveLetter = PathToDriveLetter(path); + result.Add("DriveLetter", driveLetter.ToString()); + + ManagementScope scope = new ManagementScope(@"\\.\root\microsoft\windows\storage"); + scope.Connect(); + + DiskSizeStatistics(scope, driveLetter, ref result); + + if (sizeStatsOnly) + { + return result; + } + + DiskTypeInfo(scope, driveLetter, ref result); + } + catch (Exception e) + { + result.Add("Error", e.Message); + } + + return result; + } + + private static void DiskSizeStatistics(ManagementScope scope, char driveLetter, ref Dictionary result) + { + string queryVolumeString = $"SELECT DriveType,FileSystem,FileSystemLabel,Size,SizeRemaining FROM MSFT_Volume WHERE DriveLetter=\"{driveLetter}\""; + ManagementBaseObject mbo = GetFirstRecord(scope, queryVolumeString); + if (mbo != null) + { + result.Add("VolumeDriveType", GetMapValue(MapDriveType, FetchValue(mbo, "DriveType"))); + result.Add("VolumeFileSystem", FetchValue(mbo, "FileSystem")); + result.Add("VolumeFileSystemLabel", FetchValue(mbo, "FileSystemLabel")); + result.Add("VolumeSize", FetchValue(mbo, "Size")); + result.Add("VolumeSizeRemaining", FetchValue(mbo, "SizeRemaining")); + } + } + + private static void DiskTypeInfo(ManagementScope scope, char driveLetter, ref Dictionary result) + { + string queryPartitionString = $"SELECT DiskNumber FROM MSFT_Partition WHERE DriveLetter=\"{driveLetter}\""; + ManagementBaseObject mbo = GetFirstRecord(scope, queryPartitionString); + if (mbo != null) + { + string diskNumber = FetchValue(mbo, "DiskNumber"); + result.Add("DiskNumber", diskNumber); + + if (diskNumber.Length > 0) + { + string queryDiskString = $"SELECT Model,IsBoot,IsSystem,SerialNumber FROM MSFT_Disk WHERE Number=\"{diskNumber}\""; + mbo = GetFirstRecord(scope, queryDiskString); + if (mbo != null) + { + result.Add("DiskModel", FetchValue(mbo, "Model")); + result.Add("DiskIsSystem", FetchValue(mbo, "IsSystem")); + result.Add("DiskIsBoot", FetchValue(mbo, "IsBoot")); + result.Add("DiskSerialNumber", FetchValue(mbo, "SerialNumber")); + } + + string queryPhysicalDiskString = $"SELECT MediaType,BusType,SpindleSpeed FROM MSFT_PhysicalDisk WHERE DeviceId=\"{diskNumber}\""; + mbo = GetFirstRecord(scope, queryPhysicalDiskString); + if (mbo != null) + { + result.Add("PhysicalMediaType", GetMapValue(MapMediaType, FetchValue(mbo, "MediaType"))); + result.Add("PhysicalBusType", GetMapValue(MapBusType, FetchValue(mbo, "BusType"))); + result.Add("PhysicalSpindleSpeed", FetchValue(mbo, "SpindleSpeed")); + } + } + } + } + + private static string FetchValue(ManagementBaseObject mbo, string key) + { + return (mbo[key] != null) ? mbo[key].ToString().Trim() : string.Empty; + } + + private static string GetMapValue(Dictionary map, string rawValue) + { + return int.TryParse(rawValue, out int key) && map.Keys.Contains(key) ? map[key] : rawValue; + } + + private static char PathToDriveLetter(string path) + { + FileInfo fi = new FileInfo(path); + string drive = Path.GetPathRoot(fi.FullName); + if ((drive.Length == 3) && (drive[1] == ':') && (drive[2] == '\\')) + { + if ((drive[0] >= 'A') && (drive[0] <= 'Z')) + { + return drive[0]; + } + + if ((drive[0] >= 'a') && (drive[0] <= 'z')) + { + return char.ToUpper(drive[0]); + } + } + + // A bogus path or a UNC path. This should not happen since the path should already + // have been validated. + throw new ArgumentException($"Could not map path '{path}' to a drive letter."); + } + + private static ManagementBaseObject GetFirstRecord(ManagementScope scope, string queryString) + { + ObjectQuery q = new ObjectQuery(queryString); + ManagementObjectSearcher s = new ManagementObjectSearcher(scope, q); + + // Only return the first result. (There should only be one row returned for each of these queries.) + return s.Get().Cast().FirstOrDefault(); + } + } +} diff --git a/Scalar.Platform.Windows/WindowsPlatform.Shared.cs b/Scalar.Platform.Windows/WindowsPlatform.Shared.cs index 0d7c1879bd..a003408aec 100644 --- a/Scalar.Platform.Windows/WindowsPlatform.Shared.cs +++ b/Scalar.Platform.Windows/WindowsPlatform.Shared.cs @@ -1,130 +1,130 @@ -using Microsoft.Win32.SafeHandles; -using Scalar.Common; -using System; -using System.Diagnostics; -using System.IO; -using System.Runtime.InteropServices; -using System.Security.Principal; - -namespace Scalar.Platform.Windows -{ - public partial class WindowsPlatform - { - public const string DotScalarRoot = ".scalar"; - - private const int StillActive = 259; /* from Win32 STILL_ACTIVE */ - - private enum StdHandle - { - Stdin = -10, - Stdout = -11, - Stderr = -12 - } - - private enum FileType : uint - { - Unknown = 0x0000, - Disk = 0x0001, - Char = 0x0002, - Pipe = 0x0003, - Remote = 0x8000, - } - - public static bool IsElevatedImplementation() - { - using (WindowsIdentity id = WindowsIdentity.GetCurrent()) - { - return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator); - } - } - - public static bool IsProcessActiveImplementation(int processId, bool tryGetProcessById) - { - using (SafeFileHandle process = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.QueryLimitedInformation, false, processId)) - { - if (!process.IsInvalid) - { - uint exitCode; - if (NativeMethods.GetExitCodeProcess(process, out exitCode) && exitCode == StillActive) - { - return true; - } - } - else if (tryGetProcessById) - { - // The process.IsInvalid may be true when the mount process doesn't have access to call - // OpenProcess for the specified processId. Fallback to slow way of finding process. - try - { - Process.GetProcessById(processId); - return true; - } - catch (ArgumentException) - { - return false; - } - } - - return false; - } - } - - public static string GetNamedPipeNameImplementation(string enlistmentRoot) - { - return "Scalar_" + enlistmentRoot.ToUpper().Replace(':', '_'); - } - - public static string GetDataRootForScalarImplementation() - { - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), - "Scalar"); - } - - public static string GetDataRootForScalarComponentImplementation(string componentName) - { - return Path.Combine(GetDataRootForScalarImplementation(), componentName); - } - - public static bool IsConsoleOutputRedirectedToFileImplementation() - { - return FileType.Disk == GetFileType(GetStdHandle(StdHandle.Stdout)); - } - - public static bool TryGetScalarEnlistmentRootImplementation(string directory, out string enlistmentRoot, out string errorMessage) - { - enlistmentRoot = null; - - string finalDirectory; - if (!WindowsFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage)) - { - return false; - } - - enlistmentRoot = Paths.GetRoot(finalDirectory, DotScalarRoot); - if (enlistmentRoot == null) - { - errorMessage = $"Failed to find the root directory for {DotScalarRoot} in {finalDirectory}"; - return false; - } - - return true; - } - - public static string GetUpgradeProtectedDataDirectoryImplementation() - { - return Path.Combine(GetDataRootForScalarImplementation(), ProductUpgraderInfo.UpgradeDirectoryName); - } - - public static string GetUpgradeHighestAvailableVersionDirectoryImplementation() - { - return GetUpgradeProtectedDataDirectoryImplementation(); - } - - [DllImport("kernel32.dll")] - private static extern IntPtr GetStdHandle(StdHandle std); - - [DllImport("kernel32.dll")] - private static extern FileType GetFileType(IntPtr hdl); - } -} +using Microsoft.Win32.SafeHandles; +using Scalar.Common; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Principal; + +namespace Scalar.Platform.Windows +{ + public partial class WindowsPlatform + { + public const string DotScalarRoot = ".scalar"; + + private const int StillActive = 259; /* from Win32 STILL_ACTIVE */ + + private enum StdHandle + { + Stdin = -10, + Stdout = -11, + Stderr = -12 + } + + private enum FileType : uint + { + Unknown = 0x0000, + Disk = 0x0001, + Char = 0x0002, + Pipe = 0x0003, + Remote = 0x8000, + } + + public static bool IsElevatedImplementation() + { + using (WindowsIdentity id = WindowsIdentity.GetCurrent()) + { + return new WindowsPrincipal(id).IsInRole(WindowsBuiltInRole.Administrator); + } + } + + public static bool IsProcessActiveImplementation(int processId, bool tryGetProcessById) + { + using (SafeFileHandle process = NativeMethods.OpenProcess(NativeMethods.ProcessAccessFlags.QueryLimitedInformation, false, processId)) + { + if (!process.IsInvalid) + { + uint exitCode; + if (NativeMethods.GetExitCodeProcess(process, out exitCode) && exitCode == StillActive) + { + return true; + } + } + else if (tryGetProcessById) + { + // The process.IsInvalid may be true when the mount process doesn't have access to call + // OpenProcess for the specified processId. Fallback to slow way of finding process. + try + { + Process.GetProcessById(processId); + return true; + } + catch (ArgumentException) + { + return false; + } + } + + return false; + } + } + + public static string GetNamedPipeNameImplementation(string enlistmentRoot) + { + return "Scalar_" + enlistmentRoot.ToUpper().Replace(':', '_'); + } + + public static string GetDataRootForScalarImplementation() + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData, Environment.SpecialFolderOption.Create), + "Scalar"); + } + + public static string GetDataRootForScalarComponentImplementation(string componentName) + { + return Path.Combine(GetDataRootForScalarImplementation(), componentName); + } + + public static bool IsConsoleOutputRedirectedToFileImplementation() + { + return FileType.Disk == GetFileType(GetStdHandle(StdHandle.Stdout)); + } + + public static bool TryGetScalarEnlistmentRootImplementation(string directory, out string enlistmentRoot, out string errorMessage) + { + enlistmentRoot = null; + + string finalDirectory; + if (!WindowsFileSystem.TryGetNormalizedPathImplementation(directory, out finalDirectory, out errorMessage)) + { + return false; + } + + enlistmentRoot = Paths.GetRoot(finalDirectory, DotScalarRoot); + if (enlistmentRoot == null) + { + errorMessage = $"Failed to find the root directory for {DotScalarRoot} in {finalDirectory}"; + return false; + } + + return true; + } + + public static string GetUpgradeProtectedDataDirectoryImplementation() + { + return Path.Combine(GetDataRootForScalarImplementation(), ProductUpgraderInfo.UpgradeDirectoryName); + } + + public static string GetUpgradeHighestAvailableVersionDirectoryImplementation() + { + return GetUpgradeProtectedDataDirectoryImplementation(); + } + + [DllImport("kernel32.dll")] + private static extern IntPtr GetStdHandle(StdHandle std); + + [DllImport("kernel32.dll")] + private static extern FileType GetFileType(IntPtr hdl); + } +} diff --git a/Scalar.Platform.Windows/WindowsPlatform.cs b/Scalar.Platform.Windows/WindowsPlatform.cs index 78d801a97c..2b6f02bf4e 100644 --- a/Scalar.Platform.Windows/WindowsPlatform.cs +++ b/Scalar.Platform.Windows/WindowsPlatform.cs @@ -1,299 +1,299 @@ -using Microsoft.Win32; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using Scalar.Platform.Windows.DiskLayoutUpgrades; -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.IO; -using System.IO.Pipes; -using System.Linq; -using System.Management.Automation; -using System.Security.AccessControl; -using System.Security.Principal; -using System.ServiceProcess; -using System.Text; - -namespace Scalar.Platform.Windows -{ - public partial class WindowsPlatform : ScalarPlatform - { - private const string WindowsVersionRegistryKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; - private const string BuildLabRegistryValue = "BuildLab"; - private const string BuildLabExRegistryValue = "BuildLabEx"; - - public WindowsPlatform() : base(underConstruction: new UnderConstructionFlags()) - { - } - - public override IGitInstallation GitInstallation { get; } = new WindowsGitInstallation(); - public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new WindowsDiskLayoutUpgradeData(); - public override IPlatformFileSystem FileSystem { get; } = new WindowsFileSystem(); - public override string Name { get => "Windows"; } - public override ScalarPlatformConstants Constants { get; } = new WindowsPlatformConstants(); - - public override string ScalarConfigPath - { - get - { - string servicePath = ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.ServiceName); - string scalarDirectory = Path.GetDirectoryName(servicePath); - - return Path.Combine(scalarDirectory, LocalScalarConfig.FileName); - } - } - - public static string GetStringFromRegistry(string key, string valueName) - { - object value = GetValueFromRegistry(RegistryHive.LocalMachine, key, valueName); - return value as string; - } - - public static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName) - { - object value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry64); - if (value == null) - { - value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry32); - } - - return value; - } - - public static bool TrySetDWordInRegistry(RegistryHive registryHive, string key, string valueName, uint value) - { - RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64); - RegistryKey localKeySub = localKey.OpenSubKey(key, writable: true); - - if (localKeySub == null) - { - localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry32); - localKeySub = localKey.OpenSubKey(key, writable: true); - } - - if (localKeySub == null) - { - return false; - } - - localKeySub.SetValue(valueName, value, RegistryValueKind.DWord); - return true; - } - - public override void InitializeEnlistmentACLs(string enlistmentPath) - { - // The following permissions are typically present on deskop and missing on Server - // - // ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\Authenticated Users - // [OBJECT_INHERIT_ACE] - // [CONTAINER_INHERIT_ACE] - // [INHERIT_ONLY_ACE] - // DELETE - // GENERIC_EXECUTE - // GENERIC_WRITE - // GENERIC_READ - DirectorySecurity rootSecurity = Directory.GetAccessControl(enlistmentPath); - AccessRule authenticatedUsersAccessRule = rootSecurity.AccessRuleFactory( - new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), - unchecked((int)(NativeMethods.FileAccess.DELETE | NativeMethods.FileAccess.GENERIC_EXECUTE | NativeMethods.FileAccess.GENERIC_WRITE | NativeMethods.FileAccess.GENERIC_READ)), - true, - InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, - PropagationFlags.None, - AccessControlType.Allow); - - // The return type of the AccessRuleFactory method is the base class, AccessRule, but the return value can be cast safely to the derived class. - // https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemsecurity.accessrulefactory(v=vs.110).aspx - rootSecurity.AddAccessRule((FileSystemAccessRule)authenticatedUsersAccessRule); - Directory.SetAccessControl(enlistmentPath, rootSecurity); - } - - public override string GetOSVersionInformation() - { - StringBuilder sb = new StringBuilder(); - try - { - string buildLabVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabRegistryValue); - sb.AppendFormat($"Windows BuildLab version {buildLabVersion}"); - sb.AppendLine(); - - string buildLabExVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabExRegistryValue); - sb.AppendFormat($"Windows BuildLabEx version {buildLabExVersion}"); - sb.AppendLine(); - } - catch (Exception e) - { - sb.AppendFormat($"Failed to record Windows version information. Exception: {e}"); - } - - return sb.ToString(); - } - - public override string GetDataRootForScalar() - { - return WindowsPlatform.GetDataRootForScalarImplementation(); - } - - public override string GetDataRootForScalarComponent(string componentName) - { - return WindowsPlatform.GetDataRootForScalarComponentImplementation(componentName); - } - - public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) - { - string programArguments = string.Empty; - try - { - programArguments = string.Join(" ", args.Select(arg => arg.Contains(' ') ? "\"" + arg + "\"" : arg)); - ProcessStartInfo processInfo = new ProcessStartInfo(programName, programArguments); - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - - Process executingProcess = new Process(); - executingProcess.StartInfo = processInfo; - executingProcess.Start(); - } - catch (Exception ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(programName), programName); - metadata.Add(nameof(programArguments), programArguments); - metadata.Add("Exception", ex.ToString()); - tracer.RelatedError(metadata, "Failed to start background process."); - throw; - } - } - +using Microsoft.Win32; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using Scalar.Platform.Windows.DiskLayoutUpgrades; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.IO.Pipes; +using System.Linq; +using System.Management.Automation; +using System.Security.AccessControl; +using System.Security.Principal; +using System.ServiceProcess; +using System.Text; + +namespace Scalar.Platform.Windows +{ + public partial class WindowsPlatform : ScalarPlatform + { + private const string WindowsVersionRegistryKey = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion"; + private const string BuildLabRegistryValue = "BuildLab"; + private const string BuildLabExRegistryValue = "BuildLabEx"; + + public WindowsPlatform() : base(underConstruction: new UnderConstructionFlags()) + { + } + + public override IGitInstallation GitInstallation { get; } = new WindowsGitInstallation(); + public override IDiskLayoutUpgradeData DiskLayoutUpgrade { get; } = new WindowsDiskLayoutUpgradeData(); + public override IPlatformFileSystem FileSystem { get; } = new WindowsFileSystem(); + public override string Name { get => "Windows"; } + public override ScalarPlatformConstants Constants { get; } = new WindowsPlatformConstants(); + + public override string ScalarConfigPath + { + get + { + string servicePath = ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.ServiceName); + string scalarDirectory = Path.GetDirectoryName(servicePath); + + return Path.Combine(scalarDirectory, LocalScalarConfig.FileName); + } + } + + public static string GetStringFromRegistry(string key, string valueName) + { + object value = GetValueFromRegistry(RegistryHive.LocalMachine, key, valueName); + return value as string; + } + + public static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName) + { + object value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry64); + if (value == null) + { + value = GetValueFromRegistry(registryHive, key, valueName, RegistryView.Registry32); + } + + return value; + } + + public static bool TrySetDWordInRegistry(RegistryHive registryHive, string key, string valueName, uint value) + { + RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry64); + RegistryKey localKeySub = localKey.OpenSubKey(key, writable: true); + + if (localKeySub == null) + { + localKey = RegistryKey.OpenBaseKey(registryHive, RegistryView.Registry32); + localKeySub = localKey.OpenSubKey(key, writable: true); + } + + if (localKeySub == null) + { + return false; + } + + localKeySub.SetValue(valueName, value, RegistryValueKind.DWord); + return true; + } + + public override void InitializeEnlistmentACLs(string enlistmentPath) + { + // The following permissions are typically present on deskop and missing on Server + // + // ACCESS_ALLOWED_ACE_TYPE: NT AUTHORITY\Authenticated Users + // [OBJECT_INHERIT_ACE] + // [CONTAINER_INHERIT_ACE] + // [INHERIT_ONLY_ACE] + // DELETE + // GENERIC_EXECUTE + // GENERIC_WRITE + // GENERIC_READ + DirectorySecurity rootSecurity = Directory.GetAccessControl(enlistmentPath); + AccessRule authenticatedUsersAccessRule = rootSecurity.AccessRuleFactory( + new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), + unchecked((int)(NativeMethods.FileAccess.DELETE | NativeMethods.FileAccess.GENERIC_EXECUTE | NativeMethods.FileAccess.GENERIC_WRITE | NativeMethods.FileAccess.GENERIC_READ)), + true, + InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit, + PropagationFlags.None, + AccessControlType.Allow); + + // The return type of the AccessRuleFactory method is the base class, AccessRule, but the return value can be cast safely to the derived class. + // https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemsecurity.accessrulefactory(v=vs.110).aspx + rootSecurity.AddAccessRule((FileSystemAccessRule)authenticatedUsersAccessRule); + Directory.SetAccessControl(enlistmentPath, rootSecurity); + } + + public override string GetOSVersionInformation() + { + StringBuilder sb = new StringBuilder(); + try + { + string buildLabVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabRegistryValue); + sb.AppendFormat($"Windows BuildLab version {buildLabVersion}"); + sb.AppendLine(); + + string buildLabExVersion = GetStringFromRegistry(WindowsVersionRegistryKey, BuildLabExRegistryValue); + sb.AppendFormat($"Windows BuildLabEx version {buildLabExVersion}"); + sb.AppendLine(); + } + catch (Exception e) + { + sb.AppendFormat($"Failed to record Windows version information. Exception: {e}"); + } + + return sb.ToString(); + } + + public override string GetDataRootForScalar() + { + return WindowsPlatform.GetDataRootForScalarImplementation(); + } + + public override string GetDataRootForScalarComponent(string componentName) + { + return WindowsPlatform.GetDataRootForScalarComponentImplementation(componentName); + } + + public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) + { + string programArguments = string.Empty; + try + { + programArguments = string.Join(" ", args.Select(arg => arg.Contains(' ') ? "\"" + arg + "\"" : arg)); + ProcessStartInfo processInfo = new ProcessStartInfo(programName, programArguments); + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + + Process executingProcess = new Process(); + executingProcess.StartInfo = processInfo; + executingProcess.Start(); + } + catch (Exception ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(programName), programName); + metadata.Add(nameof(programArguments), programArguments); + metadata.Add("Exception", ex.ToString()); + tracer.RelatedError(metadata, "Failed to start background process."); + throw; + } + } + public override void PrepareProcessToRunInBackground() { // No additional work required - } - - public override NamedPipeServerStream CreatePipeByName(string pipeName) - { - PipeSecurity security = new PipeSecurity(); - security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); - security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), PipeAccessRights.FullControl, AccessControlType.Allow)); - security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.FullControl, AccessControlType.Allow)); - - NamedPipeServerStream pipe = new NamedPipeServerStream( - pipeName, - PipeDirection.InOut, - NamedPipeServerStream.MaxAllowedServerInstances, - PipeTransmissionMode.Byte, - PipeOptions.WriteThrough | PipeOptions.Asynchronous, - 0, // default inBufferSize - 0, // default outBufferSize - security, - HandleInheritability.None); - - return pipe; - } - - public override bool IsElevated() - { - return WindowsPlatform.IsElevatedImplementation(); - } - - public override bool IsProcessActive(int processId) - { - return WindowsPlatform.IsProcessActiveImplementation(processId, tryGetProcessById: true); - } - - public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) - { - ServiceController service = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(name, StringComparison.Ordinal)); - - installed = service != null; - running = service != null ? service.Status == ServiceControllerStatus.Running : false; - } - - public override string GetNamedPipeName(string enlistmentRoot) - { - return WindowsPlatform.GetNamedPipeNameImplementation(enlistmentRoot); - } - - public override string GetScalarServiceNamedPipeName(string serviceName) - { - return serviceName + ".pipe"; - } - - public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) - { - try - { - const string GitBinPathEnd = "\\cmd\\git.exe"; - string[] gitVSRegistryKeyNames = - { - "HKEY_CURRENT_USER\\Software\\Microsoft\\VSCommon\\15.0\\TeamFoundation\\GitSourceControl", - "HKEY_CURRENT_USER\\Software\\Microsoft\\VSCommon\\16.0\\TeamFoundation\\GitSourceControl" - }; - const string GitVSRegistryValueName = "GitPath"; - - if (!gitBinPath.EndsWith(GitBinPathEnd)) - { - tracer.RelatedWarning( - "Unable to configure Visual Studio’s GitSourceControl regkey because invalid git.exe path found: " + gitBinPath, - Keywords.Telemetry); - - return; - } - - string regKeyValue = gitBinPath.Substring(0, gitBinPath.Length - GitBinPathEnd.Length); - foreach (string registryKeyName in gitVSRegistryKeyNames) - { - Registry.SetValue(registryKeyName, GitVSRegistryValueName, regKeyValue); - } - } - catch (Exception ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Operation", nameof(this.ConfigureVisualStudio)); - metadata.Add("Exception", ex.ToString()); - tracer.RelatedWarning(metadata, "Error while trying to set Visual Studio’s GitSourceControl regkey"); - } - } - - public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) - { - using (PowerShell powershell = PowerShell.Create()) - { - powershell.AddScript($"Get-AuthenticodeSignature -FilePath {path}"); - - Collection results = powershell.Invoke(); - if (powershell.HadErrors || results.Count <= 0) - { - subject = null; - issuer = null; - error = $"Powershell Get-AuthenticodeSignature failed, could not verify authenticode for {path}."; - return false; - } - - Signature signature = results[0].BaseObject as Signature; - bool isValid = signature.Status == SignatureStatus.Valid; - subject = signature.SignerCertificate.SubjectName.Name; - issuer = signature.SignerCertificate.IssuerName.Name; - error = isValid == false ? signature.StatusMessage : null; - return isValid; - } - } - - public override string GetCurrentUser() - { - WindowsIdentity identity = WindowsIdentity.GetCurrent(); - WindowsPrincipal principal = new WindowsPrincipal(identity); - return identity.User.Value; - } - - public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) - { - using (CurrentUser currentUser = new CurrentUser(tracer, sessionId)) - { - return currentUser.Identity.User.Value; - } - } - + } + + public override NamedPipeServerStream CreatePipeByName(string pipeName) + { + PipeSecurity security = new PipeSecurity(); + security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinUsersSid, null), PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.CreatorOwnerSid, null), PipeAccessRights.FullControl, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null), PipeAccessRights.FullControl, AccessControlType.Allow)); + + NamedPipeServerStream pipe = new NamedPipeServerStream( + pipeName, + PipeDirection.InOut, + NamedPipeServerStream.MaxAllowedServerInstances, + PipeTransmissionMode.Byte, + PipeOptions.WriteThrough | PipeOptions.Asynchronous, + 0, // default inBufferSize + 0, // default outBufferSize + security, + HandleInheritability.None); + + return pipe; + } + + public override bool IsElevated() + { + return WindowsPlatform.IsElevatedImplementation(); + } + + public override bool IsProcessActive(int processId) + { + return WindowsPlatform.IsProcessActiveImplementation(processId, tryGetProcessById: true); + } + + public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) + { + ServiceController service = ServiceController.GetServices().FirstOrDefault(s => s.ServiceName.Equals(name, StringComparison.Ordinal)); + + installed = service != null; + running = service != null ? service.Status == ServiceControllerStatus.Running : false; + } + + public override string GetNamedPipeName(string enlistmentRoot) + { + return WindowsPlatform.GetNamedPipeNameImplementation(enlistmentRoot); + } + + public override string GetScalarServiceNamedPipeName(string serviceName) + { + return serviceName + ".pipe"; + } + + public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) + { + try + { + const string GitBinPathEnd = "\\cmd\\git.exe"; + string[] gitVSRegistryKeyNames = + { + "HKEY_CURRENT_USER\\Software\\Microsoft\\VSCommon\\15.0\\TeamFoundation\\GitSourceControl", + "HKEY_CURRENT_USER\\Software\\Microsoft\\VSCommon\\16.0\\TeamFoundation\\GitSourceControl" + }; + const string GitVSRegistryValueName = "GitPath"; + + if (!gitBinPath.EndsWith(GitBinPathEnd)) + { + tracer.RelatedWarning( + "Unable to configure Visual Studio’s GitSourceControl regkey because invalid git.exe path found: " + gitBinPath, + Keywords.Telemetry); + + return; + } + + string regKeyValue = gitBinPath.Substring(0, gitBinPath.Length - GitBinPathEnd.Length); + foreach (string registryKeyName in gitVSRegistryKeyNames) + { + Registry.SetValue(registryKeyName, GitVSRegistryValueName, regKeyValue); + } + } + catch (Exception ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Operation", nameof(this.ConfigureVisualStudio)); + metadata.Add("Exception", ex.ToString()); + tracer.RelatedWarning(metadata, "Error while trying to set Visual Studio’s GitSourceControl regkey"); + } + } + + public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) + { + using (PowerShell powershell = PowerShell.Create()) + { + powershell.AddScript($"Get-AuthenticodeSignature -FilePath {path}"); + + Collection results = powershell.Invoke(); + if (powershell.HadErrors || results.Count <= 0) + { + subject = null; + issuer = null; + error = $"Powershell Get-AuthenticodeSignature failed, could not verify authenticode for {path}."; + return false; + } + + Signature signature = results[0].BaseObject as Signature; + bool isValid = signature.Status == SignatureStatus.Valid; + subject = signature.SignerCertificate.SubjectName.Name; + issuer = signature.SignerCertificate.IssuerName.Name; + error = isValid == false ? signature.StatusMessage : null; + return isValid; + } + } + + public override string GetCurrentUser() + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + return identity.User.Value; + } + + public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) + { + using (CurrentUser currentUser = new CurrentUser(tracer, sessionId)) + { + return currentUser.Identity.User.Value; + } + } + public override string GetUpgradeLogDirectoryParentDirectory() { return this.GetUpgradeProtectedDataDirectory(); @@ -309,20 +309,20 @@ public override string GetUpgradeProtectedDataDirectory() return GetUpgradeProtectedDataDirectoryImplementation(); } - public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) => WindowsPhysicalDiskInfo.GetPhysicalDiskInfo(path, sizeStatsOnly); - - public override bool IsConsoleOutputRedirectedToFile() - { - return WindowsPlatform.IsConsoleOutputRedirectedToFileImplementation(); - } - - public override FileBasedLock CreateFileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - { - return new WindowsFileBasedLock(fileSystem, tracer, lockPath); - } + public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) => WindowsPhysicalDiskInfo.GetPhysicalDiskInfo(path, sizeStatsOnly); + + public override bool IsConsoleOutputRedirectedToFile() + { + return WindowsPlatform.IsConsoleOutputRedirectedToFileImplementation(); + } + + public override FileBasedLock CreateFileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + { + return new WindowsFileBasedLock(fileSystem, tracer, lockPath); + } public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInteractions( PhysicalFileSystem fileSystem, @@ -330,97 +330,97 @@ public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInt { return new WindowsProductUpgraderPlatformStrategy(fileSystem, tracer); } - - public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) - { - return WindowsPlatform.TryGetScalarEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage); - } - + + public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) + { + return WindowsPlatform.TryGetScalarEnlistmentRootImplementation(directory, out enlistmentRoot, out errorMessage); + } + public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError) - { - string pathRoot; - + { + string pathRoot; + try - { + { pathRoot = Path.GetPathRoot(enlistmentRoot); - } - catch (ArgumentException e) - { - localCacheRoot = null; - localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}'): {e.Message}"; + } + catch (ArgumentException e) + { + localCacheRoot = null; + localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}'): {e.Message}"; return false; } - if (string.IsNullOrEmpty(pathRoot)) - { - localCacheRoot = null; - localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}', path does not contain root directory information"; - return false; + if (string.IsNullOrEmpty(pathRoot)) + { + localCacheRoot = null; + localCacheRootError = $"Failed to determine the root of '{enlistmentRoot}', path does not contain root directory information"; + return false; } - try - { + try + { localCacheRoot = Path.Combine(pathRoot, ScalarConstants.DefaultScalarCacheFolderName); localCacheRootError = null; - return true; - } - catch (ArgumentException e) - { - localCacheRoot = null; - localCacheRootError = $"Failed to build local cache path using root directory '{pathRoot}'): {e.Message}"; - return false; + return true; + } + catch (ArgumentException e) + { + localCacheRoot = null; + localCacheRootError = $"Failed to build local cache path using root directory '{pathRoot}'): {e.Message}"; + return false; + } + } + + public override bool TryKillProcessTree(int processId, out int exitCode, out string error) + { + ProcessResult result = ProcessHelper.Run("taskkill", $"/pid {processId} /f /t"); + error = result.Errors; + exitCode = result.ExitCode; + return result.ExitCode == 0; + } + + private static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName, RegistryView view) + { + RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, view); + RegistryKey localKeySub = localKey.OpenSubKey(key); + + object value = localKeySub == null ? null : localKeySub.GetValue(valueName); + return value; + } + + public class WindowsPlatformConstants : ScalarPlatformConstants + { + public override string ExecutableExtension + { + get { return ".exe"; } + } + + public override string InstallerExtension + { + get { return ".exe"; } } - } - - public override bool TryKillProcessTree(int processId, out int exitCode, out string error) - { - ProcessResult result = ProcessHelper.Run("taskkill", $"/pid {processId} /f /t"); - error = result.Errors; - exitCode = result.ExitCode; - return result.ExitCode == 0; - } - - private static object GetValueFromRegistry(RegistryHive registryHive, string key, string valueName, RegistryView view) - { - RegistryKey localKey = RegistryKey.OpenBaseKey(registryHive, view); - RegistryKey localKeySub = localKey.OpenSubKey(key); - - object value = localKeySub == null ? null : localKeySub.GetValue(valueName); - return value; - } - - public class WindowsPlatformConstants : ScalarPlatformConstants - { - public override string ExecutableExtension - { - get { return ".exe"; } - } - - public override string InstallerExtension - { - get { return ".exe"; } - } public override bool SupportsUpgradeWhileRunning => false; public override string WorkingDirectoryBackingRootPath { get { return ScalarConstants.WorkingDirectoryRootName; } - } - - public override string DotScalarRoot - { - get { return WindowsPlatform.DotScalarRoot; } - } - - public override string ScalarBinDirectoryPath - { - get - { - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), - this.ScalarBinDirectoryName); - } + } + + public override string DotScalarRoot + { + get { return WindowsPlatform.DotScalarRoot; } + } + + public override string ScalarBinDirectoryPath + { + get + { + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), + this.ScalarBinDirectoryName); + } } public override string ScalarBinDirectoryName @@ -431,20 +431,20 @@ public override string ScalarBinDirectoryName public override string ScalarExecutableName { get { return "Scalar" + this.ExecutableExtension; } - } - - public override string ProgramLocaterCommand - { - get { return "where"; } } - public override HashSet UpgradeBlockingProcesses - { - get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar", "Scalar.Mount", "git", "ssh-agent", "wish", "bash" }; } - } - - // Tests show that 250 is the max supported pipe name length - public override int MaxPipePathLength => 250; - } - } -} + public override string ProgramLocaterCommand + { + get { return "where"; } + } + + public override HashSet UpgradeBlockingProcesses + { + get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar", "Scalar.Mount", "git", "ssh-agent", "wish", "bash" }; } + } + + // Tests show that 250 is the max supported pipe name length + public override int MaxPipePathLength => 250; + } + } +} diff --git a/Scalar.Platform.Windows/WindowsProductUpgraderPlatformStrategy.cs b/Scalar.Platform.Windows/WindowsProductUpgraderPlatformStrategy.cs index ccc7f10ded..77d37e4a46 100644 --- a/Scalar.Platform.Windows/WindowsProductUpgraderPlatformStrategy.cs +++ b/Scalar.Platform.Windows/WindowsProductUpgraderPlatformStrategy.cs @@ -1,75 +1,75 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.IO; - -namespace Scalar.Platform.Windows -{ - public class WindowsProductUpgraderPlatformStrategy : ProductUpgraderPlatformStrategy - { - public WindowsProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) - : base(fileSystem, tracer) - { - } - - public override bool TryPrepareLogDirectory(out string error) - { - // Under normal circumstances - // ProductUpgraderInfo.GetLogDirectoryPath will have - // already been created by Scalar.Service. If for some - // reason it does not (e.g. the service failed to start), - // we need to create - // ProductUpgraderInfo.GetLogDirectoryPath() explicity to - // ensure that it has the correct ACLs (so that both admin - // and non-admin users can create log files). If the logs - // directory does not already exist, this call could fail - // when running as a non-elevated user. - string createDirectoryError; - if (!this.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(ProductUpgraderInfo.GetLogDirectoryPath(), out createDirectoryError)) - { - error = $"ERROR: Unable to create directory `{ProductUpgraderInfo.GetLogDirectoryPath()}`"; - error += $"\n{createDirectoryError}"; - error += $"\n\nTry running {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} from an elevated command prompt."; - return false; - } - - error = null; - return true; - } - - public override bool TryPrepareApplicationDirectory(out string error) - { - string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); - - Exception deleteDirectoryException; - if (this.FileSystem.DirectoryExists(upgradeApplicationDirectory) && - !this.FileSystem.TryDeleteDirectory(upgradeApplicationDirectory, out deleteDirectoryException)) - { - error = $"Failed to delete {upgradeApplicationDirectory} - {deleteDirectoryException.Message}"; - - this.TraceException(deleteDirectoryException, nameof(this.TryPrepareApplicationDirectory), $"Error deleting {upgradeApplicationDirectory}."); - return false; - } - - if (!this.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions( - this.Tracer, - upgradeApplicationDirectory, - out error)) - { - return false; - } - - error = null; - return true; - } - - public override bool TryPrepareDownloadDirectory(out string error) - { - return this.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions( - this.Tracer, - ProductUpgraderInfo.GetAssetDownloadsPath(), - out error); - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.IO; + +namespace Scalar.Platform.Windows +{ + public class WindowsProductUpgraderPlatformStrategy : ProductUpgraderPlatformStrategy + { + public WindowsProductUpgraderPlatformStrategy(PhysicalFileSystem fileSystem, ITracer tracer) + : base(fileSystem, tracer) + { + } + + public override bool TryPrepareLogDirectory(out string error) + { + // Under normal circumstances + // ProductUpgraderInfo.GetLogDirectoryPath will have + // already been created by Scalar.Service. If for some + // reason it does not (e.g. the service failed to start), + // we need to create + // ProductUpgraderInfo.GetLogDirectoryPath() explicity to + // ensure that it has the correct ACLs (so that both admin + // and non-admin users can create log files). If the logs + // directory does not already exist, this call could fail + // when running as a non-elevated user. + string createDirectoryError; + if (!this.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(ProductUpgraderInfo.GetLogDirectoryPath(), out createDirectoryError)) + { + error = $"ERROR: Unable to create directory `{ProductUpgraderInfo.GetLogDirectoryPath()}`"; + error += $"\n{createDirectoryError}"; + error += $"\n\nTry running {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} from an elevated command prompt."; + return false; + } + + error = null; + return true; + } + + public override bool TryPrepareApplicationDirectory(out string error) + { + string upgradeApplicationDirectory = ProductUpgraderInfo.GetUpgradeApplicationDirectory(); + + Exception deleteDirectoryException; + if (this.FileSystem.DirectoryExists(upgradeApplicationDirectory) && + !this.FileSystem.TryDeleteDirectory(upgradeApplicationDirectory, out deleteDirectoryException)) + { + error = $"Failed to delete {upgradeApplicationDirectory} - {deleteDirectoryException.Message}"; + + this.TraceException(deleteDirectoryException, nameof(this.TryPrepareApplicationDirectory), $"Error deleting {upgradeApplicationDirectory}."); + return false; + } + + if (!this.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions( + this.Tracer, + upgradeApplicationDirectory, + out error)) + { + return false; + } + + error = null; + return true; + } + + public override bool TryPrepareDownloadDirectory(out string error) + { + return this.FileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissions( + this.Tracer, + ProductUpgraderInfo.GetAssetDownloadsPath(), + out error); + } + } +} diff --git a/Scalar.Platform.Windows/packages.config b/Scalar.Platform.Windows/packages.config index 540b515283..7e7d0cf886 100644 --- a/Scalar.Platform.Windows/packages.config +++ b/Scalar.Platform.Windows/packages.config @@ -1,7 +1,7 @@ - - - - - - + + + + + + diff --git a/Scalar.PlatformLoader/PlatformLoader.Mac.cs b/Scalar.PlatformLoader/PlatformLoader.Mac.cs index 1e0b6f5044..2cc43af27b 100644 --- a/Scalar.PlatformLoader/PlatformLoader.Mac.cs +++ b/Scalar.PlatformLoader/PlatformLoader.Mac.cs @@ -1,15 +1,15 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Platform.Mac; - -namespace Scalar.PlatformLoader -{ - public static class ScalarPlatformLoader - { - public static void Initialize() - { - ScalarPlatform.Register(new MacPlatform()); - return; - } - } +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Platform.Mac; + +namespace Scalar.PlatformLoader +{ + public static class ScalarPlatformLoader + { + public static void Initialize() + { + ScalarPlatform.Register(new MacPlatform()); + return; + } + } } diff --git a/Scalar.PlatformLoader/PlatformLoader.Windows.cs b/Scalar.PlatformLoader/PlatformLoader.Windows.cs index bc8d595ba4..ef92ee1770 100644 --- a/Scalar.PlatformLoader/PlatformLoader.Windows.cs +++ b/Scalar.PlatformLoader/PlatformLoader.Windows.cs @@ -1,14 +1,14 @@ -using Scalar.Common; -using Scalar.Platform.Windows; - -namespace Scalar.PlatformLoader -{ - public static class ScalarPlatformLoader - { - public static void Initialize() - { - ScalarPlatform.Register(new WindowsPlatform()); - return; - } - } +using Scalar.Common; +using Scalar.Platform.Windows; + +namespace Scalar.PlatformLoader +{ + public static class ScalarPlatformLoader + { + public static void Initialize() + { + ScalarPlatform.Register(new WindowsPlatform()); + return; + } + } } diff --git a/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj b/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj index 4a731e8491..10f556be6d 100644 --- a/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj +++ b/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj @@ -1,130 +1,130 @@ - - - - - Debug - x64 - - - Release - x64 - - - - {5A6656D5-81C7-472C-9DC8-32D071CB2258} - Win32Proj - readobject - 10.0.17763.0 - Scalar.ReadObjectHook.Windows - Scalar.ReadObjectHook - - - - - Application - true - v141 - MultiByte - - - Application - false - v141 - true - MultiByte - - - - - - - - - - - - - - - true - - - false - - - - Use - Level4 - Disabled - _DEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - true - C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt;$(SolutionDir)\Scalar.NativeHooks.Common;%(AdditionalIncludeDirectories) - /Zc:__cplusplus - - - Console - true - C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64;%(AdditionalLibraryDirectories) - - - $(BuildOutputDir)\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log - - - - - $(BuildOutputDir) - - - - - Level4 - Use - MaxSpeed - true - true - NDEBUG;_CONSOLE;%(PreprocessorDefinitions) - true - true - C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt;$(SolutionDir)\Scalar.NativeHooks.Common;%(AdditionalIncludeDirectories) - /Zc:__cplusplus - - - Console - true - true - true - C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64;%(AdditionalLibraryDirectories) - - - $(BuildOutputDir)\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log - - - - - $(BuildOutputDir) - - - - - - - - - - - - - - - Create - Create - - - - - - - - + + + + + Debug + x64 + + + Release + x64 + + + + {5A6656D5-81C7-472C-9DC8-32D071CB2258} + Win32Proj + readobject + 10.0.17763.0 + Scalar.ReadObjectHook.Windows + Scalar.ReadObjectHook + + + + + Application + true + v141 + MultiByte + + + Application + false + v141 + true + MultiByte + + + + + + + + + + + + + + + true + + + false + + + + Use + Level4 + Disabled + _DEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt;$(SolutionDir)\Scalar.NativeHooks.Common;%(AdditionalIncludeDirectories) + /Zc:__cplusplus + + + Console + true + C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64;%(AdditionalLibraryDirectories) + + + $(BuildOutputDir)\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + + + $(BuildOutputDir) + + + + + Level4 + Use + MaxSpeed + true + true + NDEBUG;_CONSOLE;%(PreprocessorDefinitions) + true + true + C:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0\ucrt;$(SolutionDir)\Scalar.NativeHooks.Common;%(AdditionalIncludeDirectories) + /Zc:__cplusplus + + + Console + true + true + true + C:\Program Files (x86)\Windows Kits\10\Lib\10.0.10240.0\ucrt\x64;%(AdditionalLibraryDirectories) + + + $(BuildOutputDir)\$(ProjectName)\intermediate\$(Platform)\$(Configuration)\$(MSBuildProjectName).log + + + + + $(BuildOutputDir) + + + + + + + + + + + + + + + Create + Create + + + + + + + + \ No newline at end of file diff --git a/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj.filters b/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj.filters index 5587413a86..b7c3c28c71 100644 --- a/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj.filters +++ b/Scalar.ReadObjectHook/Scalar.ReadObjectHook.Windows.vcxproj.filters @@ -1,59 +1,59 @@ - - - - - {4FC737F1-C7A5-4376-A066-2A32D752A2FF} - cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx - - - {93995380-89BD-4b04-88EB-625FBE52EBFB} - h;hh;hpp;hxx;hm;inl;inc;xsd - - - {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - - - {c3243239-d853-4df9-bdbb-9a4efa72a827} - - - {e6c30bd2-e246-47d9-ad10-137165f4628c} - - - - - Header Files - - - Header Files - - - Header Files - - - Header Files - - - Shared Header Files - - - - - Source Files - - - Source Files - - - Source Files - - - Shared Source Files - - - - - Resource Files - - + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;hm;inl;inc;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {c3243239-d853-4df9-bdbb-9a4efa72a827} + + + {e6c30bd2-e246-47d9-ad10-137165f4628c} + + + + + Header Files + + + Header Files + + + Header Files + + + Header Files + + + Shared Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + Shared Source Files + + + + + Resource Files + + diff --git a/Scalar.ReadObjectHook/Version.rc b/Scalar.ReadObjectHook/Version.rc index c575a5f830..b2618c4ef1 100644 Binary files a/Scalar.ReadObjectHook/Version.rc and b/Scalar.ReadObjectHook/Version.rc differ diff --git a/Scalar.ReadObjectHook/main.cpp b/Scalar.ReadObjectHook/main.cpp index 71187a238f..9674a1b74a 100644 --- a/Scalar.ReadObjectHook/main.cpp +++ b/Scalar.ReadObjectHook/main.cpp @@ -1,150 +1,150 @@ -// Scalar.ReadObjectHook -// -// When Scalar installs Scalar.ReadObjectHook, it copies the file to -// the .git\hooks folder, and renames the executable to read-object -// read-object is called by git when it fails to find the object it's looking for on disk. -// -// Git and read-object negotiate an interface and capabilities then git issues a "get" command for the missing SHA. -// See Git Documentation/Technical/read-object-protocol.txt for details. -// Scalar.ReadObjectHook decides which Scalar instance to connect to based on its path. -// It then connects to Scalar and asks Scalar to download the requested object (to the .git\objects folder). - -#include "stdafx.h" +// Scalar.ReadObjectHook +// +// When Scalar installs Scalar.ReadObjectHook, it copies the file to +// the .git\hooks folder, and renames the executable to read-object +// read-object is called by git when it fails to find the object it's looking for on disk. +// +// Git and read-object negotiate an interface and capabilities then git issues a "get" command for the missing SHA. +// See Git Documentation/Technical/read-object-protocol.txt for details. +// Scalar.ReadObjectHook decides which Scalar instance to connect to based on its path. +// It then connects to Scalar and asks Scalar to download the requested object (to the .git\objects folder). + +#include "stdafx.h" #include "packet.h" -#include "common.h" - -#define MAX_PACKET_LENGTH 512 -#define SHA1_LENGTH 40 -#define DLO_REQUEST_LENGTH (4 + SHA1_LENGTH + 1) - +#include "common.h" + +#define MAX_PACKET_LENGTH 512 +#define SHA1_LENGTH 40 +#define DLO_REQUEST_LENGTH (4 + SHA1_LENGTH + 1) + // Expected response: // "S\x3" -> Success // "F\x3" -> Failure #define DLO_RESPONSE_LENGTH 2 - -enum ReadObjectHookErrorReturnCode -{ - ErrorReadObjectProtocol = ReturnCode::LastError + 1, -}; - -int DownloadSHA(PIPE_HANDLE pipeHandle, const char *sha1) -{ - // Construct download request message - // Format: "DLO|<40 character SHA>" - // Example: "DLO|920C34DCDDFC8F07AC4704C8C0D087D6F2095729" - char request[DLO_REQUEST_LENGTH+1]; - if (snprintf(request, DLO_REQUEST_LENGTH+1, "DLO|%s\x3", sha1) != DLO_REQUEST_LENGTH) - { - die(ReturnCode::InvalidSHA, "First argument must be a 40 character SHA, actual value: %s\n", sha1); - } - + +enum ReadObjectHookErrorReturnCode +{ + ErrorReadObjectProtocol = ReturnCode::LastError + 1, +}; + +int DownloadSHA(PIPE_HANDLE pipeHandle, const char *sha1) +{ + // Construct download request message + // Format: "DLO|<40 character SHA>" + // Example: "DLO|920C34DCDDFC8F07AC4704C8C0D087D6F2095729" + char request[DLO_REQUEST_LENGTH+1]; + if (snprintf(request, DLO_REQUEST_LENGTH+1, "DLO|%s\x3", sha1) != DLO_REQUEST_LENGTH) + { + die(ReturnCode::InvalidSHA, "First argument must be a 40 character SHA, actual value: %s\n", sha1); + } + unsigned long bytesWritten; - int error = 0; + int error = 0; bool success = WriteToPipe( pipeHandle, request, - DLO_REQUEST_LENGTH, + DLO_REQUEST_LENGTH, &bytesWritten, - &error); - - if (!success || bytesWritten != DLO_REQUEST_LENGTH) - { - die(ReturnCode::PipeWriteFailed, "Failed to write to pipe (%d)\n", error); + &error); + + if (!success || bytesWritten != DLO_REQUEST_LENGTH) + { + die(ReturnCode::PipeWriteFailed, "Failed to write to pipe (%d)\n", error); } char response[DLO_RESPONSE_LENGTH]; unsigned long totalBytesRead = 0; - error = 0; - do + error = 0; + do { - unsigned long bytesRead = 0; + unsigned long bytesRead = 0; success = ReadFromPipe( pipeHandle, response + totalBytesRead, sizeof(response) - (sizeof(char) * totalBytesRead), &bytesRead, &error); - totalBytesRead += bytesRead; + totalBytesRead += bytesRead; } while (success && totalBytesRead < DLO_RESPONSE_LENGTH); - - if (!success) - { - die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%d)\n", error); - } - - return *response == 'S' ? ReturnCode::Success : ReturnCode::FailureToDownload; -} - -int main(int, char *argv[]) -{ - char packet_buffer[MAX_PACKET_LENGTH]; - size_t len; - int err; - - DisableCRLFTranslationOnStdPipes(); - - packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "git-read-object-client")) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad welcome message\n"); - } - - packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "version=1")) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version\n"); - } - - if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version end\n"); - } - - packet_txt_write("git-read-object-server"); - packet_txt_write("version=1"); - packet_flush(); - - packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "capability=get")) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability\n"); - } - - if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability end\n"); - } - - packet_txt_write("capability=get"); - packet_flush(); - - PATH_STRING pipeName(GetScalarPipeName(argv[0])); - - PIPE_HANDLE pipeHandle = CreatePipeToScalar(pipeName); - - while (1) - { - packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if (strcmp(packet_buffer, "command=get")) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command\n"); - } - - len = packet_txt_read(packet_buffer, sizeof(packet_buffer)); - if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad sha1 in get command\n"); - } - - if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) - { - die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command end\n"); - } - - err = DownloadSHA(pipeHandle, packet_buffer + 5); - packet_txt_write(err ? "status=error" : "status=success"); - packet_flush(); - } - - // we'll never reach here as the signal to exit is having stdin closed which is handled in packet_bin_read -} + + if (!success) + { + die(ReturnCode::PipeReadFailed, "Read response from pipe failed (%d)\n", error); + } + + return *response == 'S' ? ReturnCode::Success : ReturnCode::FailureToDownload; +} + +int main(int, char *argv[]) +{ + char packet_buffer[MAX_PACKET_LENGTH]; + size_t len; + int err; + + DisableCRLFTranslationOnStdPipes(); + + packet_txt_read(packet_buffer, sizeof(packet_buffer)); + if (strcmp(packet_buffer, "git-read-object-client")) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad welcome message\n"); + } + + packet_txt_read(packet_buffer, sizeof(packet_buffer)); + if (strcmp(packet_buffer, "version=1")) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version\n"); + } + + if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad version end\n"); + } + + packet_txt_write("git-read-object-server"); + packet_txt_write("version=1"); + packet_flush(); + + packet_txt_read(packet_buffer, sizeof(packet_buffer)); + if (strcmp(packet_buffer, "capability=get")) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability\n"); + } + + if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad capability end\n"); + } + + packet_txt_write("capability=get"); + packet_flush(); + + PATH_STRING pipeName(GetScalarPipeName(argv[0])); + + PIPE_HANDLE pipeHandle = CreatePipeToScalar(pipeName); + + while (1) + { + packet_txt_read(packet_buffer, sizeof(packet_buffer)); + if (strcmp(packet_buffer, "command=get")) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command\n"); + } + + len = packet_txt_read(packet_buffer, sizeof(packet_buffer)); + if ((len != SHA1_LENGTH + 5) || strncmp(packet_buffer, "sha1=", 5)) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad sha1 in get command\n"); + } + + if (packet_txt_read(packet_buffer, sizeof(packet_buffer))) + { + die(ReadObjectHookErrorReturnCode::ErrorReadObjectProtocol, "Bad command end\n"); + } + + err = DownloadSHA(pipeHandle, packet_buffer + 5); + packet_txt_write(err ? "status=error" : "status=success"); + packet_flush(); + } + + // we'll never reach here as the signal to exit is having stdin closed which is handled in packet_bin_read +} diff --git a/Scalar.ReadObjectHook/packet.cpp b/Scalar.ReadObjectHook/packet.cpp index 35f58fa1c7..50d63e41d5 100644 --- a/Scalar.ReadObjectHook/packet.cpp +++ b/Scalar.ReadObjectHook/packet.cpp @@ -1,156 +1,156 @@ -#include "stdafx.h" -#include "packet.h" -#include "common.h" - -static void set_packet_header(char *buf, const size_t size) -{ - static char hexchar[] = "0123456789abcdef"; - -#define hex(a) (hexchar[(a) & 15]) - buf[0] = hex(size >> 12); - buf[1] = hex(size >> 8); - buf[2] = hex(size >> 4); - buf[3] = hex(size); -#undef hex -} - -const signed char hexval_table[256] = { - -1, -1, -1, -1, -1, -1, -1, -1, /* 00-07 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 08-0f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 10-17 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 18-1f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 20-27 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 28-2f */ - 0, 1, 2, 3, 4, 5, 6, 7, /* 30-37 */ - 8, 9, -1, -1, -1, -1, -1, -1, /* 38-3f */ - -1, 10, 11, 12, 13, 14, 15, -1, /* 40-47 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 48-4f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 50-57 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 58-5f */ - -1, 10, 11, 12, 13, 14, 15, -1, /* 60-67 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 68-67 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 70-77 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 78-7f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 80-87 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 88-8f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 90-97 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* 98-9f */ - -1, -1, -1, -1, -1, -1, -1, -1, /* a0-a7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* a8-af */ - -1, -1, -1, -1, -1, -1, -1, -1, /* b0-b7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* b8-bf */ - -1, -1, -1, -1, -1, -1, -1, -1, /* c0-c7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* c8-cf */ - -1, -1, -1, -1, -1, -1, -1, -1, /* d0-d7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* d8-df */ - -1, -1, -1, -1, -1, -1, -1, -1, /* e0-e7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* e8-ef */ - -1, -1, -1, -1, -1, -1, -1, -1, /* f0-f7 */ - -1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */ -}; - -static inline unsigned int hexval(unsigned char c) -{ - return hexval_table[c]; -} - -static inline int hex2chr(const char *s) -{ - int val = hexval(s[0]); - return (val < 0) ? val : (val << 4) | hexval(s[1]); -} - -static int packet_length(const char *packetlen) -{ - int val = hex2chr(packetlen); - return (val < 0) ? val : (val << 8) | hex2chr(packetlen + 2); -} - -static size_t packet_bin_read(void *buf, size_t count, FILE *stream) -{ - char packetlen[4]; - size_t len, ret; - - /* if we timeout waiting for input, exit and git will restart us if needed */ - size_t bytes_read = fread(packetlen, 1, 4, stream); - if (0 == bytes_read) - { - exit(0); - } - if (4 != bytes_read) - { - die(-1, "invalid packet length"); - } - - len = packet_length(packetlen); - if (!len) - { - return 0; - } - if (len < 4) - { - die(-1, "protocol error: bad line length character: %.4s", packetlen); - } - len -= 4; - if (len >= count) - { - die(-1, "protocol error: bad line length %zu", len); - } - ret = fread(buf, 1, len, stream); - if (ret != len) - { - die(-1, "invalid packet (%zu bytes expected; %zu bytes read)", len, ret); - } - - return len; -} - -size_t packet_txt_read(char *buf, size_t count, FILE *stream) -{ - size_t len; - - len = packet_bin_read(buf, count, stream); - if (len && buf[len - 1] == '\n') - { - len--; - } - - buf[len] = 0; - return len; -} - -void packet_txt_write(const char *buf, FILE *stream) -{ - char packetlen[4]; - size_t len, count = strlen(buf); - - set_packet_header(packetlen, count + 5); - len = fwrite(packetlen, 1, 4, stream); - if (len != 4) - { - die(-1, "error writing packet length"); - } - len = fwrite(buf, 1, count, stream); - if (len != count) - { - die(-1, "error writing packet"); - } - len = fwrite("\n", 1, 1, stream); - if (len != 1) - { - die(-1, "error writing packet"); - } - fflush(stream); -} - -void packet_flush(FILE *stream) -{ - size_t len; - - len = fwrite("0000", 1, 4, stream); - if (len != 4) - { - die(-1, "error writing flush packet"); - } - fflush(stream); -} +#include "stdafx.h" +#include "packet.h" +#include "common.h" + +static void set_packet_header(char *buf, const size_t size) +{ + static char hexchar[] = "0123456789abcdef"; + +#define hex(a) (hexchar[(a) & 15]) + buf[0] = hex(size >> 12); + buf[1] = hex(size >> 8); + buf[2] = hex(size >> 4); + buf[3] = hex(size); +#undef hex +} + +const signed char hexval_table[256] = { + -1, -1, -1, -1, -1, -1, -1, -1, /* 00-07 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 08-0f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 10-17 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 18-1f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 20-27 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 28-2f */ + 0, 1, 2, 3, 4, 5, 6, 7, /* 30-37 */ + 8, 9, -1, -1, -1, -1, -1, -1, /* 38-3f */ + -1, 10, 11, 12, 13, 14, 15, -1, /* 40-47 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 48-4f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 50-57 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 58-5f */ + -1, 10, 11, 12, 13, 14, 15, -1, /* 60-67 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 68-67 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 70-77 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 78-7f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 80-87 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 88-8f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 90-97 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* 98-9f */ + -1, -1, -1, -1, -1, -1, -1, -1, /* a0-a7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* a8-af */ + -1, -1, -1, -1, -1, -1, -1, -1, /* b0-b7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* b8-bf */ + -1, -1, -1, -1, -1, -1, -1, -1, /* c0-c7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* c8-cf */ + -1, -1, -1, -1, -1, -1, -1, -1, /* d0-d7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* d8-df */ + -1, -1, -1, -1, -1, -1, -1, -1, /* e0-e7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* e8-ef */ + -1, -1, -1, -1, -1, -1, -1, -1, /* f0-f7 */ + -1, -1, -1, -1, -1, -1, -1, -1, /* f8-ff */ +}; + +static inline unsigned int hexval(unsigned char c) +{ + return hexval_table[c]; +} + +static inline int hex2chr(const char *s) +{ + int val = hexval(s[0]); + return (val < 0) ? val : (val << 4) | hexval(s[1]); +} + +static int packet_length(const char *packetlen) +{ + int val = hex2chr(packetlen); + return (val < 0) ? val : (val << 8) | hex2chr(packetlen + 2); +} + +static size_t packet_bin_read(void *buf, size_t count, FILE *stream) +{ + char packetlen[4]; + size_t len, ret; + + /* if we timeout waiting for input, exit and git will restart us if needed */ + size_t bytes_read = fread(packetlen, 1, 4, stream); + if (0 == bytes_read) + { + exit(0); + } + if (4 != bytes_read) + { + die(-1, "invalid packet length"); + } + + len = packet_length(packetlen); + if (!len) + { + return 0; + } + if (len < 4) + { + die(-1, "protocol error: bad line length character: %.4s", packetlen); + } + len -= 4; + if (len >= count) + { + die(-1, "protocol error: bad line length %zu", len); + } + ret = fread(buf, 1, len, stream); + if (ret != len) + { + die(-1, "invalid packet (%zu bytes expected; %zu bytes read)", len, ret); + } + + return len; +} + +size_t packet_txt_read(char *buf, size_t count, FILE *stream) +{ + size_t len; + + len = packet_bin_read(buf, count, stream); + if (len && buf[len - 1] == '\n') + { + len--; + } + + buf[len] = 0; + return len; +} + +void packet_txt_write(const char *buf, FILE *stream) +{ + char packetlen[4]; + size_t len, count = strlen(buf); + + set_packet_header(packetlen, count + 5); + len = fwrite(packetlen, 1, 4, stream); + if (len != 4) + { + die(-1, "error writing packet length"); + } + len = fwrite(buf, 1, count, stream); + if (len != count) + { + die(-1, "error writing packet"); + } + len = fwrite("\n", 1, 1, stream); + if (len != 1) + { + die(-1, "error writing packet"); + } + fflush(stream); +} + +void packet_flush(FILE *stream) +{ + size_t len; + + len = fwrite("0000", 1, 4, stream); + if (len != 4) + { + die(-1, "error writing flush packet"); + } + fflush(stream); +} diff --git a/Scalar.ReadObjectHook/packet.h b/Scalar.ReadObjectHook/packet.h index e45a3ed42a..7de2697466 100644 --- a/Scalar.ReadObjectHook/packet.h +++ b/Scalar.ReadObjectHook/packet.h @@ -1,6 +1,6 @@ -#pragma once -#include - -size_t packet_txt_read(char *buf, size_t count, FILE *stream = stdin); -void packet_txt_write(const char *buf, FILE *stream = stdout); -void packet_flush(FILE *stream = stdout); +#pragma once +#include + +size_t packet_txt_read(char *buf, size_t count, FILE *stream = stdin); +void packet_txt_write(const char *buf, FILE *stream = stdout); +void packet_flush(FILE *stream = stdout); diff --git a/Scalar.ReadObjectHook/resource.h b/Scalar.ReadObjectHook/resource.h index c1b5c15962..7bc0b68ce0 100644 --- a/Scalar.ReadObjectHook/resource.h +++ b/Scalar.ReadObjectHook/resource.h @@ -1,14 +1,14 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Version.rc - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 101 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Version.rc + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 101 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/Scalar.ReadObjectHook/stdafx.cpp b/Scalar.ReadObjectHook/stdafx.cpp index bf9cec435a..d408aabbd0 100644 --- a/Scalar.ReadObjectHook/stdafx.cpp +++ b/Scalar.ReadObjectHook/stdafx.cpp @@ -1,8 +1,8 @@ -// stdafx.cpp : source file that includes just the standard includes -// Scalar.ReadObjectHook.pch will be the pre-compiled header -// stdafx.obj will contain the pre-compiled type information - -#include "stdafx.h" - -// TODO: reference any additional headers you need in STDAFX.H -// and not in this file +// stdafx.cpp : source file that includes just the standard includes +// Scalar.ReadObjectHook.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" + +// TODO: reference any additional headers you need in STDAFX.H +// and not in this file diff --git a/Scalar.ReadObjectHook/stdafx.h b/Scalar.ReadObjectHook/stdafx.h index bf75a27dc9..660b922b92 100644 --- a/Scalar.ReadObjectHook/stdafx.h +++ b/Scalar.ReadObjectHook/stdafx.h @@ -1,9 +1,9 @@ -// stdafx.h : include file for standard system include files, -// or project specific include files that are used frequently, but -// are changed infrequently -// - -#pragma once +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, but +// are changed infrequently +// + +#pragma once #ifdef _WIN32 #include "targetver.h" diff --git a/Scalar.ReadObjectHook/targetver.h b/Scalar.ReadObjectHook/targetver.h index 90e767bfce..87c0086de7 100644 --- a/Scalar.ReadObjectHook/targetver.h +++ b/Scalar.ReadObjectHook/targetver.h @@ -1,8 +1,8 @@ -#pragma once - -// Including SDKDDKVer.h defines the highest available Windows platform. - -// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and -// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. - -#include +#pragma once + +// Including SDKDDKVer.h defines the highest available Windows platform. + +// If you wish to build your application for a previous Windows platform, include WinSDKVer.h and +// set the _WIN32_WINNT macro to the platform you wish to support before including SDKDDKVer.h. + +#include diff --git a/Scalar.Service.UI/Data/ActionItem.cs b/Scalar.Service.UI/Data/ActionItem.cs index 7a464dbdc2..f5a8dc9ce8 100644 --- a/Scalar.Service.UI/Data/ActionItem.cs +++ b/Scalar.Service.UI/Data/ActionItem.cs @@ -1,13 +1,13 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - public class ActionItem - { - [XmlElement("content")] - public string Content { get; set; } - - [XmlElement("arguments")] - public string Arguments { get; set; } - } +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + public class ActionItem + { + [XmlElement("content")] + public string Content { get; set; } + + [XmlElement("arguments")] + public string Arguments { get; set; } + } } diff --git a/Scalar.Service.UI/Data/ActionsData.cs b/Scalar.Service.UI/Data/ActionsData.cs index be75f90390..5679a3b81f 100644 --- a/Scalar.Service.UI/Data/ActionsData.cs +++ b/Scalar.Service.UI/Data/ActionsData.cs @@ -1,10 +1,10 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - public class ActionsData - { - [XmlAnyElement("actions")] - public XmlList Actions { get; set; } - } -} +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + public class ActionsData + { + [XmlAnyElement("actions")] + public XmlList Actions { get; set; } + } +} diff --git a/Scalar.Service.UI/Data/BindingData.cs b/Scalar.Service.UI/Data/BindingData.cs index 8440934844..e3c4823880 100644 --- a/Scalar.Service.UI/Data/BindingData.cs +++ b/Scalar.Service.UI/Data/BindingData.cs @@ -1,13 +1,13 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - public class BindingData - { - [XmlAttribute("template")] - public string Template { get; set; } - - [XmlAnyElement] - public XmlList Items { get; set; } - } -} +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + public class BindingData + { + [XmlAttribute("template")] + public string Template { get; set; } + + [XmlAnyElement] + public XmlList Items { get; set; } + } +} diff --git a/Scalar.Service.UI/Data/BindingItem.cs b/Scalar.Service.UI/Data/BindingItem.cs index 518370e41b..2b0fa647e9 100644 --- a/Scalar.Service.UI/Data/BindingItem.cs +++ b/Scalar.Service.UI/Data/BindingItem.cs @@ -1,37 +1,37 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - public abstract class BindingItem - { - [XmlRoot("text")] - public class TextData : BindingItem - { - public TextData() - { - // Required for serialization - } - - public TextData(string value) - { - this.Value = value; - } - - [XmlText] - public string Value { get; set; } - } - - [XmlRoot("image")] - public class ImageData : BindingItem - { - [XmlAttribute("placement")] - public string Placement { get; set; } - - [XmlAttribute("src")] - public string Source { get; set; } - - [XmlAttribute("hint-crop")] - public string HintCrop { get; set; } - } - } -} +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + public abstract class BindingItem + { + [XmlRoot("text")] + public class TextData : BindingItem + { + public TextData() + { + // Required for serialization + } + + public TextData(string value) + { + this.Value = value; + } + + [XmlText] + public string Value { get; set; } + } + + [XmlRoot("image")] + public class ImageData : BindingItem + { + [XmlAttribute("placement")] + public string Placement { get; set; } + + [XmlAttribute("src")] + public string Source { get; set; } + + [XmlAttribute("hint-crop")] + public string HintCrop { get; set; } + } + } +} diff --git a/Scalar.Service.UI/Data/ToastData.cs b/Scalar.Service.UI/Data/ToastData.cs index 98a28a17e4..45dc3043f9 100644 --- a/Scalar.Service.UI/Data/ToastData.cs +++ b/Scalar.Service.UI/Data/ToastData.cs @@ -1,20 +1,20 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - [XmlRoot("toast")] - public class ToastData - { - [XmlAttribute("launch")] - public string Launch { get; set; } - - [XmlElement("visual")] - public VisualData Visual { get; set; } - - [XmlElement("actions")] - public ActionsData Actions { get; set; } - - [XmlElement("scenario")] - public string Scenario { get; set; } - } -} +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + [XmlRoot("toast")] + public class ToastData + { + [XmlAttribute("launch")] + public string Launch { get; set; } + + [XmlElement("visual")] + public VisualData Visual { get; set; } + + [XmlElement("actions")] + public ActionsData Actions { get; set; } + + [XmlElement("scenario")] + public string Scenario { get; set; } + } +} diff --git a/Scalar.Service.UI/Data/VisualData.cs b/Scalar.Service.UI/Data/VisualData.cs index 03a36fe6bb..03dfd36ae3 100644 --- a/Scalar.Service.UI/Data/VisualData.cs +++ b/Scalar.Service.UI/Data/VisualData.cs @@ -1,10 +1,10 @@ -using System.Xml.Serialization; - -namespace Scalar.Service.UI.Data -{ - public class VisualData - { - [XmlElement("binding")] - public BindingData Binding { get; set; } - } -} +using System.Xml.Serialization; + +namespace Scalar.Service.UI.Data +{ + public class VisualData + { + [XmlElement("binding")] + public BindingData Binding { get; set; } + } +} diff --git a/Scalar.Service.UI/Program.cs b/Scalar.Service.UI/Program.cs index 8e2f6adee3..ce7892325e 100644 --- a/Scalar.Service.UI/Program.cs +++ b/Scalar.Service.UI/Program.cs @@ -1,122 +1,122 @@ -using Scalar.Common; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.PlatformLoader; -using Scalar.Service.UI.Data; -using System; -using System.IO; -using System.Linq; -using System.ServiceProcess; -using System.Xml; -using System.Xml.Serialization; -using Windows.UI.Notifications; -using XmlDocument = Windows.Data.Xml.Dom.XmlDocument; - -namespace Scalar.Service.UI -{ - public class ScalarServiceUI - { - private const string ServiceAppId = "Scalar"; - - private readonly ITracer tracer; - - public ScalarServiceUI(ITracer tracer) - { - this.tracer = tracer; - } - - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - - using (JsonTracer tracer = new JsonTracer("Microsoft.Git.Scalar.Service.UI", "Service.UI")) - { - string logLocation = Path.Combine( - Environment.GetEnvironmentVariable("LocalAppData"), - ScalarConstants.Service.UIName, - "serviceUI.log"); - - tracer.AddLogFileEventListener(logLocation, EventLevel.Informational, Keywords.Any); - ScalarServiceUI process = new ScalarServiceUI(tracer); - process.Start(args); - } - } - - private void Start(string[] args) - { - using (ITracer activity = this.tracer.StartActivity("Start", EventLevel.Informational)) - using (NamedPipeServer server = NamedPipeServer.StartNewServer(ScalarConstants.Service.UIName, this.tracer, this.HandleRequest)) - { - ServiceController controller = new ServiceController(ScalarConstants.Service.ServiceName); - try - { - controller.WaitForStatus(ServiceControllerStatus.Stopped); - } - catch (InvalidOperationException) - { - // Service might not exist anymore -- that's ok, just exit - } - - this.tracer.RelatedInfo("{0} stop detected -- exiting UI.", ScalarConstants.Service.ServiceName); - } - } - - private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) - { - try - { - NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); - switch (message.Header) - { - case NamedPipeMessages.Notification.Request.Header: - NamedPipeMessages.Notification.Request toastRequest = NamedPipeMessages.Notification.Request.FromMessage(message); - if (toastRequest != null) - { - using (ITracer activity = this.tracer.StartActivity("SendToast", EventLevel.Informational)) - { - this.ShowToast(activity, toastRequest); - } - } - - break; - } - } - catch (Exception e) - { - this.tracer.RelatedError("Unhandled exception: {0}", e.ToString()); - } - } - - private void ShowToast(ITracer tracer, NamedPipeMessages.Notification.Request request) - { - ToastData toastData = new ToastData(); - toastData.Visual = new VisualData(); - - BindingData binding = new BindingData(); - toastData.Visual.Binding = binding; - - binding.Template = "ToastGeneric"; - binding.Items = new XmlList(); - binding.Items.Add(new BindingItem.TextData(request.Title)); - binding.Items.AddRange(request.Message.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(t => new BindingItem.TextData(t))); - - XmlDocument toastXml = new XmlDocument(); - using (StringWriter stringWriter = new StringWriter()) - using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { OmitXmlDeclaration = true })) - { - XmlSerializer serializer = new XmlSerializer(toastData.GetType()); - XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces(); - namespaces.Add(string.Empty, string.Empty); - - serializer.Serialize(xmlWriter, toastData, namespaces); - - toastXml.LoadXml(stringWriter.ToString()); - } - - ToastNotification toastNotification = new ToastNotification(toastXml); - - ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier(ServiceAppId); - toastNotifier.Show(toastNotification); - } - } +using Scalar.Common; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.PlatformLoader; +using Scalar.Service.UI.Data; +using System; +using System.IO; +using System.Linq; +using System.ServiceProcess; +using System.Xml; +using System.Xml.Serialization; +using Windows.UI.Notifications; +using XmlDocument = Windows.Data.Xml.Dom.XmlDocument; + +namespace Scalar.Service.UI +{ + public class ScalarServiceUI + { + private const string ServiceAppId = "Scalar"; + + private readonly ITracer tracer; + + public ScalarServiceUI(ITracer tracer) + { + this.tracer = tracer; + } + + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + + using (JsonTracer tracer = new JsonTracer("Microsoft.Git.Scalar.Service.UI", "Service.UI")) + { + string logLocation = Path.Combine( + Environment.GetEnvironmentVariable("LocalAppData"), + ScalarConstants.Service.UIName, + "serviceUI.log"); + + tracer.AddLogFileEventListener(logLocation, EventLevel.Informational, Keywords.Any); + ScalarServiceUI process = new ScalarServiceUI(tracer); + process.Start(args); + } + } + + private void Start(string[] args) + { + using (ITracer activity = this.tracer.StartActivity("Start", EventLevel.Informational)) + using (NamedPipeServer server = NamedPipeServer.StartNewServer(ScalarConstants.Service.UIName, this.tracer, this.HandleRequest)) + { + ServiceController controller = new ServiceController(ScalarConstants.Service.ServiceName); + try + { + controller.WaitForStatus(ServiceControllerStatus.Stopped); + } + catch (InvalidOperationException) + { + // Service might not exist anymore -- that's ok, just exit + } + + this.tracer.RelatedInfo("{0} stop detected -- exiting UI.", ScalarConstants.Service.ServiceName); + } + } + + private void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) + { + try + { + NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); + switch (message.Header) + { + case NamedPipeMessages.Notification.Request.Header: + NamedPipeMessages.Notification.Request toastRequest = NamedPipeMessages.Notification.Request.FromMessage(message); + if (toastRequest != null) + { + using (ITracer activity = this.tracer.StartActivity("SendToast", EventLevel.Informational)) + { + this.ShowToast(activity, toastRequest); + } + } + + break; + } + } + catch (Exception e) + { + this.tracer.RelatedError("Unhandled exception: {0}", e.ToString()); + } + } + + private void ShowToast(ITracer tracer, NamedPipeMessages.Notification.Request request) + { + ToastData toastData = new ToastData(); + toastData.Visual = new VisualData(); + + BindingData binding = new BindingData(); + toastData.Visual.Binding = binding; + + binding.Template = "ToastGeneric"; + binding.Items = new XmlList(); + binding.Items.Add(new BindingItem.TextData(request.Title)); + binding.Items.AddRange(request.Message.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries).Select(t => new BindingItem.TextData(t))); + + XmlDocument toastXml = new XmlDocument(); + using (StringWriter stringWriter = new StringWriter()) + using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, new XmlWriterSettings { OmitXmlDeclaration = true })) + { + XmlSerializer serializer = new XmlSerializer(toastData.GetType()); + XmlSerializerNamespaces namespaces = new XmlSerializerNamespaces(); + namespaces.Add(string.Empty, string.Empty); + + serializer.Serialize(xmlWriter, toastData, namespaces); + + toastXml.LoadXml(stringWriter.ToString()); + } + + ToastNotification toastNotification = new ToastNotification(toastXml); + + ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier(ServiceAppId); + toastNotifier.Show(toastNotification); + } + } } diff --git a/Scalar.Service.UI/Scalar.Service.UI.csproj b/Scalar.Service.UI/Scalar.Service.UI.csproj index 2693f93465..c23d26d504 100644 --- a/Scalar.Service.UI/Scalar.Service.UI.csproj +++ b/Scalar.Service.UI/Scalar.Service.UI.csproj @@ -1,86 +1,86 @@ - - - - - - {93B403FD-DAFB-46C5-9636-B122792A548A} - Exe - Properties - Scalar.Service.UI - Scalar.Service.UI - v4.6.1 - 512 - true - - - - - x64 - true - full - false - DEBUG;TRACE - prompt - 4 - false - - - x64 - pdbonly - true - TRACE - prompt - 4 - - - - - - - - - - - - - C:\Program Files (x86)\Windows Kits\10\App Certification Kit\winmds\windows81\Windows.winmd - - - - - PlatformLoader.Windows.cs - - - - - - - - - - - - - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - {4ce404e7-d3fc-471c-993c-64615861ea63} - Scalar.Platform.Windows - - - - - PreserveNewest - - - - - - - - + + + + + + {93B403FD-DAFB-46C5-9636-B122792A548A} + Exe + Properties + Scalar.Service.UI + Scalar.Service.UI + v4.6.1 + 512 + true + + + + + x64 + true + full + false + DEBUG;TRACE + prompt + 4 + false + + + x64 + pdbonly + true + TRACE + prompt + 4 + + + + + + + + + + + + + C:\Program Files (x86)\Windows Kits\10\App Certification Kit\winmds\windows81\Windows.winmd + + + + + PlatformLoader.Windows.cs + + + + + + + + + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + {4ce404e7-d3fc-471c-993c-64615861ea63} + Scalar.Platform.Windows + + + + + PreserveNewest + + + + + + + + diff --git a/Scalar.Service.UI/XmlList.cs b/Scalar.Service.UI/XmlList.cs index 8d81651037..047d31a909 100644 --- a/Scalar.Service.UI/XmlList.cs +++ b/Scalar.Service.UI/XmlList.cs @@ -1,32 +1,32 @@ -using System; -using System.Collections.Generic; -using System.Xml; -using System.Xml.Schema; -using System.Xml.Serialization; - -namespace Scalar.Service.UI -{ - public class XmlList : List, IXmlSerializable where T : class - { - public XmlSchema GetSchema() - { - throw new NotImplementedException(); - } - - public void ReadXml(XmlReader reader) - { - throw new NotImplementedException(); - } - - public void WriteXml(XmlWriter writer) - { - XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); - ns.Add(string.Empty, string.Empty); - foreach (T item in this) - { - XmlSerializer xml = new XmlSerializer(item.GetType()); - xml.Serialize(writer, item, ns); - } - } - } -} +using System; +using System.Collections.Generic; +using System.Xml; +using System.Xml.Schema; +using System.Xml.Serialization; + +namespace Scalar.Service.UI +{ + public class XmlList : List, IXmlSerializable where T : class + { + public XmlSchema GetSchema() + { + throw new NotImplementedException(); + } + + public void ReadXml(XmlReader reader) + { + throw new NotImplementedException(); + } + + public void WriteXml(XmlWriter writer) + { + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); + foreach (T item in this) + { + XmlSerializer xml = new XmlSerializer(item.GetType()); + xml.Serialize(writer, item, ns); + } + } + } +} diff --git a/Scalar.Service.UI/app.config b/Scalar.Service.UI/app.config index 1da8c9b53a..bd27edc04e 100644 --- a/Scalar.Service.UI/app.config +++ b/Scalar.Service.UI/app.config @@ -1,6 +1,6 @@ - - - - - - + + + + + + diff --git a/Scalar.Service.UI/packages.config b/Scalar.Service.UI/packages.config index 3951324f87..c64139aa40 100644 --- a/Scalar.Service.UI/packages.config +++ b/Scalar.Service.UI/packages.config @@ -1,4 +1,4 @@ - - - + + + diff --git a/Scalar.Service/Configuration.cs b/Scalar.Service/Configuration.cs index d693b89d1c..412a65dd42 100644 --- a/Scalar.Service/Configuration.cs +++ b/Scalar.Service/Configuration.cs @@ -1,41 +1,41 @@ -using Scalar.Common; -using System.IO; - -namespace Scalar.Service -{ - public class Configuration - { - private static Configuration instance = new Configuration(); - private static string assemblyPath = null; - - private Configuration() - { - this.ScalarLocation = Path.Combine(AssemblyPath, ScalarPlatform.Instance.Constants.ScalarExecutableName); - this.ScalarServiceUILocation = Path.Combine(AssemblyPath, ScalarConstants.Service.UIName + ScalarPlatform.Instance.Constants.ExecutableExtension); - } - - public static Configuration Instance - { - get - { - return instance; - } - } - - public static string AssemblyPath - { - get - { - if (assemblyPath == null) - { - assemblyPath = ProcessHelper.GetCurrentProcessLocation(); - } - - return assemblyPath; - } - } - - public string ScalarLocation { get; private set; } - public string ScalarServiceUILocation { get; private set; } - } -} +using Scalar.Common; +using System.IO; + +namespace Scalar.Service +{ + public class Configuration + { + private static Configuration instance = new Configuration(); + private static string assemblyPath = null; + + private Configuration() + { + this.ScalarLocation = Path.Combine(AssemblyPath, ScalarPlatform.Instance.Constants.ScalarExecutableName); + this.ScalarServiceUILocation = Path.Combine(AssemblyPath, ScalarConstants.Service.UIName + ScalarPlatform.Instance.Constants.ExecutableExtension); + } + + public static Configuration Instance + { + get + { + return instance; + } + } + + public static string AssemblyPath + { + get + { + if (assemblyPath == null) + { + assemblyPath = ProcessHelper.GetCurrentProcessLocation(); + } + + return assemblyPath; + } + } + + public string ScalarLocation { get; private set; } + public string ScalarServiceUILocation { get; private set; } + } +} diff --git a/Scalar.Service/Handlers/GetActiveRepoListHandler.cs b/Scalar.Service/Handlers/GetActiveRepoListHandler.cs index d7e733c575..78c4ff6302 100644 --- a/Scalar.Service/Handlers/GetActiveRepoListHandler.cs +++ b/Scalar.Service/Handlers/GetActiveRepoListHandler.cs @@ -1,93 +1,93 @@ -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; - } - } -} +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/INotificationHandler.cs b/Scalar.Service/Handlers/INotificationHandler.cs index 2b89279c3c..0c51950856 100644 --- a/Scalar.Service/Handlers/INotificationHandler.cs +++ b/Scalar.Service/Handlers/INotificationHandler.cs @@ -1,9 +1,9 @@ -using Scalar.Common.NamedPipes; +using Scalar.Common.NamedPipes; -namespace Scalar.Service.Handlers -{ - public interface INotificationHandler - { - void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request); - } -} +namespace Scalar.Service.Handlers +{ + public interface INotificationHandler + { + void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request); + } +} diff --git a/Scalar.Service/Handlers/MessageHandler.cs b/Scalar.Service/Handlers/MessageHandler.cs index f5e2d4e9f3..e6289b50a8 100644 --- a/Scalar.Service/Handlers/MessageHandler.cs +++ b/Scalar.Service/Handlers/MessageHandler.cs @@ -1,16 +1,16 @@ -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); - } - } - } -} +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/NotificationHandler.Mac.cs b/Scalar.Service/Handlers/NotificationHandler.Mac.cs index f0001e2c18..a81f19f6cc 100644 --- a/Scalar.Service/Handlers/NotificationHandler.Mac.cs +++ b/Scalar.Service/Handlers/NotificationHandler.Mac.cs @@ -1,45 +1,45 @@ -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System; using System.IO; -namespace Scalar.Service.Handlers -{ - public class NotificationHandler : INotificationHandler - { - private const string NotificationServerPipeName = "scalar.notification"; - private ITracer tracer; - - public NotificationHandler(ITracer tracer) - { - this.tracer = tracer; - } - - public void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request) - { - string pipeName = Path.Combine(Path.GetTempPath(), NotificationServerPipeName); - using (NamedPipeClient client = new NamedPipeClient(pipeName)) - { - if (client.Connect()) - { - try - { - client.SendRequest(request.ToMessage()); - } - catch (Exception ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(NotificationHandler)); - metadata.Add("Exception", ex.ToString()); - metadata.Add(TracingConstants.MessageKey.ErrorMessage, "MacOS notification display error"); - this.tracer.RelatedError(metadata, $"MacOS notification: {request.Title} - {request.Message}."); - } - } - else - { - this.tracer.RelatedError($"ERROR: Communication failure with native notification display tool. Notification info: {request.Title} - {request.Message}."); - } - } - } - } -} +namespace Scalar.Service.Handlers +{ + public class NotificationHandler : INotificationHandler + { + private const string NotificationServerPipeName = "scalar.notification"; + private ITracer tracer; + + public NotificationHandler(ITracer tracer) + { + this.tracer = tracer; + } + + public void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request) + { + string pipeName = Path.Combine(Path.GetTempPath(), NotificationServerPipeName); + using (NamedPipeClient client = new NamedPipeClient(pipeName)) + { + if (client.Connect()) + { + try + { + client.SendRequest(request.ToMessage()); + } + catch (Exception ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(NotificationHandler)); + metadata.Add("Exception", ex.ToString()); + metadata.Add(TracingConstants.MessageKey.ErrorMessage, "MacOS notification display error"); + this.tracer.RelatedError(metadata, $"MacOS notification: {request.Title} - {request.Message}."); + } + } + else + { + this.tracer.RelatedError($"ERROR: Communication failure with native notification display tool. Notification info: {request.Title} - {request.Message}."); + } + } + } + } +} diff --git a/Scalar.Service/Handlers/NotificationHandler.Windows.cs b/Scalar.Service/Handlers/NotificationHandler.Windows.cs index 3565e95c83..6772f6c55a 100644 --- a/Scalar.Service/Handlers/NotificationHandler.Windows.cs +++ b/Scalar.Service/Handlers/NotificationHandler.Windows.cs @@ -1,85 +1,85 @@ -using Scalar.Common; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.Platform.Windows; -using System; -using System.Diagnostics; - -namespace Scalar.Service.Handlers -{ - public class NotificationHandler : INotificationHandler - { - private ITracer tracer; - - public NotificationHandler(ITracer tracer) - { - this.tracer = tracer; - } - - public void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request) - { - NamedPipeClient client; - if (!this.TryOpenConnectionToUIProcess(out client)) - { - this.TerminateExistingProcess(ScalarConstants.Service.UIName); - - CurrentUser currentUser = new CurrentUser(this.tracer, sessionId); - if (!currentUser.RunAs( - Configuration.Instance.ScalarServiceUILocation, - string.Empty)) - { - this.tracer.RelatedError("Could not start " + ScalarConstants.Service.UIName); - return; - } - - this.TryOpenConnectionToUIProcess(out client); - } - - if (client == null) - { - this.tracer.RelatedError("Failed to connect to " + ScalarConstants.Service.UIName); - return; - } - - try - { - if (!client.TrySendRequest(request.ToMessage())) - { - this.tracer.RelatedInfo("Failed to send notification request to " + ScalarConstants.Service.UIName); - } - } - finally - { - client.Dispose(); - } - } - - private bool TryOpenConnectionToUIProcess(out NamedPipeClient client) - { - client = new NamedPipeClient(ScalarConstants.Service.UIName); - if (client.Connect()) - { - return true; - } - - client.Dispose(); - client = null; - return false; - } - - private void TerminateExistingProcess(string processName) - { - try - { - foreach (Process process in Process.GetProcessesByName(processName)) - { - process.Kill(); - } - } - catch (Exception ex) - { - this.tracer.RelatedError("Could not find and kill existing instances of {0}: {1}", processName, ex.Message); - } - } - } -} +using Scalar.Common; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.Platform.Windows; +using System; +using System.Diagnostics; + +namespace Scalar.Service.Handlers +{ + public class NotificationHandler : INotificationHandler + { + private ITracer tracer; + + public NotificationHandler(ITracer tracer) + { + this.tracer = tracer; + } + + public void SendNotification(int sessionId, NamedPipeMessages.Notification.Request request) + { + NamedPipeClient client; + if (!this.TryOpenConnectionToUIProcess(out client)) + { + this.TerminateExistingProcess(ScalarConstants.Service.UIName); + + CurrentUser currentUser = new CurrentUser(this.tracer, sessionId); + if (!currentUser.RunAs( + Configuration.Instance.ScalarServiceUILocation, + string.Empty)) + { + this.tracer.RelatedError("Could not start " + ScalarConstants.Service.UIName); + return; + } + + this.TryOpenConnectionToUIProcess(out client); + } + + if (client == null) + { + this.tracer.RelatedError("Failed to connect to " + ScalarConstants.Service.UIName); + return; + } + + try + { + if (!client.TrySendRequest(request.ToMessage())) + { + this.tracer.RelatedInfo("Failed to send notification request to " + ScalarConstants.Service.UIName); + } + } + finally + { + client.Dispose(); + } + } + + private bool TryOpenConnectionToUIProcess(out NamedPipeClient client) + { + client = new NamedPipeClient(ScalarConstants.Service.UIName); + if (client.Connect()) + { + return true; + } + + client.Dispose(); + client = null; + return false; + } + + private void TerminateExistingProcess(string processName) + { + try + { + foreach (Process process in Process.GetProcessesByName(processName)) + { + process.Kill(); + } + } + catch (Exception ex) + { + this.tracer.RelatedError("Could not find and kill existing instances of {0}: {1}", processName, ex.Message); + } + } + } +} diff --git a/Scalar.Service/Handlers/RegisterRepoHandler.cs b/Scalar.Service/Handlers/RegisterRepoHandler.cs index 60698e133e..744028e83b 100644 --- a/Scalar.Service/Handlers/RegisterRepoHandler.cs +++ b/Scalar.Service/Handlers/RegisterRepoHandler.cs @@ -1,45 +1,45 @@ -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); - } - } -} +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 e0aba4434b..38c76e0965 100644 --- a/Scalar.Service/Handlers/RequestHandler.cs +++ b/Scalar.Service/Handlers/RequestHandler.cs @@ -1,115 +1,115 @@ -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System.Runtime.Serialization; - -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) - { - this.tracer = tracer; - this.etwArea = etwArea; - this.repoRegistry = repoRegistry; - } - - public void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) - { - NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); - if (string.IsNullOrWhiteSpace(message.Header)) - { - return; - } - - using (ITracer activity = this.tracer.StartActivity(message.Header, EventLevel.Informational, new EventMetadata { { nameof(request), request } })) - { - try - { - this.HandleMessage(activity, message, connection); - } - catch (SerializationException ex) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", this.etwArea); - metadata.Add("Header", message.Header); - metadata.Add("Exception", ex.ToString()); - - activity.RelatedError(metadata, $"Could not deserialize {this.requestDescription} request: {ex.Message}"); - } - } - } - - protected virtual void HandleMessage( - ITracer tracer, - NamedPipeMessages.Message message, - NamedPipeServer.Connection connection) - { - 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(); - metadata.Add("Area", this.etwArea); - metadata.Add("Header", message.Header); - tracer.RelatedWarning(metadata, "HandleNewConnection: Unknown request", Keywords.Telemetry); - - this.TrySendResponse(tracer, NamedPipeMessages.UnknownRequest, connection); - break; - } - } - - private void TrySendResponse( - ITracer tracer, - string message, - NamedPipeServer.Connection connection) - { - if (!connection.TrySendResponse(message)) - { - tracer.RelatedError($"{nameof(this.TrySendResponse)}: Could not send response to client. Reply Info: {message}"); - } - } - } -} +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System.Runtime.Serialization; + +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) + { + this.tracer = tracer; + this.etwArea = etwArea; + this.repoRegistry = repoRegistry; + } + + public void HandleRequest(ITracer tracer, string request, NamedPipeServer.Connection connection) + { + NamedPipeMessages.Message message = NamedPipeMessages.Message.FromString(request); + if (string.IsNullOrWhiteSpace(message.Header)) + { + return; + } + + using (ITracer activity = this.tracer.StartActivity(message.Header, EventLevel.Informational, new EventMetadata { { nameof(request), request } })) + { + try + { + this.HandleMessage(activity, message, connection); + } + catch (SerializationException ex) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", this.etwArea); + metadata.Add("Header", message.Header); + metadata.Add("Exception", ex.ToString()); + + activity.RelatedError(metadata, $"Could not deserialize {this.requestDescription} request: {ex.Message}"); + } + } + } + + protected virtual void HandleMessage( + ITracer tracer, + NamedPipeMessages.Message message, + NamedPipeServer.Connection connection) + { + 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(); + metadata.Add("Area", this.etwArea); + metadata.Add("Header", message.Header); + tracer.RelatedWarning(metadata, "HandleNewConnection: Unknown request", Keywords.Telemetry); + + this.TrySendResponse(tracer, NamedPipeMessages.UnknownRequest, connection); + break; + } + } + + private void TrySendResponse( + ITracer tracer, + string message, + NamedPipeServer.Connection connection) + { + if (!connection.TrySendResponse(message)) + { + tracer.RelatedError($"{nameof(this.TrySendResponse)}: Could not send response to client. Reply Info: {message}"); + } + } + } +} diff --git a/Scalar.Service/Handlers/UnregisterRepoHandler.cs b/Scalar.Service/Handlers/UnregisterRepoHandler.cs index 449041a351..749fc158ee 100644 --- a/Scalar.Service/Handlers/UnregisterRepoHandler.cs +++ b/Scalar.Service/Handlers/UnregisterRepoHandler.cs @@ -1,45 +1,45 @@ -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); - } - } -} +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/IRepoMounter.cs b/Scalar.Service/IRepoMounter.cs index 104db9f11e..675260a2c9 100644 --- a/Scalar.Service/IRepoMounter.cs +++ b/Scalar.Service/IRepoMounter.cs @@ -1,7 +1,7 @@ -namespace Scalar.Service -{ - public interface IRepoMounter - { - bool MountRepository(string repoRoot, int sessionId); - } -} +namespace Scalar.Service +{ + public interface IRepoMounter + { + bool MountRepository(string repoRoot, int sessionId); + } +} diff --git a/Scalar.Service/IRepoRegistry.cs b/Scalar.Service/IRepoRegistry.cs index f9f9d1cbb5..fd07f716ee 100644 --- a/Scalar.Service/IRepoRegistry.cs +++ b/Scalar.Service/IRepoRegistry.cs @@ -1,14 +1,14 @@ -using System.Collections.Generic; - -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 AutoMountRepos(string userId, int sessionId); - void TraceStatus(); - } -} +using System.Collections.Generic; + +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 AutoMountRepos(string userId, int sessionId); + void TraceStatus(); + } +} diff --git a/Scalar.Service/ProductUpgradeTimer.cs b/Scalar.Service/ProductUpgradeTimer.cs index 7f7dd14ae7..ba8c8afbc5 100644 --- a/Scalar.Service/ProductUpgradeTimer.cs +++ b/Scalar.Service/ProductUpgradeTimer.cs @@ -1,250 +1,250 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.NuGetUpgrade; -using Scalar.Common.Tracing; -using Scalar.Upgrader; -using System; -using System.IO; -using System.Threading; - -namespace Scalar.Service -{ - public class ProductUpgradeTimer : IDisposable - { - private static readonly TimeSpan TimeInterval = TimeSpan.FromDays(1); - private JsonTracer tracer; - private PhysicalFileSystem fileSystem; - private Timer timer; - - public ProductUpgradeTimer(JsonTracer tracer) - { - this.tracer = tracer; - this.fileSystem = new PhysicalFileSystem(); - } - - public void Start() - { - if (!ScalarEnlistment.IsUnattended(this.tracer)) - { - TimeSpan startTime = TimeSpan.Zero; - - this.tracer.RelatedInfo("Starting auto upgrade checks."); - this.timer = new Timer( - this.TimerCallback, - state: null, - dueTime: startTime, - period: TimeInterval); - } - else - { - this.tracer.RelatedInfo("No upgrade checks scheduled, Scalar is running in unattended mode."); - } - } - - public void Stop() - { - this.tracer.RelatedInfo("Stopping auto upgrade checks"); - this.Dispose(); - } - - public void Dispose() - { - if (this.timer != null) - { - this.timer.Dispose(); - this.timer = null; - } - } - - private static EventMetadata CreateEventMetadata(Exception e) - { - EventMetadata metadata = new EventMetadata(); - if (e != null) - { - metadata.Add("Exception", e.ToString()); - } - - return metadata; - } - - private void TimerCallback(object unusedState) - { - string errorMessage = null; - - using (ITracer activity = this.tracer.StartActivity("Checking for product upgrades.", EventLevel.Informational)) - { - try - { - ProductUpgraderInfo info = new ProductUpgraderInfo( - this.tracer, - this.fileSystem); - - ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystem, - new LocalScalarConfig(), - credentialStore: null, - dryRun: false, - noVerify: false, - newUpgrader: out ProductUpgrader productUpgrader, - error: out errorMessage); - - if (productUpgrader == null) - { - string message = string.Format( - "{0}.{1}: failed to create upgrader: {2}", - nameof(ProductUpgradeTimer), - nameof(this.TimerCallback), - errorMessage); - - activity.RelatedWarning( - metadata: new EventMetadata(), - message: message, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - - if (!productUpgrader.SupportsAnonymousVersionQuery) - { - // If this is a NuGetUpgrader that does not support anonymous version query, - // fall back to using the GitHubUpgrader, to preserve existing behavior. - // Once we have completely transitioned to using the anonymous endpoint, - // we can remove this code. - if (productUpgrader is NuGetUpgrader) - { - productUpgrader = GitHubUpgrader.Create( - this.tracer, - this.fileSystem, - new LocalScalarConfig(), - dryRun: false, - noVerify: false, - error: out errorMessage); - - if (productUpgrader == null) - { - string gitHubUpgraderFailedMessage = string.Format( - "{0}.{1}: GitHubUpgrader.Create failed to create upgrader: {2}", - nameof(ProductUpgradeTimer), - nameof(this.TimerCallback), - errorMessage); - - activity.RelatedWarning( - metadata: new EventMetadata(), - message: gitHubUpgraderFailedMessage, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - } - else - { - errorMessage = string.Format( - "{0}.{1}: Configured Product Upgrader does not support anonymous version queries.", - nameof(ProductUpgradeTimer), - nameof(this.TimerCallback), - errorMessage); - - activity.RelatedWarning( - metadata: new EventMetadata(), - message: errorMessage, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - } - - InstallerPreRunChecker prerunChecker = new InstallerPreRunChecker(this.tracer, string.Empty); - if (!prerunChecker.TryRunPreUpgradeChecks(out errorMessage)) - { - string message = string.Format( - "{0}.{1}: PreUpgradeChecks failed with: {2}", - nameof(ProductUpgradeTimer), - nameof(this.TimerCallback), - errorMessage); - - activity.RelatedWarning( - metadata: new EventMetadata(), - message: message, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - - if (!productUpgrader.UpgradeAllowed(out errorMessage)) - { - errorMessage = errorMessage ?? - $"{nameof(ProductUpgradeTimer)}.{nameof(this.TimerCallback)}: Upgrade is not allowed, but no reason provided."; - activity.RelatedWarning( - metadata: new EventMetadata(), - message: errorMessage, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - - if (!this.TryQueryForNewerVersion( - activity, - productUpgrader, - out Version newerVersion, - out errorMessage)) - { - string message = string.Format( - "{0}.{1}: TryQueryForNewerVersion failed with: {2}", - nameof(ProductUpgradeTimer), - nameof(this.TimerCallback), - errorMessage); - - activity.RelatedWarning( - metadata: new EventMetadata(), - message: message, - keywords: Keywords.Telemetry); - - info.RecordHighestAvailableVersion(highestAvailableVersion: null); - return; - } - - info.RecordHighestAvailableVersion(highestAvailableVersion: newerVersion); - } - catch (Exception ex) when ( - ex is IOException || - ex is UnauthorizedAccessException || - ex is NotSupportedException) - { - this.tracer.RelatedWarning( - CreateEventMetadata(ex), - "Exception encountered recording highest available version"); - } - catch (Exception ex) - { - this.tracer.RelatedError( - CreateEventMetadata(ex), - "Unhanlded exception encountered recording highest available version"); - Environment.Exit((int)ReturnCode.GenericError); - } - } - } - - private bool TryQueryForNewerVersion(ITracer tracer, ProductUpgrader productUpgrader, out Version newVersion, out string errorMessage) - { - errorMessage = null; - tracer.RelatedInfo($"Querying server for latest version..."); - - if (!productUpgrader.TryQueryNewestVersion(out newVersion, out string detailedError)) - { - errorMessage = "Could not fetch new version info. " + detailedError; - return false; - } - - string logMessage = newVersion == null ? "No newer versions available." : $"Newer version available: {newVersion}."; - tracer.RelatedInfo(logMessage); - - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.NuGetUpgrade; +using Scalar.Common.Tracing; +using Scalar.Upgrader; +using System; +using System.IO; +using System.Threading; + +namespace Scalar.Service +{ + public class ProductUpgradeTimer : IDisposable + { + private static readonly TimeSpan TimeInterval = TimeSpan.FromDays(1); + private JsonTracer tracer; + private PhysicalFileSystem fileSystem; + private Timer timer; + + public ProductUpgradeTimer(JsonTracer tracer) + { + this.tracer = tracer; + this.fileSystem = new PhysicalFileSystem(); + } + + public void Start() + { + if (!ScalarEnlistment.IsUnattended(this.tracer)) + { + TimeSpan startTime = TimeSpan.Zero; + + this.tracer.RelatedInfo("Starting auto upgrade checks."); + this.timer = new Timer( + this.TimerCallback, + state: null, + dueTime: startTime, + period: TimeInterval); + } + else + { + this.tracer.RelatedInfo("No upgrade checks scheduled, Scalar is running in unattended mode."); + } + } + + public void Stop() + { + this.tracer.RelatedInfo("Stopping auto upgrade checks"); + this.Dispose(); + } + + public void Dispose() + { + if (this.timer != null) + { + this.timer.Dispose(); + this.timer = null; + } + } + + private static EventMetadata CreateEventMetadata(Exception e) + { + EventMetadata metadata = new EventMetadata(); + if (e != null) + { + metadata.Add("Exception", e.ToString()); + } + + return metadata; + } + + private void TimerCallback(object unusedState) + { + string errorMessage = null; + + using (ITracer activity = this.tracer.StartActivity("Checking for product upgrades.", EventLevel.Informational)) + { + try + { + ProductUpgraderInfo info = new ProductUpgraderInfo( + this.tracer, + this.fileSystem); + + ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystem, + new LocalScalarConfig(), + credentialStore: null, + dryRun: false, + noVerify: false, + newUpgrader: out ProductUpgrader productUpgrader, + error: out errorMessage); + + if (productUpgrader == null) + { + string message = string.Format( + "{0}.{1}: failed to create upgrader: {2}", + nameof(ProductUpgradeTimer), + nameof(this.TimerCallback), + errorMessage); + + activity.RelatedWarning( + metadata: new EventMetadata(), + message: message, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + + if (!productUpgrader.SupportsAnonymousVersionQuery) + { + // If this is a NuGetUpgrader that does not support anonymous version query, + // fall back to using the GitHubUpgrader, to preserve existing behavior. + // Once we have completely transitioned to using the anonymous endpoint, + // we can remove this code. + if (productUpgrader is NuGetUpgrader) + { + productUpgrader = GitHubUpgrader.Create( + this.tracer, + this.fileSystem, + new LocalScalarConfig(), + dryRun: false, + noVerify: false, + error: out errorMessage); + + if (productUpgrader == null) + { + string gitHubUpgraderFailedMessage = string.Format( + "{0}.{1}: GitHubUpgrader.Create failed to create upgrader: {2}", + nameof(ProductUpgradeTimer), + nameof(this.TimerCallback), + errorMessage); + + activity.RelatedWarning( + metadata: new EventMetadata(), + message: gitHubUpgraderFailedMessage, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + } + else + { + errorMessage = string.Format( + "{0}.{1}: Configured Product Upgrader does not support anonymous version queries.", + nameof(ProductUpgradeTimer), + nameof(this.TimerCallback), + errorMessage); + + activity.RelatedWarning( + metadata: new EventMetadata(), + message: errorMessage, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + } + + InstallerPreRunChecker prerunChecker = new InstallerPreRunChecker(this.tracer, string.Empty); + if (!prerunChecker.TryRunPreUpgradeChecks(out errorMessage)) + { + string message = string.Format( + "{0}.{1}: PreUpgradeChecks failed with: {2}", + nameof(ProductUpgradeTimer), + nameof(this.TimerCallback), + errorMessage); + + activity.RelatedWarning( + metadata: new EventMetadata(), + message: message, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + + if (!productUpgrader.UpgradeAllowed(out errorMessage)) + { + errorMessage = errorMessage ?? + $"{nameof(ProductUpgradeTimer)}.{nameof(this.TimerCallback)}: Upgrade is not allowed, but no reason provided."; + activity.RelatedWarning( + metadata: new EventMetadata(), + message: errorMessage, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + + if (!this.TryQueryForNewerVersion( + activity, + productUpgrader, + out Version newerVersion, + out errorMessage)) + { + string message = string.Format( + "{0}.{1}: TryQueryForNewerVersion failed with: {2}", + nameof(ProductUpgradeTimer), + nameof(this.TimerCallback), + errorMessage); + + activity.RelatedWarning( + metadata: new EventMetadata(), + message: message, + keywords: Keywords.Telemetry); + + info.RecordHighestAvailableVersion(highestAvailableVersion: null); + return; + } + + info.RecordHighestAvailableVersion(highestAvailableVersion: newerVersion); + } + catch (Exception ex) when ( + ex is IOException || + ex is UnauthorizedAccessException || + ex is NotSupportedException) + { + this.tracer.RelatedWarning( + CreateEventMetadata(ex), + "Exception encountered recording highest available version"); + } + catch (Exception ex) + { + this.tracer.RelatedError( + CreateEventMetadata(ex), + "Unhanlded exception encountered recording highest available version"); + Environment.Exit((int)ReturnCode.GenericError); + } + } + } + + private bool TryQueryForNewerVersion(ITracer tracer, ProductUpgrader productUpgrader, out Version newVersion, out string errorMessage) + { + errorMessage = null; + tracer.RelatedInfo($"Querying server for latest version..."); + + if (!productUpgrader.TryQueryNewestVersion(out newVersion, out string detailedError)) + { + errorMessage = "Could not fetch new version info. " + detailedError; + return false; + } + + string logMessage = newVersion == null ? "No newer versions available." : $"Newer version available: {newVersion}."; + tracer.RelatedInfo(logMessage); + + return true; + } + } +} diff --git a/Scalar.Service/Program.Mac.cs b/Scalar.Service/Program.Mac.cs index d4f63d2c3a..685b7717fa 100644 --- a/Scalar.Service/Program.Mac.cs +++ b/Scalar.Service/Program.Mac.cs @@ -1,43 +1,43 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using Scalar.PlatformLoader; -using Scalar.Service.Handlers; -using System; -using System.IO; -using System.Linq; - -namespace Scalar.Service -{ - public static class Program - { - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; - - using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) - { - CreateService(tracer, args).Run(); - } - } - - private static ScalarService CreateService(JsonTracer tracer, string[] args) - { - string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ScalarService.ServiceNameArgPrefix, StringComparison.OrdinalIgnoreCase)); - if (serviceName != null) - { - serviceName = serviceName.Substring(ScalarService.ServiceNameArgPrefix.Length); - } - else - { - serviceName = ScalarConstants.Service.ServiceName; - } - - ScalarPlatform scalarPlatform = ScalarPlatform.Instance; - - string logFilePath = Path.Combine( +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using Scalar.PlatformLoader; +using Scalar.Service.Handlers; +using System; +using System.IO; +using System.Linq; + +namespace Scalar.Service +{ + public static class Program + { + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; + + using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) + { + CreateService(tracer, args).Run(); + } + } + + private static ScalarService CreateService(JsonTracer tracer, string[] args) + { + string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ScalarService.ServiceNameArgPrefix, StringComparison.OrdinalIgnoreCase)); + if (serviceName != null) + { + serviceName = serviceName.Substring(ScalarService.ServiceNameArgPrefix.Length); + } + else + { + serviceName = ScalarConstants.Service.ServiceName; + } + + ScalarPlatform scalarPlatform = ScalarPlatform.Instance; + + string logFilePath = Path.Combine( scalarPlatform.GetDataRootForScalarComponent(serviceName), ScalarConstants.Service.LogDirectory); Directory.CreateDirectory(logFilePath); @@ -45,25 +45,25 @@ private static ScalarService CreateService(JsonTracer tracer, string[] args) tracer.AddLogFileEventListener( ScalarEnlistment.GetNewScalarLogFileName(logFilePath, ScalarConstants.LogFileTypes.Service), EventLevel.Informational, - Keywords.Any); - - string serviceDataLocation = scalarPlatform.GetDataRootForScalarComponent(serviceName); - RepoRegistry repoRegistry = new RepoRegistry( - tracer, - new PhysicalFileSystem(), - serviceDataLocation, - new ScalarMountProcess(tracer), - new NotificationHandler(tracer)); - - return new ScalarService(tracer, serviceName, repoRegistry); - } - - private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) - { - using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) - { - tracer.RelatedError($"Unhandled exception in Scalar.Service: {e.ExceptionObject.ToString()}"); - } - } - } -} + Keywords.Any); + + string serviceDataLocation = scalarPlatform.GetDataRootForScalarComponent(serviceName); + RepoRegistry repoRegistry = new RepoRegistry( + tracer, + new PhysicalFileSystem(), + serviceDataLocation, + new ScalarMountProcess(tracer), + new NotificationHandler(tracer)); + + return new ScalarService(tracer, serviceName, repoRegistry); + } + + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) + { + using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) + { + tracer.RelatedError($"Unhandled exception in Scalar.Service: {e.ExceptionObject.ToString()}"); + } + } + } +} diff --git a/Scalar.Service/Program.Windows.cs b/Scalar.Service/Program.Windows.cs index 967a731c96..ac2f35c970 100644 --- a/Scalar.Service/Program.Windows.cs +++ b/Scalar.Service/Program.Windows.cs @@ -1,40 +1,40 @@ -using Scalar.Common; -using Scalar.Common.Tracing; -using Scalar.PlatformLoader; -using System; -using System.Diagnostics; -using System.ServiceProcess; - -namespace Scalar.Service -{ - public static class Program - { - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - - AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; - - using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) - { - using (ScalarService service = new ScalarService(tracer)) - { - // This will fail with a popup from a command prompt. To install as a service, run: - // %windir%\Microsoft.NET\Framework64\v4.0.30319\installutil Scalar.Service.exe - ServiceBase.Run(service); - } - } - } - - private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) - { - using (EventLog eventLog = new EventLog("Application")) - { - eventLog.Source = "Application"; - eventLog.WriteEntry( - "Unhandled exception in Scalar.Service: " + e.ExceptionObject.ToString(), - EventLogEntryType.Error); - } - } - } +using Scalar.Common; +using Scalar.Common.Tracing; +using Scalar.PlatformLoader; +using System; +using System.Diagnostics; +using System.ServiceProcess; + +namespace Scalar.Service +{ + public static class Program + { + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + + AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler; + + using (JsonTracer tracer = new JsonTracer(ScalarConstants.Service.ServiceName, ScalarConstants.Service.ServiceName)) + { + using (ScalarService service = new ScalarService(tracer)) + { + // This will fail with a popup from a command prompt. To install as a service, run: + // %windir%\Microsoft.NET\Framework64\v4.0.30319\installutil Scalar.Service.exe + ServiceBase.Run(service); + } + } + } + + private static void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs e) + { + using (EventLog eventLog = new EventLog("Application")) + { + eventLog.Source = "Application"; + eventLog.WriteEntry( + "Unhandled exception in Scalar.Service: " + e.ExceptionObject.ToString(), + EventLogEntryType.Error); + } + } + } } diff --git a/Scalar.Service/Properties/AssemblyInfo.cs b/Scalar.Service/Properties/AssemblyInfo.cs index 8630278001..948ab9e072 100644 --- a/Scalar.Service/Properties/AssemblyInfo.cs +++ b/Scalar.Service/Properties/AssemblyInfo.cs @@ -1,22 +1,22 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar.Service")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar.Service")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("b8c1dfba-cafd-4f7e-a1a3-e11907b5467b")] +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar.Service")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar.Service")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b8c1dfba-cafd-4f7e-a1a3-e11907b5467b")] diff --git a/Scalar.Service/RepoRegistration.cs b/Scalar.Service/RepoRegistration.cs index 0908651a74..e175725161 100644 --- a/Scalar.Service/RepoRegistration.cs +++ b/Scalar.Service/RepoRegistration.cs @@ -1,47 +1,47 @@ -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); - } - } +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 index 748bb49435..ea9f3d4a9a 100644 --- a/Scalar.Service/RepoRegistry.cs +++ b/Scalar.Service/RepoRegistry.cs @@ -1,358 +1,358 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.Service.Handlers; -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 IRepoMounter repoMounter; - private INotificationHandler notificationHandler; - - public RepoRegistry( - ITracer tracer, - PhysicalFileSystem fileSystem, - string serviceDataLocation, - IRepoMounter repoMounter, - INotificationHandler notificationHandler) - { - this.tracer = tracer; - this.fileSystem = fileSystem; - this.registryParentFolderPath = serviceDataLocation; - this.repoMounter = repoMounter; - this.notificationHandler = notificationHandler; - - 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 void Upgrade() - { - // Version 1 to Version 2, added OwnerSID - Dictionary allRepos = this.ReadRegistry(); - if (allRepos.Any()) - { - this.WriteRegistry(allRepos); - } - } - - 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 AutoMountRepos(string userId, int sessionId) - { - using (ITracer activity = this.tracer.StartActivity("AutoMount", EventLevel.Informational)) - { - List activeRepos = this.GetActiveReposForUser(userId); - foreach (RepoRegistration repo in activeRepos) - { - // TODO #1089: We need to respect the elevation level of the original mount - if (!this.repoMounter.MountRepository(repo.EnlistmentRoot, sessionId)) - { - this.SendNotification( - sessionId, - NamedPipeMessages.Notification.Request.Identifier.MountFailure, - repo.EnlistmentRoot); - } - } - } - } - - 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 = new EventMetadata(); - metadata.Add("Area", EtwArea); - 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 = new EventMetadata(); - 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 = new EventMetadata(); - 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 = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add("entry", entry); - metadata.Add("Exception", e.ToString()); - 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 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 SendNotification( - int sessionId, - NamedPipeMessages.Notification.Request.Identifier requestId, - string enlistment = null, - int enlistmentCount = 0) - { - NamedPipeMessages.Notification.Request request = new NamedPipeMessages.Notification.Request(); - request.Id = requestId; - request.Enlistment = enlistment; - request.EnlistmentCount = enlistmentCount; - - this.notificationHandler.SendNotification(sessionId, request); - } - - 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)); - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.Service.Handlers; +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 IRepoMounter repoMounter; + private INotificationHandler notificationHandler; + + public RepoRegistry( + ITracer tracer, + PhysicalFileSystem fileSystem, + string serviceDataLocation, + IRepoMounter repoMounter, + INotificationHandler notificationHandler) + { + this.tracer = tracer; + this.fileSystem = fileSystem; + this.registryParentFolderPath = serviceDataLocation; + this.repoMounter = repoMounter; + this.notificationHandler = notificationHandler; + + 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 void Upgrade() + { + // Version 1 to Version 2, added OwnerSID + Dictionary allRepos = this.ReadRegistry(); + if (allRepos.Any()) + { + this.WriteRegistry(allRepos); + } + } + + 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 AutoMountRepos(string userId, int sessionId) + { + using (ITracer activity = this.tracer.StartActivity("AutoMount", EventLevel.Informational)) + { + List activeRepos = this.GetActiveReposForUser(userId); + foreach (RepoRegistration repo in activeRepos) + { + // TODO #1089: We need to respect the elevation level of the original mount + if (!this.repoMounter.MountRepository(repo.EnlistmentRoot, sessionId)) + { + this.SendNotification( + sessionId, + NamedPipeMessages.Notification.Request.Identifier.MountFailure, + repo.EnlistmentRoot); + } + } + } + } + + 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 = new EventMetadata(); + metadata.Add("Area", EtwArea); + 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 = new EventMetadata(); + 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 = new EventMetadata(); + 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 = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add("entry", entry); + metadata.Add("Exception", e.ToString()); + 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 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 SendNotification( + int sessionId, + NamedPipeMessages.Notification.Request.Identifier requestId, + string enlistment = null, + int enlistmentCount = 0) + { + NamedPipeMessages.Notification.Request request = new NamedPipeMessages.Notification.Request(); + request.Id = requestId; + request.Enlistment = enlistment; + request.EnlistmentCount = enlistmentCount; + + this.notificationHandler.SendNotification(sessionId, request); + } + + 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/Scalar.Service.Mac.csproj b/Scalar.Service/Scalar.Service.Mac.csproj index a29134f98e..86e7893ab9 100644 --- a/Scalar.Service/Scalar.Service.Mac.csproj +++ b/Scalar.Service/Scalar.Service.Mac.csproj @@ -1,51 +1,51 @@ - - - - Exe - Scalar.Service - Scalar.Service - - netcoreapp2.1; netstandard2.0 - x64 - osx-x64 - false - true - - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - - - + + + + Exe + Scalar.Service + Scalar.Service + + netcoreapp2.1; netstandard2.0 + x64 + osx-x64 + false + true + + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + + + diff --git a/Scalar.Service/Scalar.Service.Windows.csproj b/Scalar.Service/Scalar.Service.Windows.csproj index 5b22848cc7..af9ea5ea5d 100644 --- a/Scalar.Service/Scalar.Service.Windows.csproj +++ b/Scalar.Service/Scalar.Service.Windows.csproj @@ -1,108 +1,108 @@ - - - - - - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} - Exe - Properties - Scalar.Service - Scalar.Service - v4.6.1 - 512 - true - - - - - x64 - true - full - false - DEBUG;TRACE - prompt - 4 - - - x64 - pdbonly - true - TRACE - prompt - 4 - - - - - - $(BuildOutputDir)\Scalar.Service.exe.manifest - - - - False - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - True - - - - - - - - - - - - - - - - - Component - - - - - - - - - - - - - - - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - {4ce404e7-d3fc-471c-993c-64615861ea63} - Scalar.Platform.Windows - - - {93b403fd-dafb-46c5-9636-b122792a548a} - Scalar.Service.UI - - - - - - Scalar.Service.exe.manifest - - - - - - - - - - - - - + + + + + + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} + Exe + Properties + Scalar.Service + Scalar.Service + v4.6.1 + 512 + true + + + + + x64 + true + full + false + DEBUG;TRACE + prompt + 4 + + + x64 + pdbonly + true + TRACE + prompt + 4 + + + + + + $(BuildOutputDir)\Scalar.Service.exe.manifest + + + + False + ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True + + + + + + + + + + + + + + + + + Component + + + + + + + + + + + + + + + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + {4ce404e7-d3fc-471c-993c-64615861ea63} + Scalar.Platform.Windows + + + {93b403fd-dafb-46c5-9636-b122792a548a} + Scalar.Service.UI + + + + + + Scalar.Service.exe.manifest + + + + + + + + + + + + + diff --git a/Scalar.Service/ScalarMountProcess.Mac.cs b/Scalar.Service/ScalarMountProcess.Mac.cs index 1e2459549e..f760740da5 100644 --- a/Scalar.Service/ScalarMountProcess.Mac.cs +++ b/Scalar.Service/ScalarMountProcess.Mac.cs @@ -1,87 +1,87 @@ -using Scalar.Common; +using Scalar.Common; using Scalar.Common.Tracing; using System.Diagnostics; -using System.IO; - -namespace Scalar.Service -{ - public class ScalarMountProcess : IRepoMounter - { - private const string ExecutablePath = "/bin/launchctl"; - - private MountLauncher processLauncher; - private ITracer tracer; - - public ScalarMountProcess(ITracer tracer, MountLauncher processLauncher = null) - { - this.tracer = tracer; - this.processLauncher = processLauncher ?? new MountLauncher(tracer); - } - - public bool MountRepository(string repoRoot, int sessionId) - { - string arguments = string.Format( - "asuser {0} {1} mount {2}", - sessionId, - Path.Combine(ScalarPlatform.Instance.Constants.ScalarBinDirectoryPath, ScalarPlatform.Instance.Constants.ScalarExecutableName), - repoRoot); - - if (!this.processLauncher.LaunchProcess(ExecutablePath, arguments, repoRoot)) - { - this.tracer.RelatedError($"{nameof(this.MountRepository)}: Unable to start the Scalar process."); - return false; - } - - string errorMessage; - if (!this.processLauncher.WaitUntilMounted(this.tracer, repoRoot, false, out errorMessage)) - { - this.tracer.RelatedError(errorMessage); - return false; - } - - return true; - } - - public class MountLauncher - { - private ITracer tracer; - - public MountLauncher(ITracer tracer) - { - this.tracer = tracer; - } - - public virtual bool LaunchProcess(string executablePath, string arguments, string workingDirectory) - { - ProcessStartInfo processInfo = new ProcessStartInfo(executablePath); - processInfo.Arguments = arguments; - processInfo.WindowStyle = ProcessWindowStyle.Hidden; - processInfo.WorkingDirectory = workingDirectory; - processInfo.UseShellExecute = false; - processInfo.RedirectStandardOutput = true; - - ProcessResult result = ProcessHelper.Run(processInfo); - if (result.ExitCode != 0) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", nameof(ScalarMountProcess)); - metadata.Add(nameof(executablePath), executablePath); - metadata.Add(nameof(arguments), arguments); - metadata.Add(nameof(workingDirectory), workingDirectory); - metadata.Add(nameof(result.ExitCode), result.ExitCode); - metadata.Add(nameof(result.Errors), result.Errors); - - this.tracer.RelatedError(metadata, $"{nameof(this.LaunchProcess)} ERROR: Could not launch {executablePath}"); - return false; - } - - return true; - } - - public virtual bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage) - { - return ScalarEnlistment.WaitUntilMounted(tracer, enlistmentRoot, unattended: false, errorMessage: out errorMessage); - } - } - } -} +using System.IO; + +namespace Scalar.Service +{ + public class ScalarMountProcess : IRepoMounter + { + private const string ExecutablePath = "/bin/launchctl"; + + private MountLauncher processLauncher; + private ITracer tracer; + + public ScalarMountProcess(ITracer tracer, MountLauncher processLauncher = null) + { + this.tracer = tracer; + this.processLauncher = processLauncher ?? new MountLauncher(tracer); + } + + public bool MountRepository(string repoRoot, int sessionId) + { + string arguments = string.Format( + "asuser {0} {1} mount {2}", + sessionId, + Path.Combine(ScalarPlatform.Instance.Constants.ScalarBinDirectoryPath, ScalarPlatform.Instance.Constants.ScalarExecutableName), + repoRoot); + + if (!this.processLauncher.LaunchProcess(ExecutablePath, arguments, repoRoot)) + { + this.tracer.RelatedError($"{nameof(this.MountRepository)}: Unable to start the Scalar process."); + return false; + } + + string errorMessage; + if (!this.processLauncher.WaitUntilMounted(this.tracer, repoRoot, false, out errorMessage)) + { + this.tracer.RelatedError(errorMessage); + return false; + } + + return true; + } + + public class MountLauncher + { + private ITracer tracer; + + public MountLauncher(ITracer tracer) + { + this.tracer = tracer; + } + + public virtual bool LaunchProcess(string executablePath, string arguments, string workingDirectory) + { + ProcessStartInfo processInfo = new ProcessStartInfo(executablePath); + processInfo.Arguments = arguments; + processInfo.WindowStyle = ProcessWindowStyle.Hidden; + processInfo.WorkingDirectory = workingDirectory; + processInfo.UseShellExecute = false; + processInfo.RedirectStandardOutput = true; + + ProcessResult result = ProcessHelper.Run(processInfo); + if (result.ExitCode != 0) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", nameof(ScalarMountProcess)); + metadata.Add(nameof(executablePath), executablePath); + metadata.Add(nameof(arguments), arguments); + metadata.Add(nameof(workingDirectory), workingDirectory); + metadata.Add(nameof(result.ExitCode), result.ExitCode); + metadata.Add(nameof(result.Errors), result.Errors); + + this.tracer.RelatedError(metadata, $"{nameof(this.LaunchProcess)} ERROR: Could not launch {executablePath}"); + return false; + } + + return true; + } + + public virtual bool WaitUntilMounted(ITracer tracer, string enlistmentRoot, bool unattended, out string errorMessage) + { + return ScalarEnlistment.WaitUntilMounted(tracer, enlistmentRoot, unattended: false, errorMessage: out errorMessage); + } + } + } +} diff --git a/Scalar.Service/ScalarMountProcess.Windows.cs b/Scalar.Service/ScalarMountProcess.Windows.cs index 731350f85b..09e8b1303b 100644 --- a/Scalar.Service/ScalarMountProcess.Windows.cs +++ b/Scalar.Service/ScalarMountProcess.Windows.cs @@ -1,46 +1,46 @@ -using Scalar.Common; -using Scalar.Common.Tracing; -using Scalar.Platform.Windows; -using Scalar.Service.Handlers; - -namespace Scalar.Service -{ - public class ScalarMountProcess : IRepoMounter - { - private readonly ITracer tracer; - - public ScalarMountProcess(ITracer tracer) - { - this.tracer = tracer; - } - - public bool MountRepository(string repoRoot, int sessionId) - { - using (CurrentUser currentUser = new CurrentUser(this.tracer, sessionId)) - { - if (!this.CallScalarMount(repoRoot, currentUser)) - { - this.tracer.RelatedError($"{nameof(this.MountRepository)}: Unable to start the Scalar.exe process."); - return false; - } - - string errorMessage; - if (!ScalarEnlistment.WaitUntilMounted(this.tracer, repoRoot, false, out errorMessage)) - { - this.tracer.RelatedError(errorMessage); - return false; - } - } - - return true; - } - - private bool CallScalarMount(string repoRoot, CurrentUser currentUser) - { - InternalVerbParameters mountInternal = new InternalVerbParameters(startedByService: true); - return currentUser.RunAs( - Configuration.Instance.ScalarLocation, - $"mount {repoRoot} --{ScalarConstants.VerbParameters.InternalUseOnly} {mountInternal.ToJson()}"); - } - } -} +using Scalar.Common; +using Scalar.Common.Tracing; +using Scalar.Platform.Windows; +using Scalar.Service.Handlers; + +namespace Scalar.Service +{ + public class ScalarMountProcess : IRepoMounter + { + private readonly ITracer tracer; + + public ScalarMountProcess(ITracer tracer) + { + this.tracer = tracer; + } + + public bool MountRepository(string repoRoot, int sessionId) + { + using (CurrentUser currentUser = new CurrentUser(this.tracer, sessionId)) + { + if (!this.CallScalarMount(repoRoot, currentUser)) + { + this.tracer.RelatedError($"{nameof(this.MountRepository)}: Unable to start the Scalar.exe process."); + return false; + } + + string errorMessage; + if (!ScalarEnlistment.WaitUntilMounted(this.tracer, repoRoot, false, out errorMessage)) + { + this.tracer.RelatedError(errorMessage); + return false; + } + } + + return true; + } + + private bool CallScalarMount(string repoRoot, CurrentUser currentUser) + { + InternalVerbParameters mountInternal = new InternalVerbParameters(startedByService: true); + return currentUser.RunAs( + Configuration.Instance.ScalarLocation, + $"mount {repoRoot} --{ScalarConstants.VerbParameters.InternalUseOnly} {mountInternal.ToJson()}"); + } + } +} diff --git a/Scalar.Service/ScalarService.Mac.cs b/Scalar.Service/ScalarService.Mac.cs index cde1d6a434..e7fb24aa8f 100644 --- a/Scalar.Service/ScalarService.Mac.cs +++ b/Scalar.Service/ScalarService.Mac.cs @@ -1,109 +1,109 @@ -using Scalar.Common; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.Service.Handlers; -using System; -using System.Threading; - -namespace Scalar.Service -{ - public class ScalarService - { - public const string ServiceNameArgPrefix = "--servicename="; - - private const string EtwArea = nameof(ScalarService); - - private ITracer tracer; - private Thread serviceThread; - private ManualResetEvent serviceStopped; - private string serviceName; - private IRepoRegistry repoRegistry; - private RequestHandler requestHandler; - - public ScalarService( - ITracer tracer, - string serviceName, - IRepoRegistry repoRegistry) - { - this.tracer = tracer; - this.repoRegistry = repoRegistry; - this.serviceName = serviceName; - - this.serviceStopped = new ManualResetEvent(false); - this.serviceThread = new Thread(this.ServiceThreadMain); - this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); - } - - public void Run() - { - try - { - this.AutoMountReposForUser(); - - if (!string.IsNullOrEmpty(this.serviceName)) - { - string pipeName = ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.serviceName); - this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); - - using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer( - pipeName, - this.tracer, - this.requestHandler.HandleRequest)) - { - this.serviceThread.Start(); - this.serviceThread.Join(); - } - } - else - { - this.tracer.RelatedError("No name specified for Service Pipe."); - } - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.Run)); - } - } - - private void ServiceThreadMain() - { - try - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); - this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ScalarService)}_{nameof(this.ServiceThreadMain)}", metadata); - - this.serviceStopped.WaitOne(); - this.serviceStopped.Dispose(); - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.ServiceThreadMain)); - } - } - - private void AutoMountReposForUser() - { - string currentUser = ScalarPlatform.Instance.GetCurrentUser(); - if (int.TryParse(currentUser, out int sessionId)) - { - // On Mac, there is no separate session Id. currentUser is used as sessionId - this.repoRegistry.AutoMountRepos(currentUser, sessionId); - this.repoRegistry.TraceStatus(); - } - else - { - this.tracer.RelatedError($"{nameof(this.AutoMountReposForUser)} Error: could not parse current user({currentUser}) info from RepoRegistry."); - } - } - - private void LogExceptionAndExit(Exception e, string method) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add("Exception", e.ToString()); - this.tracer.RelatedError(metadata, "Unhandled exception in " + method); - Environment.Exit((int)ReturnCode.GenericError); - } - } -} +using Scalar.Common; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.Service.Handlers; +using System; +using System.Threading; + +namespace Scalar.Service +{ + public class ScalarService + { + public const string ServiceNameArgPrefix = "--servicename="; + + private const string EtwArea = nameof(ScalarService); + + private ITracer tracer; + private Thread serviceThread; + private ManualResetEvent serviceStopped; + private string serviceName; + private IRepoRegistry repoRegistry; + private RequestHandler requestHandler; + + public ScalarService( + ITracer tracer, + string serviceName, + IRepoRegistry repoRegistry) + { + this.tracer = tracer; + this.repoRegistry = repoRegistry; + this.serviceName = serviceName; + + this.serviceStopped = new ManualResetEvent(false); + this.serviceThread = new Thread(this.ServiceThreadMain); + this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); + } + + public void Run() + { + try + { + this.AutoMountReposForUser(); + + if (!string.IsNullOrEmpty(this.serviceName)) + { + string pipeName = ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.serviceName); + this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); + + using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer( + pipeName, + this.tracer, + this.requestHandler.HandleRequest)) + { + this.serviceThread.Start(); + this.serviceThread.Join(); + } + } + else + { + this.tracer.RelatedError("No name specified for Service Pipe."); + } + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.Run)); + } + } + + private void ServiceThreadMain() + { + try + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); + this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ScalarService)}_{nameof(this.ServiceThreadMain)}", metadata); + + this.serviceStopped.WaitOne(); + this.serviceStopped.Dispose(); + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.ServiceThreadMain)); + } + } + + private void AutoMountReposForUser() + { + string currentUser = ScalarPlatform.Instance.GetCurrentUser(); + if (int.TryParse(currentUser, out int sessionId)) + { + // On Mac, there is no separate session Id. currentUser is used as sessionId + this.repoRegistry.AutoMountRepos(currentUser, sessionId); + this.repoRegistry.TraceStatus(); + } + else + { + this.tracer.RelatedError($"{nameof(this.AutoMountReposForUser)} Error: could not parse current user({currentUser}) info from RepoRegistry."); + } + } + + private void LogExceptionAndExit(Exception e, string method) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(metadata, "Unhandled exception in " + method); + Environment.Exit((int)ReturnCode.GenericError); + } + } +} diff --git a/Scalar.Service/ScalarService.Windows.cs b/Scalar.Service/ScalarService.Windows.cs index 33d49b90d6..c23d886d0e 100644 --- a/Scalar.Service/ScalarService.Windows.cs +++ b/Scalar.Service/ScalarService.Windows.cs @@ -1,302 +1,302 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.Platform.Windows; -using Scalar.Service.Handlers; -using System; -using System.IO; -using System.Linq; -using System.Security.AccessControl; -using System.ServiceProcess; -using System.Threading; - -namespace Scalar.Service -{ - public class ScalarService : ServiceBase - { - private const string ServiceNameArgPrefix = "--servicename="; - private const string EtwArea = nameof(ScalarService); - - private JsonTracer tracer; - private Thread serviceThread; - private ManualResetEvent serviceStopped; - private string serviceName; - private string serviceDataLocation; - private RepoRegistry repoRegistry; - private ProductUpgradeTimer productUpgradeTimer; - private RequestHandler requestHandler; - - public ScalarService(JsonTracer tracer) - { - this.tracer = tracer; - this.serviceName = ScalarConstants.Service.ServiceName; - this.CanHandleSessionChangeEvent = true; - this.productUpgradeTimer = new ProductUpgradeTimer(tracer); - } - - public void Run() - { - try - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); - this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ScalarService)}_{nameof(this.Run)}", metadata); - - this.repoRegistry = new RepoRegistry( - this.tracer, - new PhysicalFileSystem(), - this.serviceDataLocation, - new ScalarMountProcess(this.tracer), - new NotificationHandler(this.tracer)); - this.repoRegistry.Upgrade(); - this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); - - string pipeName = ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.serviceName); - this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); - - using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer( - pipeName, - this.tracer, - this.requestHandler.HandleRequest)) - { - this.productUpgradeTimer.Start(); - - this.serviceStopped.WaitOne(); - } - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.Run)); - } - } - - public void StopRunning() - { - if (this.serviceStopped == null) - { - return; - } - - try - { - if (this.productUpgradeTimer != null) - { - this.productUpgradeTimer.Stop(); - } - - if (this.tracer != null) - { - this.tracer.RelatedInfo("Stopping"); - } - - if (this.serviceStopped != null) - { - this.serviceStopped.Set(); - } - - if (this.serviceThread != null) - { - this.serviceThread.Join(); - this.serviceThread = null; - - if (this.serviceStopped != null) - { - this.serviceStopped.Dispose(); - this.serviceStopped = null; - } - } - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.StopRunning)); - } - } - - protected override void OnSessionChange(SessionChangeDescription changeDescription) - { - try - { - base.OnSessionChange(changeDescription); - - if (!ScalarEnlistment.IsUnattended(tracer: null)) - { - if (changeDescription.Reason == SessionChangeReason.SessionLogon) - { - this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", changeDescription.SessionId); - using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational)) - { - this.repoRegistry.AutoMountRepos( - ScalarPlatform.Instance.GetUserIdFromLoginSessionId(changeDescription.SessionId, this.tracer), - changeDescription.SessionId); - this.repoRegistry.TraceStatus(); - } - } - else if (changeDescription.Reason == SessionChangeReason.SessionLogoff) - { - this.tracer.RelatedInfo("SessionLogoff detected"); - } - } - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.OnSessionChange)); - } - } - - protected override void OnStart(string[] args) - { - if (this.serviceThread != null) - { - throw new InvalidOperationException("Cannot start service twice in a row."); - } - - // TODO: 865304 Used for functional tests and development only. Replace with a smarter appConfig-based solution - string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ServiceNameArgPrefix)); - if (serviceName != null) - { - this.serviceName = serviceName.Substring(ServiceNameArgPrefix.Length); - } - - string serviceLogsDirectoryPath = Path.Combine( - ScalarPlatform.Instance.GetDataRootForScalarComponent(this.serviceName), - ScalarConstants.Service.LogDirectory); - - // Create the logs directory explicitly *before* creating a log file event listener to ensure that it - // and its ancestor directories are created with the correct ACLs. - this.CreateServiceLogsDirectory(serviceLogsDirectoryPath); - this.tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(serviceLogsDirectoryPath, ScalarConstants.LogFileTypes.Service), - EventLevel.Verbose, - Keywords.Any); - - try - { - this.serviceDataLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(this.serviceName); - this.CreateAndConfigureProgramDataDirectories(); - this.Start(); - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.OnStart)); - } - } - - protected override void OnStop() - { - try - { - this.StopRunning(); - } - catch (Exception e) - { - this.LogExceptionAndExit(e, nameof(this.OnStart)); - } - } - - protected override void Dispose(bool disposing) - { - this.StopRunning(); - - if (this.tracer != null) - { - this.tracer.Dispose(); - this.tracer = null; - } - - base.Dispose(disposing); - } - - private void Start() - { - if (this.serviceStopped != null) - { - return; - } - - this.serviceStopped = new ManualResetEvent(false); - this.serviceThread = new Thread(this.Run); - - this.serviceThread.Start(); - } - - private void LogExceptionAndExit(Exception e, string method) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add("Exception", e.ToString()); - this.tracer.RelatedError(metadata, "Unhandled exception in " + method); - Environment.Exit((int)ReturnCode.GenericError); - } - - private void CreateServiceLogsDirectory(string serviceLogsDirectoryPath) - { - if (!Directory.Exists(serviceLogsDirectoryPath)) - { - DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceLogsDirectoryPath); - Directory.CreateDirectory(serviceLogsDirectoryPath); - } - } - - private void CreateAndConfigureProgramDataDirectories() - { - string serviceDataRootPath = Path.GetDirectoryName(this.serviceDataLocation); - - DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceDataRootPath); - - // Create Scalar.Service and Scalar.Upgrade related directories (if they don't already exist) - Directory.CreateDirectory(serviceDataRootPath, serviceDataRootSecurity); - Directory.CreateDirectory(this.serviceDataLocation, serviceDataRootSecurity); - Directory.CreateDirectory(ProductUpgraderInfo.GetUpgradeProtectedDataDirectory(), serviceDataRootSecurity); - - // Ensure the ACLs are set correctly on any files or directories that were already created (e.g. after upgrading Scalar) - Directory.SetAccessControl(serviceDataRootPath, serviceDataRootSecurity); - - // Special rules for the upgrader logs, as non-elevated users need to be be able to write - this.CreateAndConfigureUpgradeLogDirectory(); - } - - private void CreateAndConfigureUpgradeLogDirectory() - { - string upgradeLogsPath = ProductUpgraderInfo.GetLogDirectoryPath(); - - string error; - if (!ScalarPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(upgradeLogsPath, out error)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Area", EtwArea); - metadata.Add(nameof(upgradeLogsPath), upgradeLogsPath); - metadata.Add(nameof(error), error); - this.tracer.RelatedWarning( - metadata, - $"{nameof(this.CreateAndConfigureUpgradeLogDirectory)}: Failed to create upgrade logs directory", - Keywords.Telemetry); - } - } - - private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath) - { - DirectorySecurity serviceDataRootSecurity; - if (Directory.Exists(serviceDataRootPath)) - { - this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} exists, modifying ACLs."); - serviceDataRootSecurity = Directory.GetAccessControl(serviceDataRootPath); - } - else - { - this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} does not exist, creating new ACLs."); - serviceDataRootSecurity = new DirectorySecurity(); - } - - // Protect the access rules from inheritance and remove any inherited rules - serviceDataRootSecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); - - // Remove any existing ACLs and add new ACLs for users and admins - WindowsFileSystem.RemoveAllFileSystemAccessRulesFromDirectorySecurity(serviceDataRootSecurity); - WindowsFileSystem.AddUsersAccessRulesToDirectorySecurity(serviceDataRootSecurity, grantUsersModifyPermissions: false); - WindowsFileSystem.AddAdminAccessRulesToDirectorySecurity(serviceDataRootSecurity); - - return serviceDataRootSecurity; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.Platform.Windows; +using Scalar.Service.Handlers; +using System; +using System.IO; +using System.Linq; +using System.Security.AccessControl; +using System.ServiceProcess; +using System.Threading; + +namespace Scalar.Service +{ + public class ScalarService : ServiceBase + { + private const string ServiceNameArgPrefix = "--servicename="; + private const string EtwArea = nameof(ScalarService); + + private JsonTracer tracer; + private Thread serviceThread; + private ManualResetEvent serviceStopped; + private string serviceName; + private string serviceDataLocation; + private RepoRegistry repoRegistry; + private ProductUpgradeTimer productUpgradeTimer; + private RequestHandler requestHandler; + + public ScalarService(JsonTracer tracer) + { + this.tracer = tracer; + this.serviceName = ScalarConstants.Service.ServiceName; + this.CanHandleSessionChangeEvent = true; + this.productUpgradeTimer = new ProductUpgradeTimer(tracer); + } + + public void Run() + { + try + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Version", ProcessHelper.GetCurrentProcessVersion()); + this.tracer.RelatedEvent(EventLevel.Informational, $"{nameof(ScalarService)}_{nameof(this.Run)}", metadata); + + this.repoRegistry = new RepoRegistry( + this.tracer, + new PhysicalFileSystem(), + this.serviceDataLocation, + new ScalarMountProcess(this.tracer), + new NotificationHandler(this.tracer)); + this.repoRegistry.Upgrade(); + this.requestHandler = new RequestHandler(this.tracer, EtwArea, this.repoRegistry); + + string pipeName = ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.serviceName); + this.tracer.RelatedInfo("Starting pipe server with name: " + pipeName); + + using (NamedPipeServer pipeServer = NamedPipeServer.StartNewServer( + pipeName, + this.tracer, + this.requestHandler.HandleRequest)) + { + this.productUpgradeTimer.Start(); + + this.serviceStopped.WaitOne(); + } + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.Run)); + } + } + + public void StopRunning() + { + if (this.serviceStopped == null) + { + return; + } + + try + { + if (this.productUpgradeTimer != null) + { + this.productUpgradeTimer.Stop(); + } + + if (this.tracer != null) + { + this.tracer.RelatedInfo("Stopping"); + } + + if (this.serviceStopped != null) + { + this.serviceStopped.Set(); + } + + if (this.serviceThread != null) + { + this.serviceThread.Join(); + this.serviceThread = null; + + if (this.serviceStopped != null) + { + this.serviceStopped.Dispose(); + this.serviceStopped = null; + } + } + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.StopRunning)); + } + } + + protected override void OnSessionChange(SessionChangeDescription changeDescription) + { + try + { + base.OnSessionChange(changeDescription); + + if (!ScalarEnlistment.IsUnattended(tracer: null)) + { + if (changeDescription.Reason == SessionChangeReason.SessionLogon) + { + this.tracer.RelatedInfo("SessionLogon detected, sessionId: {0}", changeDescription.SessionId); + using (ITracer activity = this.tracer.StartActivity("LogonAutomount", EventLevel.Informational)) + { + this.repoRegistry.AutoMountRepos( + ScalarPlatform.Instance.GetUserIdFromLoginSessionId(changeDescription.SessionId, this.tracer), + changeDescription.SessionId); + this.repoRegistry.TraceStatus(); + } + } + else if (changeDescription.Reason == SessionChangeReason.SessionLogoff) + { + this.tracer.RelatedInfo("SessionLogoff detected"); + } + } + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.OnSessionChange)); + } + } + + protected override void OnStart(string[] args) + { + if (this.serviceThread != null) + { + throw new InvalidOperationException("Cannot start service twice in a row."); + } + + // TODO: 865304 Used for functional tests and development only. Replace with a smarter appConfig-based solution + string serviceName = args.FirstOrDefault(arg => arg.StartsWith(ServiceNameArgPrefix)); + if (serviceName != null) + { + this.serviceName = serviceName.Substring(ServiceNameArgPrefix.Length); + } + + string serviceLogsDirectoryPath = Path.Combine( + ScalarPlatform.Instance.GetDataRootForScalarComponent(this.serviceName), + ScalarConstants.Service.LogDirectory); + + // Create the logs directory explicitly *before* creating a log file event listener to ensure that it + // and its ancestor directories are created with the correct ACLs. + this.CreateServiceLogsDirectory(serviceLogsDirectoryPath); + this.tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(serviceLogsDirectoryPath, ScalarConstants.LogFileTypes.Service), + EventLevel.Verbose, + Keywords.Any); + + try + { + this.serviceDataLocation = ScalarPlatform.Instance.GetDataRootForScalarComponent(this.serviceName); + this.CreateAndConfigureProgramDataDirectories(); + this.Start(); + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.OnStart)); + } + } + + protected override void OnStop() + { + try + { + this.StopRunning(); + } + catch (Exception e) + { + this.LogExceptionAndExit(e, nameof(this.OnStart)); + } + } + + protected override void Dispose(bool disposing) + { + this.StopRunning(); + + if (this.tracer != null) + { + this.tracer.Dispose(); + this.tracer = null; + } + + base.Dispose(disposing); + } + + private void Start() + { + if (this.serviceStopped != null) + { + return; + } + + this.serviceStopped = new ManualResetEvent(false); + this.serviceThread = new Thread(this.Run); + + this.serviceThread.Start(); + } + + private void LogExceptionAndExit(Exception e, string method) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add("Exception", e.ToString()); + this.tracer.RelatedError(metadata, "Unhandled exception in " + method); + Environment.Exit((int)ReturnCode.GenericError); + } + + private void CreateServiceLogsDirectory(string serviceLogsDirectoryPath) + { + if (!Directory.Exists(serviceLogsDirectoryPath)) + { + DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceLogsDirectoryPath); + Directory.CreateDirectory(serviceLogsDirectoryPath); + } + } + + private void CreateAndConfigureProgramDataDirectories() + { + string serviceDataRootPath = Path.GetDirectoryName(this.serviceDataLocation); + + DirectorySecurity serviceDataRootSecurity = this.GetServiceDirectorySecurity(serviceDataRootPath); + + // Create Scalar.Service and Scalar.Upgrade related directories (if they don't already exist) + Directory.CreateDirectory(serviceDataRootPath, serviceDataRootSecurity); + Directory.CreateDirectory(this.serviceDataLocation, serviceDataRootSecurity); + Directory.CreateDirectory(ProductUpgraderInfo.GetUpgradeProtectedDataDirectory(), serviceDataRootSecurity); + + // Ensure the ACLs are set correctly on any files or directories that were already created (e.g. after upgrading Scalar) + Directory.SetAccessControl(serviceDataRootPath, serviceDataRootSecurity); + + // Special rules for the upgrader logs, as non-elevated users need to be be able to write + this.CreateAndConfigureUpgradeLogDirectory(); + } + + private void CreateAndConfigureUpgradeLogDirectory() + { + string upgradeLogsPath = ProductUpgraderInfo.GetLogDirectoryPath(); + + string error; + if (!ScalarPlatform.Instance.FileSystem.TryCreateDirectoryWithAdminAndUserModifyPermissions(upgradeLogsPath, out error)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Area", EtwArea); + metadata.Add(nameof(upgradeLogsPath), upgradeLogsPath); + metadata.Add(nameof(error), error); + this.tracer.RelatedWarning( + metadata, + $"{nameof(this.CreateAndConfigureUpgradeLogDirectory)}: Failed to create upgrade logs directory", + Keywords.Telemetry); + } + } + + private DirectorySecurity GetServiceDirectorySecurity(string serviceDataRootPath) + { + DirectorySecurity serviceDataRootSecurity; + if (Directory.Exists(serviceDataRootPath)) + { + this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} exists, modifying ACLs."); + serviceDataRootSecurity = Directory.GetAccessControl(serviceDataRootPath); + } + else + { + this.tracer.RelatedInfo($"{nameof(this.GetServiceDirectorySecurity)}: {serviceDataRootPath} does not exist, creating new ACLs."); + serviceDataRootSecurity = new DirectorySecurity(); + } + + // Protect the access rules from inheritance and remove any inherited rules + serviceDataRootSecurity.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); + + // Remove any existing ACLs and add new ACLs for users and admins + WindowsFileSystem.RemoveAllFileSystemAccessRulesFromDirectorySecurity(serviceDataRootSecurity); + WindowsFileSystem.AddUsersAccessRulesToDirectorySecurity(serviceDataRootSecurity, grantUsersModifyPermissions: false); + WindowsFileSystem.AddAdminAccessRulesToDirectorySecurity(serviceDataRootSecurity); + + return serviceDataRootSecurity; + } + } +} diff --git a/Scalar.Service/app.config b/Scalar.Service/app.config index 1da8c9b53a..bd27edc04e 100644 --- a/Scalar.Service/app.config +++ b/Scalar.Service/app.config @@ -1,6 +1,6 @@ - - - - - - + + + + + + diff --git a/Scalar.Service/packages.config b/Scalar.Service/packages.config index 1cc7546717..7e38df55c1 100644 --- a/Scalar.Service/packages.config +++ b/Scalar.Service/packages.config @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/Scalar.SignFiles/Scalar.SignFiles.csproj b/Scalar.SignFiles/Scalar.SignFiles.csproj index b74fa9e93f..2325fb6637 100644 --- a/Scalar.SignFiles/Scalar.SignFiles.csproj +++ b/Scalar.SignFiles/Scalar.SignFiles.csproj @@ -1,78 +1,78 @@ - - - - - - - {2F63B22B-EE26-4266-BF17-28A9146483A1} - Library - Properties - Scalar.SignFiles - Scalar.SignFiles - $(BuildOutputDir) - - v4.6.1 - 512 - - - - - - true - DEBUG;TRACE - full - x64 - prompt - MinimumRecommendedRules.ruleset - - - TRACE - true - pdbonly - x64 - prompt - MinimumRecommendedRules.ruleset - - - - Microsoft400 - false - - - - - Designer - - - - - - - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - + + + + + + + {2F63B22B-EE26-4266-BF17-28A9146483A1} + Library + Properties + Scalar.SignFiles + Scalar.SignFiles + $(BuildOutputDir) + + v4.6.1 + 512 + + + + + + true + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + + + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + + + + Microsoft400 + false + + + + + Designer + + + + + + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + diff --git a/Scalar.SignFiles/packages.config b/Scalar.SignFiles/packages.config index 6d4eba11fc..4339ce5166 100644 --- a/Scalar.SignFiles/packages.config +++ b/Scalar.SignFiles/packages.config @@ -1,4 +1,4 @@ - - - + + + diff --git a/Scalar.Tests/DataSources.cs b/Scalar.Tests/DataSources.cs index 15c80393b1..e4c68589fe 100644 --- a/Scalar.Tests/DataSources.cs +++ b/Scalar.Tests/DataSources.cs @@ -1,4 +1,4 @@ -namespace Scalar.Tests +namespace Scalar.Tests { public class DataSources { diff --git a/Scalar.Tests/NUnitRunner.cs b/Scalar.Tests/NUnitRunner.cs index 34141ecf82..d91c9ad127 100644 --- a/Scalar.Tests/NUnitRunner.cs +++ b/Scalar.Tests/NUnitRunner.cs @@ -1,70 +1,70 @@ -using NUnitLite; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Scalar.Tests -{ - public class NUnitRunner - { - private List args; - - public NUnitRunner(string[] args) - { - this.args = new List(args); - } - - public string GetCustomArgWithParam(string arg) - { - string match = this.args.Where(a => a.StartsWith(arg + "=")).SingleOrDefault(); - if (match == null) - { - return null; - } - - this.args.Remove(match); - return match.Substring(arg.Length + 1); - } - - public bool HasCustomArg(string arg) - { - // We also remove it as we're checking, because nunit wouldn't understand what it means - return this.args.Remove(arg); - } - - public int RunTests(ICollection includeCategories, ICollection excludeCategories) - { - string filters = GetFiltersArgument(includeCategories, excludeCategories); - if (filters.Length > 0) - { - this.args.Add("--where"); - this.args.Add(filters); - } - - DateTime now = DateTime.Now; - int result = new AutoRun(Assembly.GetEntryAssembly()).Execute(this.args.ToArray()); - - Console.WriteLine("Completed test pass in {0}", DateTime.Now - now); - Console.WriteLine(); - - return result; - } - - private static string GetFiltersArgument(ICollection includeCategories, ICollection excludeCategories) - { - string filters = string.Empty; - if (includeCategories != null && includeCategories.Any()) - { - filters = "(" + string.Join("||", includeCategories.Select(x => $"cat=={x}")) + ")"; - } - - if (excludeCategories != null && excludeCategories.Any()) - { - filters += (filters.Length > 0 ? "&&" : string.Empty) + string.Join("&&", excludeCategories.Select(x => $"cat!={x}")); - } - - return filters; - } - } -} +using NUnitLite; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Scalar.Tests +{ + public class NUnitRunner + { + private List args; + + public NUnitRunner(string[] args) + { + this.args = new List(args); + } + + public string GetCustomArgWithParam(string arg) + { + string match = this.args.Where(a => a.StartsWith(arg + "=")).SingleOrDefault(); + if (match == null) + { + return null; + } + + this.args.Remove(match); + return match.Substring(arg.Length + 1); + } + + public bool HasCustomArg(string arg) + { + // We also remove it as we're checking, because nunit wouldn't understand what it means + return this.args.Remove(arg); + } + + public int RunTests(ICollection includeCategories, ICollection excludeCategories) + { + string filters = GetFiltersArgument(includeCategories, excludeCategories); + if (filters.Length > 0) + { + this.args.Add("--where"); + this.args.Add(filters); + } + + DateTime now = DateTime.Now; + int result = new AutoRun(Assembly.GetEntryAssembly()).Execute(this.args.ToArray()); + + Console.WriteLine("Completed test pass in {0}", DateTime.Now - now); + Console.WriteLine(); + + return result; + } + + private static string GetFiltersArgument(ICollection includeCategories, ICollection excludeCategories) + { + string filters = string.Empty; + if (includeCategories != null && includeCategories.Any()) + { + filters = "(" + string.Join("||", includeCategories.Select(x => $"cat=={x}")) + ")"; + } + + if (excludeCategories != null && excludeCategories.Any()) + { + filters += (filters.Length > 0 ? "&&" : string.Empty) + string.Join("&&", excludeCategories.Select(x => $"cat!={x}")); + } + + return filters; + } + } +} diff --git a/Scalar.Tests/Scalar.Tests.csproj b/Scalar.Tests/Scalar.Tests.csproj index 15c9d7b426..106d6b858b 100644 --- a/Scalar.Tests/Scalar.Tests.csproj +++ b/Scalar.Tests/Scalar.Tests.csproj @@ -1,24 +1,24 @@ - - - - - netcoreapp2.1;netstandard2.0 - x64 - true - true - win-x64;osx-x64 - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - - all - - + + + + + netcoreapp2.1;netstandard2.0 + x64 + true + true + win-x64;osx-x64 + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + + all + + diff --git a/Scalar.Tests/Should/EnumerableShouldExtensions.cs b/Scalar.Tests/Should/EnumerableShouldExtensions.cs index b162e156fa..c3dca5f90e 100644 --- a/Scalar.Tests/Should/EnumerableShouldExtensions.cs +++ b/Scalar.Tests/Should/EnumerableShouldExtensions.cs @@ -1,155 +1,155 @@ -using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace Scalar.Tests.Should -{ - public static class EnumerableShouldExtensions - { - public static IEnumerable ShouldBeEmpty(this IEnumerable group, string message = null) - { - CollectionAssert.IsEmpty(group, message); - return group; - } - - public static IEnumerable ShouldBeNonEmpty(this IEnumerable group) - { - CollectionAssert.IsNotEmpty(group); - return group; - } - - public static Dictionary ShouldContain(this Dictionary dictionary, TKey key, TValue value) - { - TValue dictionaryValue; - dictionary.TryGetValue(key, out dictionaryValue).ShouldBeTrue($"Dictionary {nameof(ShouldContain)} does not contain {key}"); - dictionaryValue.ShouldEqual(value, $"Dictionary {nameof(ShouldContain)} does not match on key {key} expected: {value} actual: {dictionaryValue}"); - - return dictionary; - } - - public static T ShouldContain(this IEnumerable group, Func predicate) - { - T item = group.FirstOrDefault(predicate); - item.ShouldNotEqual(default(T), "No matching entries found in {" + string.Join(",", group.ToArray()) + "}"); - - return item; - } - - public static T ShouldContainSingle(this IEnumerable group, Func predicate) - { - T item = group.Single(predicate); - item.ShouldNotEqual(default(T)); - - return item; - } - - public static void ShouldNotContain(this IEnumerable group, Func predicate) - { - T item = group.SingleOrDefault(predicate); - item.ShouldEqual(default(T), "Unexpected matching entry found in {" + string.Join(",", group) + "}"); - } - - public static IEnumerable ShouldNotContain(this IEnumerable group, IEnumerable unexpectedValues, Func predicate) - { - List groupList = new List(group); - - foreach (T unexpectedValue in unexpectedValues) - { - Assert.IsFalse(groupList.Any(item => predicate(item, unexpectedValue))); - } - - return group; - } - - public static IEnumerable ShouldContain(this IEnumerable group, IEnumerable expectedValues, Func predicate) - { - List groupList = new List(group); - - foreach (T expectedValue in expectedValues) - { - Assert.IsTrue(groupList.Any(item => predicate(item, expectedValue))); - } - - return group; - } - - public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params Action[] itemCheckers) - { - List groupList = new List(group); - List> itemCheckersList = new List>(itemCheckers); - - for (int i = 0; i < groupList.Count; i++) - { - itemCheckersList[i](groupList[i]); - } - - return group; - } - - public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues, Func equals, string message = "") - { - List groupList = new List(group); - List expectedValuesList = new List(expectedValues); - - Comparer comparer = new Comparer(equals); - List groupExtraItems = groupList.Except(expectedValues, comparer).ToList(); - List groupMissingItems = expectedValues.Except(groupList, comparer).ToList(); - - StringBuilder errorMessage = new StringBuilder(); - - if (groupList.Count != expectedValuesList.Count) - { - errorMessage.AppendLine(string.Format("{0} counts do not match. was: {1} expected: {2}", message, groupList.Count, expectedValuesList.Count)); - } - - foreach (T groupExtraItem in groupExtraItems) - { - errorMessage.AppendLine(string.Format("Extra: {0}", groupExtraItem)); - } - - foreach (T groupMissingItem in groupMissingItems) - { - errorMessage.AppendLine(string.Format("Missing: {0}", groupMissingItem)); - } - - if (errorMessage.Length > 0) - { - Assert.Fail("{0}\r\n{1}", message, errorMessage); - } - - 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; - - public Comparer(Func equals) - { - this.equals = equals; - } - - public bool Equals(T x, T y) - { - return this.equals(x, y); - } - - public int GetHashCode(T obj) - { - return obj.GetHashCode(); - } - } - } -} +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Scalar.Tests.Should +{ + public static class EnumerableShouldExtensions + { + public static IEnumerable ShouldBeEmpty(this IEnumerable group, string message = null) + { + CollectionAssert.IsEmpty(group, message); + return group; + } + + public static IEnumerable ShouldBeNonEmpty(this IEnumerable group) + { + CollectionAssert.IsNotEmpty(group); + return group; + } + + public static Dictionary ShouldContain(this Dictionary dictionary, TKey key, TValue value) + { + TValue dictionaryValue; + dictionary.TryGetValue(key, out dictionaryValue).ShouldBeTrue($"Dictionary {nameof(ShouldContain)} does not contain {key}"); + dictionaryValue.ShouldEqual(value, $"Dictionary {nameof(ShouldContain)} does not match on key {key} expected: {value} actual: {dictionaryValue}"); + + return dictionary; + } + + public static T ShouldContain(this IEnumerable group, Func predicate) + { + T item = group.FirstOrDefault(predicate); + item.ShouldNotEqual(default(T), "No matching entries found in {" + string.Join(",", group.ToArray()) + "}"); + + return item; + } + + public static T ShouldContainSingle(this IEnumerable group, Func predicate) + { + T item = group.Single(predicate); + item.ShouldNotEqual(default(T)); + + return item; + } + + public static void ShouldNotContain(this IEnumerable group, Func predicate) + { + T item = group.SingleOrDefault(predicate); + item.ShouldEqual(default(T), "Unexpected matching entry found in {" + string.Join(",", group) + "}"); + } + + public static IEnumerable ShouldNotContain(this IEnumerable group, IEnumerable unexpectedValues, Func predicate) + { + List groupList = new List(group); + + foreach (T unexpectedValue in unexpectedValues) + { + Assert.IsFalse(groupList.Any(item => predicate(item, unexpectedValue))); + } + + return group; + } + + public static IEnumerable ShouldContain(this IEnumerable group, IEnumerable expectedValues, Func predicate) + { + List groupList = new List(group); + + foreach (T expectedValue in expectedValues) + { + Assert.IsTrue(groupList.Any(item => predicate(item, expectedValue))); + } + + return group; + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, params Action[] itemCheckers) + { + List groupList = new List(group); + List> itemCheckersList = new List>(itemCheckers); + + for (int i = 0; i < groupList.Count; i++) + { + itemCheckersList[i](groupList[i]); + } + + return group; + } + + public static IEnumerable ShouldMatchInOrder(this IEnumerable group, IEnumerable expectedValues, Func equals, string message = "") + { + List groupList = new List(group); + List expectedValuesList = new List(expectedValues); + + Comparer comparer = new Comparer(equals); + List groupExtraItems = groupList.Except(expectedValues, comparer).ToList(); + List groupMissingItems = expectedValues.Except(groupList, comparer).ToList(); + + StringBuilder errorMessage = new StringBuilder(); + + if (groupList.Count != expectedValuesList.Count) + { + errorMessage.AppendLine(string.Format("{0} counts do not match. was: {1} expected: {2}", message, groupList.Count, expectedValuesList.Count)); + } + + foreach (T groupExtraItem in groupExtraItems) + { + errorMessage.AppendLine(string.Format("Extra: {0}", groupExtraItem)); + } + + foreach (T groupMissingItem in groupMissingItems) + { + errorMessage.AppendLine(string.Format("Missing: {0}", groupMissingItem)); + } + + if (errorMessage.Length > 0) + { + Assert.Fail("{0}\r\n{1}", message, errorMessage); + } + + 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; + + public Comparer(Func equals) + { + this.equals = equals; + } + + public bool Equals(T x, T y) + { + return this.equals(x, y); + } + + public int GetHashCode(T obj) + { + return obj.GetHashCode(); + } + } + } +} diff --git a/Scalar.Tests/Should/StringExtensions.cs b/Scalar.Tests/Should/StringExtensions.cs index 75f90dc5db..9e03059858 100644 --- a/Scalar.Tests/Should/StringExtensions.cs +++ b/Scalar.Tests/Should/StringExtensions.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Scalar.Tests.Should -{ - public static class StringExtensions - { - public static string Repeat(this string self, int count) - { - return string.Join(string.Empty, Enumerable.Range(0, count).Select(x => self).ToArray()); - } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scalar.Tests.Should +{ + public static class StringExtensions + { + public static string Repeat(this string self, int count) + { + return string.Join(string.Empty, Enumerable.Range(0, count).Select(x => self).ToArray()); + } + } +} diff --git a/Scalar.Tests/Should/StringShouldExtensions.cs b/Scalar.Tests/Should/StringShouldExtensions.cs index b134a79cb0..8c7eb6b761 100644 --- a/Scalar.Tests/Should/StringShouldExtensions.cs +++ b/Scalar.Tests/Should/StringShouldExtensions.cs @@ -1,68 +1,68 @@ -using NUnit.Framework; -using System; - -namespace Scalar.Tests.Should -{ - public static class StringShouldExtensions - { - public static int ShouldBeAnInt(this string value, string message) - { - int output; - Assert.IsTrue(int.TryParse(value, out output), message); - return output; - } - - public static string ShouldContain(this string actualValue, params string[] expectedSubstrings) - { - foreach (string expectedSubstring in expectedSubstrings) - { - Assert.IsTrue( - actualValue.Contains(expectedSubstring), - "Expected substring '{0}' not found in '{1}'", - expectedSubstring, - actualValue); - } - - return actualValue; - } - - public static string ShouldNotContain(this string actualValue, bool ignoreCase, params string[] unexpectedSubstrings) - { - foreach (string unexpectedSubstring in unexpectedSubstrings) - { - if (ignoreCase) - { - Assert.IsFalse( - actualValue.IndexOf(unexpectedSubstring, 0, StringComparison.OrdinalIgnoreCase) >= 0, - "Unexpected substring '{0}' found in '{1}'", - unexpectedSubstring, - actualValue); - } - else - { - Assert.IsFalse( - actualValue.Contains(unexpectedSubstring), - "Unexpected substring '{0}' found in '{1}'", - unexpectedSubstring, - actualValue); - } - } - - return actualValue; - } - - public static string ShouldContainOneOf(this string actualValue, params string[] expectedSubstrings) - { - for (int i = 0; i < expectedSubstrings.Length; i++) - { - if (actualValue.Contains(expectedSubstrings[i])) - { - return actualValue; - } - } - - Assert.Fail("No expected substrings found in '{0}'", actualValue); - return actualValue; - } - } -} +using NUnit.Framework; +using System; + +namespace Scalar.Tests.Should +{ + public static class StringShouldExtensions + { + public static int ShouldBeAnInt(this string value, string message) + { + int output; + Assert.IsTrue(int.TryParse(value, out output), message); + return output; + } + + public static string ShouldContain(this string actualValue, params string[] expectedSubstrings) + { + foreach (string expectedSubstring in expectedSubstrings) + { + Assert.IsTrue( + actualValue.Contains(expectedSubstring), + "Expected substring '{0}' not found in '{1}'", + expectedSubstring, + actualValue); + } + + return actualValue; + } + + public static string ShouldNotContain(this string actualValue, bool ignoreCase, params string[] unexpectedSubstrings) + { + foreach (string unexpectedSubstring in unexpectedSubstrings) + { + if (ignoreCase) + { + Assert.IsFalse( + actualValue.IndexOf(unexpectedSubstring, 0, StringComparison.OrdinalIgnoreCase) >= 0, + "Unexpected substring '{0}' found in '{1}'", + unexpectedSubstring, + actualValue); + } + else + { + Assert.IsFalse( + actualValue.Contains(unexpectedSubstring), + "Unexpected substring '{0}' found in '{1}'", + unexpectedSubstring, + actualValue); + } + } + + return actualValue; + } + + public static string ShouldContainOneOf(this string actualValue, params string[] expectedSubstrings) + { + for (int i = 0; i < expectedSubstrings.Length; i++) + { + if (actualValue.Contains(expectedSubstrings[i])) + { + return actualValue; + } + } + + Assert.Fail("No expected substrings found in '{0}'", actualValue); + return actualValue; + } + } +} diff --git a/Scalar.Tests/Should/ValueShouldExtensions.cs b/Scalar.Tests/Should/ValueShouldExtensions.cs index 92e81e0fa9..ab3f8b46ff 100644 --- a/Scalar.Tests/Should/ValueShouldExtensions.cs +++ b/Scalar.Tests/Should/ValueShouldExtensions.cs @@ -1,86 +1,86 @@ -using NUnit.Framework; -using System; - -namespace Scalar.Tests.Should -{ - public static class ValueShouldExtensions - { - public static bool ShouldBeTrue(this bool actualValue, string message = "") - { - actualValue.ShouldEqual(true, message); - return actualValue; - } - - public static bool ShouldBeFalse(this bool actualValue, string message = "") - { - actualValue.ShouldEqual(false, message); - return actualValue; - } - - public static T ShouldBeAtLeast(this T actualValue, T expectedValue, string message = "") where T : IComparable - { - Assert.GreaterOrEqual(actualValue, expectedValue, message); - return actualValue; - } - - public static T ShouldBeAtMost(this T actualValue, T expectedValue, string message = "") where T : IComparable - { - Assert.LessOrEqual(actualValue, expectedValue, message); - return actualValue; - } - - public static T ShouldEqual(this T actualValue, T expectedValue, string message = "") - { - Assert.AreEqual(expectedValue, actualValue, message); - return actualValue; - } - - public static T[] ShouldEqual(this T[] actualValue, T[] expectedValue, int start, int count) - { - expectedValue.Length.ShouldBeAtLeast(start + count); - for (int i = 0; i < count; ++i) - { - actualValue[i].ShouldEqual(expectedValue[i + start]); - } - - return actualValue; - } - - public static T ShouldNotEqual(this T actualValue, T unexpectedValue, string message = "") - { - Assert.AreNotEqual(unexpectedValue, actualValue, message); - return actualValue; - } - - public static T ShouldBeSameAs(this T actualValue, T expectedValue, string message = "") - { - Assert.AreSame(expectedValue, actualValue, message); - return actualValue; - } - - public static T ShouldNotBeSameAs(this T actualValue, T expectedValue, string message = "") - { - Assert.AreNotSame(expectedValue, actualValue, message); - return actualValue; - } - - public static T ShouldBeOfType(this object obj) - { - Assert.IsTrue(obj is T, "Expected type {0}, but the object is actually of type {1}", typeof(T), obj.GetType()); - return (T)obj; - } - - public static void ShouldBeNull(this T obj, string message = "") - where T : class - { - Assert.IsNull(obj, message); - } - - public static T ShouldNotBeNull(this T obj, string message = "") - where T : class - { - Assert.IsNotNull(obj, message); - return obj; - } - } -} +using NUnit.Framework; +using System; + +namespace Scalar.Tests.Should +{ + public static class ValueShouldExtensions + { + public static bool ShouldBeTrue(this bool actualValue, string message = "") + { + actualValue.ShouldEqual(true, message); + return actualValue; + } + + public static bool ShouldBeFalse(this bool actualValue, string message = "") + { + actualValue.ShouldEqual(false, message); + return actualValue; + } + + public static T ShouldBeAtLeast(this T actualValue, T expectedValue, string message = "") where T : IComparable + { + Assert.GreaterOrEqual(actualValue, expectedValue, message); + return actualValue; + } + + public static T ShouldBeAtMost(this T actualValue, T expectedValue, string message = "") where T : IComparable + { + Assert.LessOrEqual(actualValue, expectedValue, message); + return actualValue; + } + + public static T ShouldEqual(this T actualValue, T expectedValue, string message = "") + { + Assert.AreEqual(expectedValue, actualValue, message); + return actualValue; + } + + public static T[] ShouldEqual(this T[] actualValue, T[] expectedValue, int start, int count) + { + expectedValue.Length.ShouldBeAtLeast(start + count); + for (int i = 0; i < count; ++i) + { + actualValue[i].ShouldEqual(expectedValue[i + start]); + } + + return actualValue; + } + + public static T ShouldNotEqual(this T actualValue, T unexpectedValue, string message = "") + { + Assert.AreNotEqual(unexpectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldBeSameAs(this T actualValue, T expectedValue, string message = "") + { + Assert.AreSame(expectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldNotBeSameAs(this T actualValue, T expectedValue, string message = "") + { + Assert.AreNotSame(expectedValue, actualValue, message); + return actualValue; + } + + public static T ShouldBeOfType(this object obj) + { + Assert.IsTrue(obj is T, "Expected type {0}, but the object is actually of type {1}", typeof(T), obj.GetType()); + return (T)obj; + } + + public static void ShouldBeNull(this T obj, string message = "") + where T : class + { + Assert.IsNull(obj, message); + } + + public static T ShouldNotBeNull(this T obj, string message = "") + where T : class + { + Assert.IsNotNull(obj, message); + return obj; + } + } +} diff --git a/Scalar.UnitTests.Windows/App.config b/Scalar.UnitTests.Windows/App.config index f7255267a4..d71110415a 100644 --- a/Scalar.UnitTests.Windows/App.config +++ b/Scalar.UnitTests.Windows/App.config @@ -1,22 +1,22 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.UnitTests.Windows/Properties/AssemblyInfo.cs b/Scalar.UnitTests.Windows/Properties/AssemblyInfo.cs index f79fc7e6a2..773508bd03 100644 --- a/Scalar.UnitTests.Windows/Properties/AssemblyInfo.cs +++ b/Scalar.UnitTests.Windows/Properties/AssemblyInfo.cs @@ -1,23 +1,23 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar.UnitTests.Windows")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar.UnitTests.Windows")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("8e0d0989-21f6-4dd8-946c-39f992523cc6")] +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar.UnitTests.Windows")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar.UnitTests.Windows")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8e0d0989-21f6-4dd8-946c-39f992523cc6")] diff --git a/Scalar.UnitTests.Windows/Scalar.UnitTests.Windows.csproj b/Scalar.UnitTests.Windows/Scalar.UnitTests.Windows.csproj index 0cca1ceb7a..4ab6907d9a 100644 --- a/Scalar.UnitTests.Windows/Scalar.UnitTests.Windows.csproj +++ b/Scalar.UnitTests.Windows/Scalar.UnitTests.Windows.csproj @@ -1,198 +1,198 @@ - - - - - - - - {8E0D0989-21F6-4DD8-946C-39F992523CC6} - Exe - Properties - Scalar.UnitTests.Windows - Scalar.UnitTests.Windows - v4.6.1 - 512 - true - - - - - true - DEBUG;TRACE - full - x64 - prompt - true - true - - - TRACE - true - pdbonly - x64 - prompt - true - true - - - - ..\..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll - - - ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll - - - ..\..\packages\Moq.4.10.1\lib\net45\Moq.dll - - - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - True - - - ..\..\packages\NuGet.Common.4.9.2\lib\net46\NuGet.Common.dll - - - ..\..\packages\NuGet.Configuration.4.9.2\lib\net46\NuGet.Configuration.dll - - - ..\..\packages\NuGet.Frameworks.4.9.2\lib\net46\NuGet.Frameworks.dll - - - ..\..\packages\NuGet.Packaging.4.9.2\lib\net46\NuGet.Packaging.dll - - - ..\..\packages\NuGet.Packaging.Core.4.9.2\lib\net46\NuGet.Packaging.Core.dll - - - ..\..\packages\NuGet.Protocol.4.9.2\lib\net46\NuGet.Protocol.dll - - - ..\..\packages\NuGet.Versioning.4.9.2\lib\net46\NuGet.Versioning.dll - - - ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll - - - ..\..\packages\NUnitLite.3.12.0\lib\net45\nunitlite.dll - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll - - - ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll - - - ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll - - - - - - ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - - - - - - ..\..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll - - - - - - - - - - - - - - - - - - - Designer - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - {1dac3da6-3d21-4917-b9a8-d60c8712252a} - Scalar.Platform.Mac - - - {15fae44c-0d21-4312-9fd3-28f05a5ab7a6} - Scalar.Platform.POSIX - - - {4ce404e7-d3fc-471c-993c-64615861ea63} - Scalar.Platform.Windows - - - {03769a07-f216-456b-886b-e07caf6c5e81} - Scalar.Service.Mac - - - {72701bc3-5da9-4c7a-bf10-9e98c9fc8eac} - Scalar.Tests - - - {aecec217-2499-403d-b0bb-2962b9be5970} - Scalar.Upgrader - - - {32220664-594C-4425-B9A0-88E0BE2F3D2A} - Scalar.Windows - - - - - - - - NetCore\%(RecursiveDir)\%(Filename)%(Extension) - - - Data\%(RecursiveDir)\%(Filename)%(Extension) - Always - - - Readme.md - - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - - - - + + + + + + + + {8E0D0989-21F6-4DD8-946C-39F992523CC6} + Exe + Properties + Scalar.UnitTests.Windows + Scalar.UnitTests.Windows + v4.6.1 + 512 + true + + + + + true + DEBUG;TRACE + full + x64 + prompt + true + true + + + TRACE + true + pdbonly + x64 + prompt + true + true + + + + ..\..\packages\Castle.Core.4.3.1\lib\net45\Castle.Core.dll + + + ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll + + + ..\..\packages\Moq.4.10.1\lib\net45\Moq.dll + + + ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\NuGet.Common.4.9.2\lib\net46\NuGet.Common.dll + + + ..\..\packages\NuGet.Configuration.4.9.2\lib\net46\NuGet.Configuration.dll + + + ..\..\packages\NuGet.Frameworks.4.9.2\lib\net46\NuGet.Frameworks.dll + + + ..\..\packages\NuGet.Packaging.4.9.2\lib\net46\NuGet.Packaging.dll + + + ..\..\packages\NuGet.Packaging.Core.4.9.2\lib\net46\NuGet.Packaging.Core.dll + + + ..\..\packages\NuGet.Protocol.4.9.2\lib\net46\NuGet.Protocol.dll + + + ..\..\packages\NuGet.Versioning.4.9.2\lib\net46\NuGet.Versioning.dll + + + ..\..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + ..\..\packages\NUnitLite.3.12.0\lib\net45\nunitlite.dll + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll + + + ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll + + + ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll + + + + + + ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.0\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll + + + + + + + + ..\..\packages\System.Threading.Tasks.Extensions.4.5.1\lib\netstandard2.0\System.Threading.Tasks.Extensions.dll + + + + + + + + + + + + + + + + + + + Designer + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + {1dac3da6-3d21-4917-b9a8-d60c8712252a} + Scalar.Platform.Mac + + + {15fae44c-0d21-4312-9fd3-28f05a5ab7a6} + Scalar.Platform.POSIX + + + {4ce404e7-d3fc-471c-993c-64615861ea63} + Scalar.Platform.Windows + + + {03769a07-f216-456b-886b-e07caf6c5e81} + Scalar.Service.Mac + + + {72701bc3-5da9-4c7a-bf10-9e98c9fc8eac} + Scalar.Tests + + + {aecec217-2499-403d-b0bb-2962b9be5970} + Scalar.Upgrader + + + {32220664-594C-4425-B9A0-88E0BE2F3D2A} + Scalar.Windows + + + + + + + + NetCore\%(RecursiveDir)\%(Filename)%(Extension) + + + Data\%(RecursiveDir)\%(Filename)%(Extension) + Always + + + Readme.md + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + + + + diff --git a/Scalar.UnitTests.Windows/Windows/Mock/MockProcessLauncher.cs b/Scalar.UnitTests.Windows/Windows/Mock/MockProcessLauncher.cs index 0de5c8b3d3..32bf557e81 100644 --- a/Scalar.UnitTests.Windows/Windows/Mock/MockProcessLauncher.cs +++ b/Scalar.UnitTests.Windows/Windows/Mock/MockProcessLauncher.cs @@ -1,44 +1,44 @@ -using System; - -namespace Scalar.UnitTests.Windows.Upgrader -{ - public class MockProcessLauncher : Scalar.CommandLine.UpgradeVerb.ProcessLauncher - { - private int exitCode; - private bool hasExited; - private bool startResult; - - public MockProcessLauncher( - int exitCode, - bool hasExited, - bool startResult) - { - this.exitCode = exitCode; - this.hasExited = hasExited; - this.startResult = startResult; - } - - public bool IsLaunched { get; private set; } - - public string LaunchPath { get; private set; } - - public override bool HasExited - { - get { return this.hasExited; } - } - - public override int ExitCode - { - get { return this.exitCode; } - } - - public override bool TryStart(string path, string args, bool useShellExecute, out Exception exception) - { - this.LaunchPath = path; - this.IsLaunched = true; - - exception = null; - return this.startResult; - } - } -} +using System; + +namespace Scalar.UnitTests.Windows.Upgrader +{ + public class MockProcessLauncher : Scalar.CommandLine.UpgradeVerb.ProcessLauncher + { + private int exitCode; + private bool hasExited; + private bool startResult; + + public MockProcessLauncher( + int exitCode, + bool hasExited, + bool startResult) + { + this.exitCode = exitCode; + this.hasExited = hasExited; + this.startResult = startResult; + } + + public bool IsLaunched { get; private set; } + + public string LaunchPath { get; private set; } + + public override bool HasExited + { + get { return this.hasExited; } + } + + public override int ExitCode + { + get { return this.exitCode; } + } + + public override bool TryStart(string path, string args, bool useShellExecute, out Exception exception) + { + this.LaunchPath = path; + this.IsLaunched = true; + + exception = null; + return this.startResult; + } + } +} diff --git a/Scalar.UnitTests.Windows/Windows/Upgrader/UpgradeVerbTests.cs b/Scalar.UnitTests.Windows/Windows/Upgrader/UpgradeVerbTests.cs index fd3ebba230..6ba059ff74 100644 --- a/Scalar.UnitTests.Windows/Windows/Upgrader/UpgradeVerbTests.cs +++ b/Scalar.UnitTests.Windows/Windows/Upgrader/UpgradeVerbTests.cs @@ -1,234 +1,234 @@ -using NUnit.Framework; -using Scalar.CommandLine; -using Scalar.Common; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Upgrader; -using Scalar.UnitTests.Upgrader; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Windows.Upgrader -{ - [TestFixture] - public class UpgradeVerbTests : UpgradeTests - { - private MockProcessLauncher processLauncher; - private UpgradeVerb upgradeVerb; - - [SetUp] - public override void Setup() - { - base.Setup(); - - this.processLauncher = new MockProcessLauncher(exitCode: 0, hasExited: true, startResult: true); - this.upgradeVerb = new UpgradeVerb( - this.Upgrader, - this.Tracer, - this.FileSystem, - this.PrerunChecker, - this.processLauncher, - this.Output); - this.upgradeVerb.Confirmed = false; - this.PrerunChecker.SetCommandToRerun("`scalar upgrade`"); - } - - [TestCase] - public void UpgradeAvailabilityReporting() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("Slow"); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - upgradeVersion: NewerThanLocalVersion, - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "New Scalar version " + NewerThanLocalVersion + " available in ring Slow", - "When ready, run `scalar upgrade --confirm` from an elevated command prompt." - }, - expectedErrors: null); - } - - [TestCase] - public void DowngradePrevention() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("Slow"); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - upgradeVersion: OlderThanLocalVersion, - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "Checking for Scalar upgrades...Succeeded", - "Great news, you're all caught up on upgrades in the Slow ring!" - }, - expectedErrors: null); - } - - [TestCase] - public void LaunchInstaller() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("Slow"); - this.upgradeVerb.Confirmed = true; - this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "New Scalar version " + NewerThanLocalVersion + " available in ring Slow", - "Launching upgrade tool..." - }, - expectedErrors: null); - - this.processLauncher.IsLaunched.ShouldBeTrue(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public override void NoneLocalRing() - { - base.NoneLocalRing(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public override void InvalidUpgradeRing() - { - base.InvalidUpgradeRing(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void CopyTools() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("Slow"); - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.CopyTools); - this.upgradeVerb.Confirmed = true; - this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Could not launch upgrade tool. Unable to copy upgrader tools" - }, - expectedErrors: new List - { - "Could not launch upgrade tool. Unable to copy upgrader tools" - }); - } - - [TestCase] - public void IsScalarServiceRunningPreCheck() - { - this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); - this.ConfigureRunAndVerify( - configure: () => - { - this.upgradeVerb.Confirmed = true; - this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsServiceInstalledAndNotRunning); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Scalar Service is not running.", - "Run `sc start Scalar.Service` and run `scalar upgrade --confirm` again from an elevated command prompt." - }, - expectedErrors: null, - expectedWarnings: new List - { - "Scalar Service is not running." - }); - } - - [TestCase] - public void ElevatedRunPreCheck() - { - this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); - this.ConfigureRunAndVerify( - configure: () => - { - this.upgradeVerb.Confirmed = true; - this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsElevated); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "The installer needs to be run from an elevated command prompt.", - "Run `scalar upgrade --confirm` again from an elevated command prompt." - }, - expectedErrors: null, - expectedWarnings: new List - { - "The installer needs to be run from an elevated command prompt." - }); - } - - [TestCase] - public void UnAttendedModePreCheck() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.upgradeVerb.Confirmed = true; - this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnattendedMode); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "`scalar upgrade` is not supported in unattended mode" - }, - expectedErrors: null, - expectedWarnings: new List - { - "`scalar upgrade` is not supported in unattended mode" - }); - } - - [TestCase] - public void DryRunLaunchesUpgradeTool() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.upgradeVerb.DryRun = true; - this.SetUpgradeRing("Slow"); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - upgradeVersion: NewerThanLocalVersion, - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "Installer launched in a new window." - }, - expectedErrors: null); - } - - protected override ReturnCode RunUpgrade() - { - try - { - this.upgradeVerb.Execute(); - } - catch (ScalarVerb.VerbAbortedException) - { - // ignore. exceptions are expected while simulating some failures. - } - - return this.upgradeVerb.ReturnCode; - } - } +using NUnit.Framework; +using Scalar.CommandLine; +using Scalar.Common; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Upgrader; +using Scalar.UnitTests.Upgrader; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Windows.Upgrader +{ + [TestFixture] + public class UpgradeVerbTests : UpgradeTests + { + private MockProcessLauncher processLauncher; + private UpgradeVerb upgradeVerb; + + [SetUp] + public override void Setup() + { + base.Setup(); + + this.processLauncher = new MockProcessLauncher(exitCode: 0, hasExited: true, startResult: true); + this.upgradeVerb = new UpgradeVerb( + this.Upgrader, + this.Tracer, + this.FileSystem, + this.PrerunChecker, + this.processLauncher, + this.Output); + this.upgradeVerb.Confirmed = false; + this.PrerunChecker.SetCommandToRerun("`scalar upgrade`"); + } + + [TestCase] + public void UpgradeAvailabilityReporting() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("Slow"); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + upgradeVersion: NewerThanLocalVersion, + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "New Scalar version " + NewerThanLocalVersion + " available in ring Slow", + "When ready, run `scalar upgrade --confirm` from an elevated command prompt." + }, + expectedErrors: null); + } + + [TestCase] + public void DowngradePrevention() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("Slow"); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + upgradeVersion: OlderThanLocalVersion, + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "Checking for Scalar upgrades...Succeeded", + "Great news, you're all caught up on upgrades in the Slow ring!" + }, + expectedErrors: null); + } + + [TestCase] + public void LaunchInstaller() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("Slow"); + this.upgradeVerb.Confirmed = true; + this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "New Scalar version " + NewerThanLocalVersion + " available in ring Slow", + "Launching upgrade tool..." + }, + expectedErrors: null); + + this.processLauncher.IsLaunched.ShouldBeTrue(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public override void NoneLocalRing() + { + base.NoneLocalRing(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public override void InvalidUpgradeRing() + { + base.InvalidUpgradeRing(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void CopyTools() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("Slow"); + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.CopyTools); + this.upgradeVerb.Confirmed = true; + this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Could not launch upgrade tool. Unable to copy upgrader tools" + }, + expectedErrors: new List + { + "Could not launch upgrade tool. Unable to copy upgrader tools" + }); + } + + [TestCase] + public void IsScalarServiceRunningPreCheck() + { + this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); + this.ConfigureRunAndVerify( + configure: () => + { + this.upgradeVerb.Confirmed = true; + this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsServiceInstalledAndNotRunning); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Scalar Service is not running.", + "Run `sc start Scalar.Service` and run `scalar upgrade --confirm` again from an elevated command prompt." + }, + expectedErrors: null, + expectedWarnings: new List + { + "Scalar Service is not running." + }); + } + + [TestCase] + public void ElevatedRunPreCheck() + { + this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); + this.ConfigureRunAndVerify( + configure: () => + { + this.upgradeVerb.Confirmed = true; + this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsElevated); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "The installer needs to be run from an elevated command prompt.", + "Run `scalar upgrade --confirm` again from an elevated command prompt." + }, + expectedErrors: null, + expectedWarnings: new List + { + "The installer needs to be run from an elevated command prompt." + }); + } + + [TestCase] + public void UnAttendedModePreCheck() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.upgradeVerb.Confirmed = true; + this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnattendedMode); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "`scalar upgrade` is not supported in unattended mode" + }, + expectedErrors: null, + expectedWarnings: new List + { + "`scalar upgrade` is not supported in unattended mode" + }); + } + + [TestCase] + public void DryRunLaunchesUpgradeTool() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.upgradeVerb.DryRun = true; + this.SetUpgradeRing("Slow"); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + upgradeVersion: NewerThanLocalVersion, + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "Installer launched in a new window." + }, + expectedErrors: null); + } + + protected override ReturnCode RunUpgrade() + { + try + { + this.upgradeVerb.Execute(); + } + catch (ScalarVerb.VerbAbortedException) + { + // ignore. exceptions are expected while simulating some failures. + } + + return this.upgradeVerb.ReturnCode; + } + } } diff --git a/Scalar.UnitTests.Windows/Windows/Upgrader/WindowsNuGetUpgraderTests.cs b/Scalar.UnitTests.Windows/Windows/Upgrader/WindowsNuGetUpgraderTests.cs index b0a775f9e2..bd1b80422b 100644 --- a/Scalar.UnitTests.Windows/Windows/Upgrader/WindowsNuGetUpgraderTests.cs +++ b/Scalar.UnitTests.Windows/Windows/Upgrader/WindowsNuGetUpgraderTests.cs @@ -1,60 +1,60 @@ -using Moq; -using NuGet.Packaging.Core; -using NuGet.Protocol.Core.Types; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Platform.Windows; -using Scalar.Tests.Should; -using Scalar.UnitTests.Common.NuGetUpgrade; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.UnitTests.Windows.Common.Upgrader -{ - [TestFixture] - public class WindowsNuGetUpgraderTests : NuGetUpgraderTests - { - public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformStrategy() - { - return new WindowsProductUpgraderPlatformStrategy(this.mockFileSystem, this.tracer); - } - - [TestCase] - public void TrySetupUpgradeApplicationDirectoryFailsIfCreateToolsDirectoryFails() - { - this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = false; - this.upgrader.TrySetupUpgradeApplicationDirectory(out string _, out string _).ShouldBeFalse(); - this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; - } - - [TestCase] - public void CanDownloadNewestVersionFailsIfDownloadDirectoryCreationFails() - { - Version actualNewestVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); - - bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); - - // Assert that no new version was returned - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); - - this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = false; - bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); - this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; - downloadSuccessful.ShouldBeFalse(); - } - } -} +using Moq; +using NuGet.Packaging.Core; +using NuGet.Protocol.Core.Types; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Platform.Windows; +using Scalar.Tests.Should; +using Scalar.UnitTests.Common.NuGetUpgrade; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.UnitTests.Windows.Common.Upgrader +{ + [TestFixture] + public class WindowsNuGetUpgraderTests : NuGetUpgraderTests + { + public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformStrategy() + { + return new WindowsProductUpgraderPlatformStrategy(this.mockFileSystem, this.tracer); + } + + [TestCase] + public void TrySetupUpgradeApplicationDirectoryFailsIfCreateToolsDirectoryFails() + { + this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = false; + this.upgrader.TrySetupUpgradeApplicationDirectory(out string _, out string _).ShouldBeFalse(); + this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; + } + + [TestCase] + public void CanDownloadNewestVersionFailsIfDownloadDirectoryCreationFails() + { + Version actualNewestVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); + + bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); + + // Assert that no new version was returned + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); + + this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = false; + bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); + this.mockFileSystem.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; + downloadSuccessful.ShouldBeFalse(); + } + } +} diff --git a/Scalar.UnitTests.Windows/Windows/WindowsFileBasedLockTests.cs b/Scalar.UnitTests.Windows/Windows/WindowsFileBasedLockTests.cs index 96c980aea3..545ff8ac15 100644 --- a/Scalar.UnitTests.Windows/Windows/WindowsFileBasedLockTests.cs +++ b/Scalar.UnitTests.Windows/Windows/WindowsFileBasedLockTests.cs @@ -1,59 +1,59 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Platform.Windows; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using System; -using System.IO; - -namespace Scalar.UnitTests.Windows -{ - [TestFixture] - public class WindowsFileBasedLockTests - { - [TestCase] - public void CreateLockWhenDirectoryMissing() - { - string parentPath = Path.Combine("mock:", "path", "to"); - string lockPath = Path.Combine(parentPath, "lock"); - MockTracer tracer = new MockTracer(); - FileBasedLockFileSystem fs = new FileBasedLockFileSystem(); - FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath); - - fileBasedLock.TryAcquireLock().ShouldBeTrue(); - fs.CreateDirectoryPath.ShouldNotBeNull(); - fs.CreateDirectoryPath.ShouldEqual(parentPath); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void AttemptToAcquireLockWhenAlreadyLocked() - { - string parentPath = Path.Combine("mock:", "path", "to"); - string lockPath = Path.Combine(parentPath, "lock"); - MockTracer tracer = new MockTracer(); - FileBasedLockFileSystem fs = new FileBasedLockFileSystem(); - FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath); - - fileBasedLock.TryAcquireLock().ShouldBeTrue(); - Assert.Throws(() => fileBasedLock.TryAcquireLock()); - } - - private class FileBasedLockFileSystem : ConfigurableFileSystem - { - public string CreateDirectoryPath { get; set; } - - public override void CreateDirectory(string path) - { - this.CreateDirectoryPath = path; - } - - public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) - { - return new MemoryStream(); - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Platform.Windows; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using System; +using System.IO; + +namespace Scalar.UnitTests.Windows +{ + [TestFixture] + public class WindowsFileBasedLockTests + { + [TestCase] + public void CreateLockWhenDirectoryMissing() + { + string parentPath = Path.Combine("mock:", "path", "to"); + string lockPath = Path.Combine(parentPath, "lock"); + MockTracer tracer = new MockTracer(); + FileBasedLockFileSystem fs = new FileBasedLockFileSystem(); + FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath); + + fileBasedLock.TryAcquireLock().ShouldBeTrue(); + fs.CreateDirectoryPath.ShouldNotBeNull(); + fs.CreateDirectoryPath.ShouldEqual(parentPath); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void AttemptToAcquireLockWhenAlreadyLocked() + { + string parentPath = Path.Combine("mock:", "path", "to"); + string lockPath = Path.Combine(parentPath, "lock"); + MockTracer tracer = new MockTracer(); + FileBasedLockFileSystem fs = new FileBasedLockFileSystem(); + FileBasedLock fileBasedLock = new WindowsFileBasedLock(fs, tracer, lockPath); + + fileBasedLock.TryAcquireLock().ShouldBeTrue(); + Assert.Throws(() => fileBasedLock.TryAcquireLock()); + } + + private class FileBasedLockFileSystem : ConfigurableFileSystem + { + public string CreateDirectoryPath { get; set; } + + public override void CreateDirectory(string path) + { + this.CreateDirectoryPath = path; + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) + { + return new MemoryStream(); + } + } + } +} diff --git a/Scalar.UnitTests.Windows/packages.config b/Scalar.UnitTests.Windows/packages.config index 019b84b803..a8790266f6 100644 --- a/Scalar.UnitTests.Windows/packages.config +++ b/Scalar.UnitTests.Windows/packages.config @@ -1,25 +1,25 @@ - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scalar.UnitTests/Category/CategoryConstants.cs b/Scalar.UnitTests/Category/CategoryConstants.cs index 35b872a32c..2c9c3d43ad 100644 --- a/Scalar.UnitTests/Category/CategoryConstants.cs +++ b/Scalar.UnitTests/Category/CategoryConstants.cs @@ -1,7 +1,7 @@ -namespace Scalar.UnitTests.Category -{ - public static class CategoryConstants - { - public const string ExceptionExpected = "ExceptionExpected"; - } -} +namespace Scalar.UnitTests.Category +{ + public static class CategoryConstants + { + public const string ExceptionExpected = "ExceptionExpected"; + } +} diff --git a/Scalar.UnitTests/Common/CacheServerResolverTests.cs b/Scalar.UnitTests/Common/CacheServerResolverTests.cs index b8bc8481d3..64eeb6561d 100644 --- a/Scalar.UnitTests/Common/CacheServerResolverTests.cs +++ b/Scalar.UnitTests/Common/CacheServerResolverTests.cs @@ -1,229 +1,229 @@ -using Newtonsoft.Json; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class CacheServerResolverTests - { - private const string CacheServerUrl = "https://cache/server"; - private const string CacheServerName = "TestCacheServer"; - - [TestCase] - public void CanGetCacheServerFromNewConfig() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(CacheServerUrl); - CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); - - cacheServer.Url.ShouldEqual(CacheServerUrl); - CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl); - } - - [TestCase] - public void CanGetCacheServerFromOldConfig() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(null, CacheServerUrl); - CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); - - cacheServer.Url.ShouldEqual(CacheServerUrl); - CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl); - } - - [TestCase] - public void CanGetCacheServerWithNoConfig() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(); - - this.ValidateIsNone(enlistment, CacheServerResolver.GetCacheServerFromConfig(enlistment)); - CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(enlistment.RepoUrl); - } - - [TestCase] - public void CanResolveUrlForKnownName() - { - CacheServerResolver resolver = this.CreateResolver(); - - CacheServerInfo resolvedCacheServer; - string error; - resolver.TryResolveUrlFromRemote(CacheServerName, this.CreateScalarConfig(), out resolvedCacheServer, out error); - - resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); - resolvedCacheServer.Name.ShouldEqual(CacheServerName); - } - - [TestCase] - public void CanResolveNameFromKnownUrl() - { - CacheServerResolver resolver = this.CreateResolver(); - CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CacheServerUrl, this.CreateScalarConfig()); - - resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); - resolvedCacheServer.Name.ShouldEqual(CacheServerName); - } - - [TestCase] - public void CanResolveNameFromCustomUrl() - { - const string CustomUrl = "https://not/a/known/cache/server"; - - CacheServerResolver resolver = this.CreateResolver(); - CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CustomUrl, this.CreateScalarConfig()); - - resolvedCacheServer.Url.ShouldEqual(CustomUrl); - resolvedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined); - } - - [TestCase] - public void CanResolveUrlAsRepoUrl() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(); - CacheServerResolver resolver = this.CreateResolver(enlistment); - - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl, this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "/", this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "//", this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper(), this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper() + "/", this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower(), this.CreateScalarConfig())); - this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower() + "/", this.CreateScalarConfig())); - } - - [TestCase] - public void CanParseUrl() - { - CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment()); - CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerUrl); - - parsedCacheServer.Url.ShouldEqual(CacheServerUrl); - parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined); - } - - [TestCase] - public void CanParseName() - { - CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment()); - CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerName); - - parsedCacheServer.Url.ShouldEqual(null); - parsedCacheServer.Name.ShouldEqual(CacheServerName); - } - - [TestCase] - public void CanParseAndResolveDefault() - { - CacheServerResolver resolver = this.CreateResolver(); - - CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(null); - parsedCacheServer.Url.ShouldEqual(null); - parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.Default); - - CacheServerInfo resolvedCacheServer; - string error; - resolver.TryResolveUrlFromRemote(parsedCacheServer.Name, this.CreateScalarConfig(), out resolvedCacheServer, out error); - - resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); - resolvedCacheServer.Name.ShouldEqual(CacheServerName); - } - - [TestCase] - public void CanParseAndResolveNoCacheServer() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(); - CacheServerResolver resolver = this.CreateResolver(enlistment); - - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(CacheServerInfo.ReservedNames.None)); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl)); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl)); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "/")); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "//")); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper())); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper() + "/")); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower())); - this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower() + "/")); - - CacheServerInfo resolvedCacheServer; - string error; - resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateScalarConfig(), out resolvedCacheServer, out error) - .ShouldEqual(false, "Should not succeed in resolving the name 'None'"); - - resolvedCacheServer.ShouldEqual(null); - error.ShouldNotBeNull(); - } - - [TestCase] - public void CanParseAndResolveDefaultWhenServerAdvertisesNullListOfCacheServers() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(); - CacheServerResolver resolver = this.CreateResolver(enlistment); - - CacheServerInfo resolvedCacheServer; - string error; - resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.Default, this.CreateDefaultDeserializedScalarConfig(), out resolvedCacheServer, out error) - .ShouldEqual(true); - - this.ValidateIsNone(enlistment, resolvedCacheServer); - } - - [TestCase] - public void CanParseAndResolveOtherWhenServerAdvertisesNullListOfCacheServers() - { - MockScalarEnlistment enlistment = this.CreateEnlistment(); - CacheServerResolver resolver = this.CreateResolver(enlistment); - - CacheServerInfo resolvedCacheServer; - string error; - resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateDefaultDeserializedScalarConfig(), out resolvedCacheServer, out error) - .ShouldEqual(false, "Should not succeed in resolving the name 'None'"); - - resolvedCacheServer.ShouldEqual(null); - error.ShouldNotBeNull(); - } - - private void ValidateIsNone(Enlistment enlistment, CacheServerInfo cacheServer) - { - cacheServer.Url.ShouldEqual(enlistment.RepoUrl); - cacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.None); - } - - private MockScalarEnlistment CreateEnlistment(string newConfigValue = null, string oldConfigValue = null) - { - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult( - "config --local scalar.cache-server", - () => new GitProcess.Result(newConfigValue ?? string.Empty, string.Empty, newConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode)); - gitProcess.SetExpectedCommandResult( - "config scalar.mock:..repourl.cache-server-url", - () => new GitProcess.Result(oldConfigValue ?? string.Empty, string.Empty, oldConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode)); - - return new MockScalarEnlistment(gitProcess); - } - - private ServerScalarConfig CreateScalarConfig() - { - return new ServerScalarConfig - { - CacheServers = new[] - { - new CacheServerInfo(CacheServerUrl, CacheServerName, globalDefault: true), - } - }; - } - - private ServerScalarConfig CreateDefaultDeserializedScalarConfig() - { - return JsonConvert.DeserializeObject("{}"); - } - - private CacheServerResolver CreateResolver(MockScalarEnlistment enlistment = null) - { - enlistment = enlistment ?? this.CreateEnlistment(); - return new CacheServerResolver(new MockTracer(), enlistment); - } - } -} +using Newtonsoft.Json; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class CacheServerResolverTests + { + private const string CacheServerUrl = "https://cache/server"; + private const string CacheServerName = "TestCacheServer"; + + [TestCase] + public void CanGetCacheServerFromNewConfig() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(CacheServerUrl); + CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + + cacheServer.Url.ShouldEqual(CacheServerUrl); + CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl); + } + + [TestCase] + public void CanGetCacheServerFromOldConfig() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(null, CacheServerUrl); + CacheServerInfo cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + + cacheServer.Url.ShouldEqual(CacheServerUrl); + CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(CacheServerUrl); + } + + [TestCase] + public void CanGetCacheServerWithNoConfig() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(); + + this.ValidateIsNone(enlistment, CacheServerResolver.GetCacheServerFromConfig(enlistment)); + CacheServerResolver.GetUrlFromConfig(enlistment).ShouldEqual(enlistment.RepoUrl); + } + + [TestCase] + public void CanResolveUrlForKnownName() + { + CacheServerResolver resolver = this.CreateResolver(); + + CacheServerInfo resolvedCacheServer; + string error; + resolver.TryResolveUrlFromRemote(CacheServerName, this.CreateScalarConfig(), out resolvedCacheServer, out error); + + resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); + resolvedCacheServer.Name.ShouldEqual(CacheServerName); + } + + [TestCase] + public void CanResolveNameFromKnownUrl() + { + CacheServerResolver resolver = this.CreateResolver(); + CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CacheServerUrl, this.CreateScalarConfig()); + + resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); + resolvedCacheServer.Name.ShouldEqual(CacheServerName); + } + + [TestCase] + public void CanResolveNameFromCustomUrl() + { + const string CustomUrl = "https://not/a/known/cache/server"; + + CacheServerResolver resolver = this.CreateResolver(); + CacheServerInfo resolvedCacheServer = resolver.ResolveNameFromRemote(CustomUrl, this.CreateScalarConfig()); + + resolvedCacheServer.Url.ShouldEqual(CustomUrl); + resolvedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined); + } + + [TestCase] + public void CanResolveUrlAsRepoUrl() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(); + CacheServerResolver resolver = this.CreateResolver(enlistment); + + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl, this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "/", this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl + "//", this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper(), this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToUpper() + "/", this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower(), this.CreateScalarConfig())); + this.ValidateIsNone(enlistment, resolver.ResolveNameFromRemote(enlistment.RepoUrl.ToLower() + "/", this.CreateScalarConfig())); + } + + [TestCase] + public void CanParseUrl() + { + CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment()); + CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerUrl); + + parsedCacheServer.Url.ShouldEqual(CacheServerUrl); + parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.UserDefined); + } + + [TestCase] + public void CanParseName() + { + CacheServerResolver resolver = new CacheServerResolver(new MockTracer(), this.CreateEnlistment()); + CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(CacheServerName); + + parsedCacheServer.Url.ShouldEqual(null); + parsedCacheServer.Name.ShouldEqual(CacheServerName); + } + + [TestCase] + public void CanParseAndResolveDefault() + { + CacheServerResolver resolver = this.CreateResolver(); + + CacheServerInfo parsedCacheServer = resolver.ParseUrlOrFriendlyName(null); + parsedCacheServer.Url.ShouldEqual(null); + parsedCacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.Default); + + CacheServerInfo resolvedCacheServer; + string error; + resolver.TryResolveUrlFromRemote(parsedCacheServer.Name, this.CreateScalarConfig(), out resolvedCacheServer, out error); + + resolvedCacheServer.Url.ShouldEqual(CacheServerUrl); + resolvedCacheServer.Name.ShouldEqual(CacheServerName); + } + + [TestCase] + public void CanParseAndResolveNoCacheServer() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(); + CacheServerResolver resolver = this.CreateResolver(enlistment); + + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(CacheServerInfo.ReservedNames.None)); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl)); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl)); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "/")); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl + "//")); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper())); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToUpper() + "/")); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower())); + this.ValidateIsNone(enlistment, resolver.ParseUrlOrFriendlyName(enlistment.RepoUrl.ToLower() + "/")); + + CacheServerInfo resolvedCacheServer; + string error; + resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateScalarConfig(), out resolvedCacheServer, out error) + .ShouldEqual(false, "Should not succeed in resolving the name 'None'"); + + resolvedCacheServer.ShouldEqual(null); + error.ShouldNotBeNull(); + } + + [TestCase] + public void CanParseAndResolveDefaultWhenServerAdvertisesNullListOfCacheServers() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(); + CacheServerResolver resolver = this.CreateResolver(enlistment); + + CacheServerInfo resolvedCacheServer; + string error; + resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.Default, this.CreateDefaultDeserializedScalarConfig(), out resolvedCacheServer, out error) + .ShouldEqual(true); + + this.ValidateIsNone(enlistment, resolvedCacheServer); + } + + [TestCase] + public void CanParseAndResolveOtherWhenServerAdvertisesNullListOfCacheServers() + { + MockScalarEnlistment enlistment = this.CreateEnlistment(); + CacheServerResolver resolver = this.CreateResolver(enlistment); + + CacheServerInfo resolvedCacheServer; + string error; + resolver.TryResolveUrlFromRemote(CacheServerInfo.ReservedNames.None, this.CreateDefaultDeserializedScalarConfig(), out resolvedCacheServer, out error) + .ShouldEqual(false, "Should not succeed in resolving the name 'None'"); + + resolvedCacheServer.ShouldEqual(null); + error.ShouldNotBeNull(); + } + + private void ValidateIsNone(Enlistment enlistment, CacheServerInfo cacheServer) + { + cacheServer.Url.ShouldEqual(enlistment.RepoUrl); + cacheServer.Name.ShouldEqual(CacheServerInfo.ReservedNames.None); + } + + private MockScalarEnlistment CreateEnlistment(string newConfigValue = null, string oldConfigValue = null) + { + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult( + "config --local scalar.cache-server", + () => new GitProcess.Result(newConfigValue ?? string.Empty, string.Empty, newConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode)); + gitProcess.SetExpectedCommandResult( + "config scalar.mock:..repourl.cache-server-url", + () => new GitProcess.Result(oldConfigValue ?? string.Empty, string.Empty, oldConfigValue != null ? GitProcess.Result.SuccessCode : GitProcess.Result.GenericFailureCode)); + + return new MockScalarEnlistment(gitProcess); + } + + private ServerScalarConfig CreateScalarConfig() + { + return new ServerScalarConfig + { + CacheServers = new[] + { + new CacheServerInfo(CacheServerUrl, CacheServerName, globalDefault: true), + } + }; + } + + private ServerScalarConfig CreateDefaultDeserializedScalarConfig() + { + return JsonConvert.DeserializeObject("{}"); + } + + private CacheServerResolver CreateResolver(MockScalarEnlistment enlistment = null) + { + enlistment = enlistment ?? this.CreateEnlistment(); + return new CacheServerResolver(new MockTracer(), enlistment); + } + } +} diff --git a/Scalar.UnitTests/Common/EpochConverterTests.cs b/Scalar.UnitTests/Common/EpochConverterTests.cs index be5fa4c9bb..9969e69e51 100644 --- a/Scalar.UnitTests/Common/EpochConverterTests.cs +++ b/Scalar.UnitTests/Common/EpochConverterTests.cs @@ -1,55 +1,55 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class EpochConverterTests - { - [TestCase] - public void DateToEpochToDate() - { - DateTime time = new DateTime(2018, 12, 18, 8, 12, 13, DateTimeKind.Utc); - DateTime converted = EpochConverter.FromUnixEpochSeconds(EpochConverter.ToUnixEpochSeconds(time)); - - time.ShouldEqual(converted); - } - - [TestCase] - public void EpochToDateToEpoch() - { - long time = 15237623489; - long converted = EpochConverter.ToUnixEpochSeconds(EpochConverter.FromUnixEpochSeconds(time)); - - time.ShouldEqual(converted); - } - - [TestCase] - public void FixedDates() - { - DateTime[] times = new DateTime[] - { - new DateTime(2018, 12, 13, 20, 53, 30, DateTimeKind.Utc), - new DateTime(2035, 1, 3, 5, 0, 59, DateTimeKind.Utc), - new DateTime(1989, 12, 31, 23, 59, 59, DateTimeKind.Utc) - }; - long[] epochs = new long[] - { - 1544734410, - 2051413259, - 631151999 - }; - - for (int i = 0; i < times.Length; i++) - { - long epoch = EpochConverter.ToUnixEpochSeconds(times[i]); - epoch.ShouldEqual(epochs[i]); - - DateTime time = EpochConverter.FromUnixEpochSeconds(epochs[i]); - time.ShouldEqual(times[i]); - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class EpochConverterTests + { + [TestCase] + public void DateToEpochToDate() + { + DateTime time = new DateTime(2018, 12, 18, 8, 12, 13, DateTimeKind.Utc); + DateTime converted = EpochConverter.FromUnixEpochSeconds(EpochConverter.ToUnixEpochSeconds(time)); + + time.ShouldEqual(converted); + } + + [TestCase] + public void EpochToDateToEpoch() + { + long time = 15237623489; + long converted = EpochConverter.ToUnixEpochSeconds(EpochConverter.FromUnixEpochSeconds(time)); + + time.ShouldEqual(converted); + } + + [TestCase] + public void FixedDates() + { + DateTime[] times = new DateTime[] + { + new DateTime(2018, 12, 13, 20, 53, 30, DateTimeKind.Utc), + new DateTime(2035, 1, 3, 5, 0, 59, DateTimeKind.Utc), + new DateTime(1989, 12, 31, 23, 59, 59, DateTimeKind.Utc) + }; + long[] epochs = new long[] + { + 1544734410, + 2051413259, + 631151999 + }; + + for (int i = 0; i < times.Length; i++) + { + long epoch = EpochConverter.ToUnixEpochSeconds(times[i]); + epoch.ShouldEqual(epochs[i]); + + DateTime time = EpochConverter.FromUnixEpochSeconds(epochs[i]); + time.ShouldEqual(times[i]); + } + } + } +} diff --git a/Scalar.UnitTests/Common/FileBasedDictionaryTests.cs b/Scalar.UnitTests/Common/FileBasedDictionaryTests.cs index da78b8b601..d44bb957a3 100644 --- a/Scalar.UnitTests/Common/FileBasedDictionaryTests.cs +++ b/Scalar.UnitTests/Common/FileBasedDictionaryTests.cs @@ -1,428 +1,428 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock; -using Scalar.UnitTests.Mock.FileSystem; -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.IO; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class FileBasedDictionaryTests - { - private const string MockEntryFileName = "mock:\\entries.dat"; - - private const string TestKey = "akey"; - private const string TestValue = "avalue"; - private const string UpdatedTestValue = "avalue2"; - - private const string TestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue\"}\r\n"; - private const string UpdatedTestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue2\"}\r\n"; - - private const string TestKey2 = "bkey"; - private const string TestValue2 = "bvalue"; - private const string UpdatedTestValue2 = "bvalue2"; - - private const string TestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue\"}\r\n"; - private const string UpdatedTestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue2\"}\r\n"; - - [TestCase] - public void ParsesExistingDataCorrectly() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); - - string value; - dut.TryGetValue(TestKey, out value).ShouldEqual(true); - value.ShouldEqual(TestValue); - } - - [TestCase] - public void SetValueAndFlushWritesEntryToDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - public void SetValuesAndFlushWritesEntriesToDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValuesAndFlush( - new[] - { - new KeyValuePair(TestKey, TestValue), - new KeyValuePair(TestKey2, TestValue2), - }); - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 }); - } - - [TestCase] - public void SetValuesAndFlushWritesNewEntryAndUpdatesExistingEntryOnDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - - // Add TestKey to disk - dut.SetValueAndFlush(TestKey, TestValue); - fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry); - - // This call to SetValuesAndFlush should update TestKey and write TestKey2 - dut.SetValuesAndFlush( - new[] - { - new KeyValuePair(TestKey, UpdatedTestValue), - new KeyValuePair(TestKey2, TestValue2), - }); - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, TestEntry2 }); - } - - [TestCase] - public void SetValuesAndFlushWritesUpdatesExistingEntriesOnDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - - dut.SetValuesAndFlush( - new[] - { - new KeyValuePair(TestKey, TestValue), - new KeyValuePair(TestKey2, TestValue2), - }); - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 }); - - dut.SetValuesAndFlush( - new[] - { - new KeyValuePair(TestKey, UpdatedTestValue), - new KeyValuePair(TestKey2, UpdatedTestValue2), - }); - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, UpdatedTestEntry2 }); - } - - [TestCase] - public void SetValuesAndFlushUsesLastValueWhenKeyDuplicated() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - - dut.SetValuesAndFlush( - new[] - { - new KeyValuePair(TestKey, TestValue), - new KeyValuePair(TestKey, UpdatedTestValue), - }); - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry }); - } - - [TestCase] - public void SetValueAndFlushUpdatedEntryOnDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); - dut.SetValueAndFlush(TestKey, UpdatedTestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry }); - } - - [TestCase] - [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] - public void SetValueAndFlushRecoversFromFailedOpenFileStream() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( - openFileStreamFailurePath: MockEntryFileName + ".tmp", - maxOpenFileStreamFailures: 5, - fileExistsFailurePath: null, - maxFileExistsFailures: 0, - maxMoveAndOverwriteFileFailures: 5); - - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - public void SetValueAndFlushRecoversFromDeletedTmp() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( - openFileStreamFailurePath: null, - maxOpenFileStreamFailures: 0, - fileExistsFailurePath: MockEntryFileName + ".tmp", - maxFileExistsFailures: 5, - maxMoveAndOverwriteFileFailures: 0); - - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] - public void SetValueAndFlushRecoversFromFailedOverwrite() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( - openFileStreamFailurePath: null, - maxOpenFileStreamFailures: 0, - fileExistsFailurePath: null, - maxFileExistsFailures: 0, - maxMoveAndOverwriteFileFailures: 5); - - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] - public void SetValueAndFlushRecoversFromDeletedTempAndFailedOverwrite() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( - openFileStreamFailurePath: null, - maxOpenFileStreamFailures: 0, - fileExistsFailurePath: MockEntryFileName + ".tmp", - maxFileExistsFailures: 5, - maxMoveAndOverwriteFileFailures: 5); - - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] - public void SetValueAndFlushRecoversFromMixOfFailures() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(failuresAcrossOpenExistsAndOverwritePath: MockEntryFileName + ".tmp"); - - FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); - dut.SetValueAndFlush(TestKey, TestValue); - - this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); - } - - [TestCase] - public void DeleteFlushesToDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); - dut.RemoveAndFlush(TestKey); - - fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldBeEmpty(); - } - - [TestCase] - public void DeleteUnusedKeyFlushesToDisk() - { - FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); - FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); - dut.RemoveAndFlush("UnusedKey"); - - fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry); - } - - private static FileBasedDictionary CreateFileBasedDictionary(FileBasedDictionaryFileSystem fs, string initialContents) - { - fs.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream(initialContents)); - - fs.ExpectedOpenFileStreams.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty)); - fs.ExpectedOpenFileStreams.Add(MockEntryFileName, fs.ExpectedFiles[MockEntryFileName]); - - string error; - FileBasedDictionary dut; - FileBasedDictionary.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true, error); - dut.ShouldNotBeNull(); - - // FileBasedDictionary should only open a file stream to the non-tmp file when being created. At all other times it should - // write to a tmp file and overwrite the non-tmp file - fs.ExpectedOpenFileStreams.Remove(MockEntryFileName); - - return dut; - } - - private void FileBasedDictionaryFileSystemShouldContain( - FileBasedDictionaryFileSystem fs, - IList expectedEntries) - { - string delimiter = "\r\n"; - string fileContents = fs.ExpectedFiles[MockEntryFileName].ReadAsString(); - fileContents.Substring(fileContents.Length - delimiter.Length).ShouldEqual(delimiter); - - // Remove the trailing delimiter - fileContents = fileContents.Substring(0, fileContents.Length - delimiter.Length); - - string[] fileLines = fileContents.Split(new[] { delimiter }, StringSplitOptions.None); - fileLines.Length.ShouldEqual(expectedEntries.Count); - - foreach (string expectedEntry in expectedEntries) - { - fileLines.ShouldContain(line => line.Equals(expectedEntry.Substring(0, expectedEntry.Length - delimiter.Length))); - } - } - - private class FileBasedDictionaryFileSystem : ConfigurableFileSystem - { - private int openFileStreamFailureCount; - private int maxOpenFileStreamFailures; - private string openFileStreamFailurePath; - - private int fileExistsFailureCount; - private int maxFileExistsFailures; - private string fileExistsFailurePath; - - private int moveAndOverwriteFileFailureCount; - private int maxMoveAndOverwriteFileFailures; - - private string failuresAcrossOpenExistsAndOverwritePath; - private int failuresAcrossOpenExistsAndOverwriteCount; - - public FileBasedDictionaryFileSystem() - { - this.ExpectedOpenFileStreams = new Dictionary(); - } - - public FileBasedDictionaryFileSystem( - string openFileStreamFailurePath, - int maxOpenFileStreamFailures, - string fileExistsFailurePath, - int maxFileExistsFailures, - int maxMoveAndOverwriteFileFailures) - { - this.maxOpenFileStreamFailures = maxOpenFileStreamFailures; - this.openFileStreamFailurePath = openFileStreamFailurePath; - this.fileExistsFailurePath = fileExistsFailurePath; - this.maxFileExistsFailures = maxFileExistsFailures; - this.maxMoveAndOverwriteFileFailures = maxMoveAndOverwriteFileFailures; - this.ExpectedOpenFileStreams = new Dictionary(); - } - - /// - /// Fail a mix of OpenFileStream, FileExists, and Overwrite. - /// - /// - /// Order of failures will be: - /// 1. OpenFileStream - /// 2. FileExists - /// 3. Overwrite - /// - public FileBasedDictionaryFileSystem(string failuresAcrossOpenExistsAndOverwritePath) - { - this.failuresAcrossOpenExistsAndOverwritePath = failuresAcrossOpenExistsAndOverwritePath; - this.ExpectedOpenFileStreams = new Dictionary(); - } - - public Dictionary ExpectedOpenFileStreams { get; } - - public override bool FileExists(string path) - { - if (this.maxFileExistsFailures > 0) - { - if (this.fileExistsFailureCount < this.maxFileExistsFailures && - string.Equals(path, this.fileExistsFailurePath, System.StringComparison.OrdinalIgnoreCase)) - { - if (this.ExpectedFiles.ContainsKey(path)) - { - this.ExpectedFiles.Remove(path); - } - - ++this.fileExistsFailureCount; - } - } - else if (this.failuresAcrossOpenExistsAndOverwritePath != null) - { - if (this.failuresAcrossOpenExistsAndOverwriteCount == 1 && - string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, System.StringComparison.OrdinalIgnoreCase)) - { - if (this.ExpectedFiles.ContainsKey(path)) - { - this.ExpectedFiles.Remove(path); - } - - ++this.failuresAcrossOpenExistsAndOverwriteCount; - } - } - - return this.ExpectedFiles.ContainsKey(path); - } - - public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - if (this.maxMoveAndOverwriteFileFailures > 0) - { - if (this.moveAndOverwriteFileFailureCount < this.maxMoveAndOverwriteFileFailures) - { - ++this.moveAndOverwriteFileFailureCount; - throw new Win32Exception(); - } - } - else if (this.failuresAcrossOpenExistsAndOverwritePath != null) - { - if (this.failuresAcrossOpenExistsAndOverwriteCount == 2) - { - ++this.failuresAcrossOpenExistsAndOverwriteCount; - throw new Win32Exception(); - } - } - - ReusableMemoryStream source; - this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName); - this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename); - - this.ExpectedFiles.Remove(sourceFileName); - this.ExpectedFiles[destinationFilename] = source; - } - - public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) - { - ReusableMemoryStream stream; - this.ExpectedOpenFileStreams.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); - - if (this.maxOpenFileStreamFailures > 0) - { - if (this.openFileStreamFailureCount < this.maxOpenFileStreamFailures && - string.Equals(path, this.openFileStreamFailurePath, System.StringComparison.OrdinalIgnoreCase)) - { - ++this.openFileStreamFailureCount; - - if (this.openFileStreamFailureCount % 2 == 0) - { - throw new IOException(); - } - else - { - throw new UnauthorizedAccessException(); - } - } - } - else if (this.failuresAcrossOpenExistsAndOverwritePath != null) - { - if (this.failuresAcrossOpenExistsAndOverwriteCount == 0 && - string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, System.StringComparison.OrdinalIgnoreCase)) - { - ++this.failuresAcrossOpenExistsAndOverwriteCount; - throw new IOException(); - } - } - - if (fileMode == FileMode.Create) - { - this.ExpectedFiles[path] = new ReusableMemoryStream(string.Empty); - } - - this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); - return stream; - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock; +using Scalar.UnitTests.Mock.FileSystem; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class FileBasedDictionaryTests + { + private const string MockEntryFileName = "mock:\\entries.dat"; + + private const string TestKey = "akey"; + private const string TestValue = "avalue"; + private const string UpdatedTestValue = "avalue2"; + + private const string TestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue\"}\r\n"; + private const string UpdatedTestEntry = "A {\"Key\":\"akey\",\"Value\":\"avalue2\"}\r\n"; + + private const string TestKey2 = "bkey"; + private const string TestValue2 = "bvalue"; + private const string UpdatedTestValue2 = "bvalue2"; + + private const string TestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue\"}\r\n"; + private const string UpdatedTestEntry2 = "A {\"Key\":\"bkey\",\"Value\":\"bvalue2\"}\r\n"; + + [TestCase] + public void ParsesExistingDataCorrectly() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); + + string value; + dut.TryGetValue(TestKey, out value).ShouldEqual(true); + value.ShouldEqual(TestValue); + } + + [TestCase] + public void SetValueAndFlushWritesEntryToDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + public void SetValuesAndFlushWritesEntriesToDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValuesAndFlush( + new[] + { + new KeyValuePair(TestKey, TestValue), + new KeyValuePair(TestKey2, TestValue2), + }); + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 }); + } + + [TestCase] + public void SetValuesAndFlushWritesNewEntryAndUpdatesExistingEntryOnDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + + // Add TestKey to disk + dut.SetValueAndFlush(TestKey, TestValue); + fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry); + + // This call to SetValuesAndFlush should update TestKey and write TestKey2 + dut.SetValuesAndFlush( + new[] + { + new KeyValuePair(TestKey, UpdatedTestValue), + new KeyValuePair(TestKey2, TestValue2), + }); + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, TestEntry2 }); + } + + [TestCase] + public void SetValuesAndFlushWritesUpdatesExistingEntriesOnDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + + dut.SetValuesAndFlush( + new[] + { + new KeyValuePair(TestKey, TestValue), + new KeyValuePair(TestKey2, TestValue2), + }); + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry, TestEntry2 }); + + dut.SetValuesAndFlush( + new[] + { + new KeyValuePair(TestKey, UpdatedTestValue), + new KeyValuePair(TestKey2, UpdatedTestValue2), + }); + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry, UpdatedTestEntry2 }); + } + + [TestCase] + public void SetValuesAndFlushUsesLastValueWhenKeyDuplicated() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + + dut.SetValuesAndFlush( + new[] + { + new KeyValuePair(TestKey, TestValue), + new KeyValuePair(TestKey, UpdatedTestValue), + }); + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry }); + } + + [TestCase] + public void SetValueAndFlushUpdatedEntryOnDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); + dut.SetValueAndFlush(TestKey, UpdatedTestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { UpdatedTestEntry }); + } + + [TestCase] + [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] + public void SetValueAndFlushRecoversFromFailedOpenFileStream() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( + openFileStreamFailurePath: MockEntryFileName + ".tmp", + maxOpenFileStreamFailures: 5, + fileExistsFailurePath: null, + maxFileExistsFailures: 0, + maxMoveAndOverwriteFileFailures: 5); + + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + public void SetValueAndFlushRecoversFromDeletedTmp() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( + openFileStreamFailurePath: null, + maxOpenFileStreamFailures: 0, + fileExistsFailurePath: MockEntryFileName + ".tmp", + maxFileExistsFailures: 5, + maxMoveAndOverwriteFileFailures: 0); + + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] + public void SetValueAndFlushRecoversFromFailedOverwrite() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( + openFileStreamFailurePath: null, + maxOpenFileStreamFailures: 0, + fileExistsFailurePath: null, + maxFileExistsFailures: 0, + maxMoveAndOverwriteFileFailures: 5); + + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] + public void SetValueAndFlushRecoversFromDeletedTempAndFailedOverwrite() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem( + openFileStreamFailurePath: null, + maxOpenFileStreamFailures: 0, + fileExistsFailurePath: MockEntryFileName + ".tmp", + maxFileExistsFailures: 5, + maxMoveAndOverwriteFileFailures: 5); + + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + [NUnit.Framework.Category(CategoryConstants.ExceptionExpected)] + public void SetValueAndFlushRecoversFromMixOfFailures() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(failuresAcrossOpenExistsAndOverwritePath: MockEntryFileName + ".tmp"); + + FileBasedDictionary dut = CreateFileBasedDictionary(fs, string.Empty); + dut.SetValueAndFlush(TestKey, TestValue); + + this.FileBasedDictionaryFileSystemShouldContain(fs, new[] { TestEntry }); + } + + [TestCase] + public void DeleteFlushesToDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); + dut.RemoveAndFlush(TestKey); + + fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldBeEmpty(); + } + + [TestCase] + public void DeleteUnusedKeyFlushesToDisk() + { + FileBasedDictionaryFileSystem fs = new FileBasedDictionaryFileSystem(); + FileBasedDictionary dut = CreateFileBasedDictionary(fs, TestEntry); + dut.RemoveAndFlush("UnusedKey"); + + fs.ExpectedFiles[MockEntryFileName].ReadAsString().ShouldEqual(TestEntry); + } + + private static FileBasedDictionary CreateFileBasedDictionary(FileBasedDictionaryFileSystem fs, string initialContents) + { + fs.ExpectedFiles.Add(MockEntryFileName, new ReusableMemoryStream(initialContents)); + + fs.ExpectedOpenFileStreams.Add(MockEntryFileName + ".tmp", new ReusableMemoryStream(string.Empty)); + fs.ExpectedOpenFileStreams.Add(MockEntryFileName, fs.ExpectedFiles[MockEntryFileName]); + + string error; + FileBasedDictionary dut; + FileBasedDictionary.TryCreate(null, MockEntryFileName, fs, out dut, out error).ShouldEqual(true, error); + dut.ShouldNotBeNull(); + + // FileBasedDictionary should only open a file stream to the non-tmp file when being created. At all other times it should + // write to a tmp file and overwrite the non-tmp file + fs.ExpectedOpenFileStreams.Remove(MockEntryFileName); + + return dut; + } + + private void FileBasedDictionaryFileSystemShouldContain( + FileBasedDictionaryFileSystem fs, + IList expectedEntries) + { + string delimiter = "\r\n"; + string fileContents = fs.ExpectedFiles[MockEntryFileName].ReadAsString(); + fileContents.Substring(fileContents.Length - delimiter.Length).ShouldEqual(delimiter); + + // Remove the trailing delimiter + fileContents = fileContents.Substring(0, fileContents.Length - delimiter.Length); + + string[] fileLines = fileContents.Split(new[] { delimiter }, StringSplitOptions.None); + fileLines.Length.ShouldEqual(expectedEntries.Count); + + foreach (string expectedEntry in expectedEntries) + { + fileLines.ShouldContain(line => line.Equals(expectedEntry.Substring(0, expectedEntry.Length - delimiter.Length))); + } + } + + private class FileBasedDictionaryFileSystem : ConfigurableFileSystem + { + private int openFileStreamFailureCount; + private int maxOpenFileStreamFailures; + private string openFileStreamFailurePath; + + private int fileExistsFailureCount; + private int maxFileExistsFailures; + private string fileExistsFailurePath; + + private int moveAndOverwriteFileFailureCount; + private int maxMoveAndOverwriteFileFailures; + + private string failuresAcrossOpenExistsAndOverwritePath; + private int failuresAcrossOpenExistsAndOverwriteCount; + + public FileBasedDictionaryFileSystem() + { + this.ExpectedOpenFileStreams = new Dictionary(); + } + + public FileBasedDictionaryFileSystem( + string openFileStreamFailurePath, + int maxOpenFileStreamFailures, + string fileExistsFailurePath, + int maxFileExistsFailures, + int maxMoveAndOverwriteFileFailures) + { + this.maxOpenFileStreamFailures = maxOpenFileStreamFailures; + this.openFileStreamFailurePath = openFileStreamFailurePath; + this.fileExistsFailurePath = fileExistsFailurePath; + this.maxFileExistsFailures = maxFileExistsFailures; + this.maxMoveAndOverwriteFileFailures = maxMoveAndOverwriteFileFailures; + this.ExpectedOpenFileStreams = new Dictionary(); + } + + /// + /// Fail a mix of OpenFileStream, FileExists, and Overwrite. + /// + /// + /// Order of failures will be: + /// 1. OpenFileStream + /// 2. FileExists + /// 3. Overwrite + /// + public FileBasedDictionaryFileSystem(string failuresAcrossOpenExistsAndOverwritePath) + { + this.failuresAcrossOpenExistsAndOverwritePath = failuresAcrossOpenExistsAndOverwritePath; + this.ExpectedOpenFileStreams = new Dictionary(); + } + + public Dictionary ExpectedOpenFileStreams { get; } + + public override bool FileExists(string path) + { + if (this.maxFileExistsFailures > 0) + { + if (this.fileExistsFailureCount < this.maxFileExistsFailures && + string.Equals(path, this.fileExistsFailurePath, System.StringComparison.OrdinalIgnoreCase)) + { + if (this.ExpectedFiles.ContainsKey(path)) + { + this.ExpectedFiles.Remove(path); + } + + ++this.fileExistsFailureCount; + } + } + else if (this.failuresAcrossOpenExistsAndOverwritePath != null) + { + if (this.failuresAcrossOpenExistsAndOverwriteCount == 1 && + string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, System.StringComparison.OrdinalIgnoreCase)) + { + if (this.ExpectedFiles.ContainsKey(path)) + { + this.ExpectedFiles.Remove(path); + } + + ++this.failuresAcrossOpenExistsAndOverwriteCount; + } + } + + return this.ExpectedFiles.ContainsKey(path); + } + + public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + if (this.maxMoveAndOverwriteFileFailures > 0) + { + if (this.moveAndOverwriteFileFailureCount < this.maxMoveAndOverwriteFileFailures) + { + ++this.moveAndOverwriteFileFailureCount; + throw new Win32Exception(); + } + } + else if (this.failuresAcrossOpenExistsAndOverwritePath != null) + { + if (this.failuresAcrossOpenExistsAndOverwriteCount == 2) + { + ++this.failuresAcrossOpenExistsAndOverwriteCount; + throw new Win32Exception(); + } + } + + ReusableMemoryStream source; + this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName); + this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename); + + this.ExpectedFiles.Remove(sourceFileName); + this.ExpectedFiles[destinationFilename] = source; + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) + { + ReusableMemoryStream stream; + this.ExpectedOpenFileStreams.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); + + if (this.maxOpenFileStreamFailures > 0) + { + if (this.openFileStreamFailureCount < this.maxOpenFileStreamFailures && + string.Equals(path, this.openFileStreamFailurePath, System.StringComparison.OrdinalIgnoreCase)) + { + ++this.openFileStreamFailureCount; + + if (this.openFileStreamFailureCount % 2 == 0) + { + throw new IOException(); + } + else + { + throw new UnauthorizedAccessException(); + } + } + } + else if (this.failuresAcrossOpenExistsAndOverwritePath != null) + { + if (this.failuresAcrossOpenExistsAndOverwriteCount == 0 && + string.Equals(path, this.failuresAcrossOpenExistsAndOverwritePath, System.StringComparison.OrdinalIgnoreCase)) + { + ++this.failuresAcrossOpenExistsAndOverwriteCount; + throw new IOException(); + } + } + + if (fileMode == FileMode.Create) + { + this.ExpectedFiles[path] = new ReusableMemoryStream(string.Empty); + } + + this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); + return stream; + } + } + } +} diff --git a/Scalar.UnitTests/Common/Git/Sha1IdTests.cs b/Scalar.UnitTests/Common/Git/Sha1IdTests.cs index 866962e381..b0ccf33992 100644 --- a/Scalar.UnitTests/Common/Git/Sha1IdTests.cs +++ b/Scalar.UnitTests/Common/Git/Sha1IdTests.cs @@ -1,47 +1,47 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; - -namespace Scalar.UnitTests.Common.Git -{ - [TestFixture] - public class Sha1IdTests - { - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryParseFailsForLowerCaseShas() - { - Sha1Id sha1; - string error; - Sha1Id.TryParse("abcdef7890123456789012345678901234567890", out sha1, out error).ShouldBeFalse(); - Sha1Id.TryParse(new string('a', 40), out sha1, out error).ShouldBeFalse(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryParseFailsForInvalidShas() - { - Sha1Id sha1; - string error; - Sha1Id.TryParse(null, out sha1, out error).ShouldBeFalse(); - Sha1Id.TryParse("0", out sha1, out error).ShouldBeFalse(); - Sha1Id.TryParse("abcdef", out sha1, out error).ShouldBeFalse(); - Sha1Id.TryParse(new string('H', 40), out sha1, out error).ShouldBeFalse(); - } - - [TestCase] - public void TryParseSucceedsForUpperCaseShas() - { - Sha1Id sha1Id; - string error; - string sha = "ABCDEF7890123456789012345678901234567890"; - Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue(); - sha1Id.ToString().ShouldEqual(sha); - - sha = new string('A', 40); - Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue(); - sha1Id.ToString().ShouldEqual(sha); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; + +namespace Scalar.UnitTests.Common.Git +{ + [TestFixture] + public class Sha1IdTests + { + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryParseFailsForLowerCaseShas() + { + Sha1Id sha1; + string error; + Sha1Id.TryParse("abcdef7890123456789012345678901234567890", out sha1, out error).ShouldBeFalse(); + Sha1Id.TryParse(new string('a', 40), out sha1, out error).ShouldBeFalse(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryParseFailsForInvalidShas() + { + Sha1Id sha1; + string error; + Sha1Id.TryParse(null, out sha1, out error).ShouldBeFalse(); + Sha1Id.TryParse("0", out sha1, out error).ShouldBeFalse(); + Sha1Id.TryParse("abcdef", out sha1, out error).ShouldBeFalse(); + Sha1Id.TryParse(new string('H', 40), out sha1, out error).ShouldBeFalse(); + } + + [TestCase] + public void TryParseSucceedsForUpperCaseShas() + { + Sha1Id sha1Id; + string error; + string sha = "ABCDEF7890123456789012345678901234567890"; + Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue(); + sha1Id.ToString().ShouldEqual(sha); + + sha = new string('A', 40); + Sha1Id.TryParse(sha, out sha1Id, out error).ShouldBeTrue(); + sha1Id.ToString().ShouldEqual(sha); + } + } +} diff --git a/Scalar.UnitTests/Common/GitCommandLineParserTests.cs b/Scalar.UnitTests/Common/GitCommandLineParserTests.cs index d022525c54..e41b534e6a 100644 --- a/Scalar.UnitTests/Common/GitCommandLineParserTests.cs +++ b/Scalar.UnitTests/Common/GitCommandLineParserTests.cs @@ -1,100 +1,100 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class GitCommandLineParserTests - { - [TestCase] - public void IsVerbTests() - { - new GitCommandLineParser("gits status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - - new GitCommandLineParser("git status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true); - new GitCommandLineParser("git status").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true); - new GitCommandLineParser("git statuses --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git statuses").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - - new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git clean").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); - - new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); - new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Checkout).ShouldEqual(true); - new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Commit).ShouldEqual(true); - new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Move).ShouldEqual(true); - new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Reset).ShouldEqual(true); - new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); - new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(true); - new GitCommandLineParser("git updateindex").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(false); - - new GitCommandLineParser("git add some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); - new GitCommandLineParser("git stage some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); - new GitCommandLineParser("git adds some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); - new GitCommandLineParser("git stages some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); - new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); - new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); - new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.Other).ShouldEqual(true); - } - - [TestCase] - public void IsResetSoftOrMixedTests() - { - new GitCommandLineParser("gits reset --soft").IsResetSoftOrMixed().ShouldEqual(false); - - new GitCommandLineParser("git reset --soft").IsResetSoftOrMixed().ShouldEqual(true); - new GitCommandLineParser("git reset --mixed").IsResetSoftOrMixed().ShouldEqual(true); - new GitCommandLineParser("git reset").IsResetSoftOrMixed().ShouldEqual(true); - - new GitCommandLineParser("git reset --hard").IsResetSoftOrMixed().ShouldEqual(false); - new GitCommandLineParser("git reset --keep").IsResetSoftOrMixed().ShouldEqual(false); - new GitCommandLineParser("git reset --merge").IsResetSoftOrMixed().ShouldEqual(false); - - new GitCommandLineParser("git checkout").IsResetSoftOrMixed().ShouldEqual(false); - new GitCommandLineParser("git status").IsResetSoftOrMixed().ShouldEqual(false); - } - - [TestCase] - public void IsCheckoutWithFilePathsTests() - { - new GitCommandLineParser("gits checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(false); - - new GitCommandLineParser("git checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(true); - new GitCommandLineParser("git checkout branch -- file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true); - new GitCommandLineParser("git checkout HEAD -- file").IsCheckoutWithFilePaths().ShouldEqual(true); - - new GitCommandLineParser("git checkout HEAD file").IsCheckoutWithFilePaths().ShouldEqual(true); - new GitCommandLineParser("git checkout HEAD file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true); - - new GitCommandLineParser("git checkout branch file").IsCheckoutWithFilePaths().ShouldEqual(false); - new GitCommandLineParser("git checkout branch").IsCheckoutWithFilePaths().ShouldEqual(false); - new GitCommandLineParser("git checkout HEAD").IsCheckoutWithFilePaths().ShouldEqual(false); - - new GitCommandLineParser("git checkout -b topic").IsCheckoutWithFilePaths().ShouldEqual(false); - - new GitCommandLineParser("git checkout -b topic --").IsCheckoutWithFilePaths().ShouldEqual(false); - new GitCommandLineParser("git checkout HEAD --").IsCheckoutWithFilePaths().ShouldEqual(false); - new GitCommandLineParser("git checkout HEAD -- ").IsCheckoutWithFilePaths().ShouldEqual(false); - } - - [TestCase] - public void IsSerializedStatusTests() - { - new GitCommandLineParser("git status --serialized=some/file").IsSerializedStatus().ShouldEqual(true); - new GitCommandLineParser("git status --serialized").IsSerializedStatus().ShouldEqual(true); - - new GitCommandLineParser("git checkout branch -- file").IsSerializedStatus().ShouldEqual(false); - new GitCommandLineParser("git status").IsSerializedStatus().ShouldEqual(false); - new GitCommandLineParser("git checkout --serialized").IsSerializedStatus().ShouldEqual(false); - new GitCommandLineParser("git checkout --serialized=some/file").IsSerializedStatus().ShouldEqual(false); - new GitCommandLineParser("gits status --serialized=some/file").IsSerializedStatus().ShouldEqual(false); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class GitCommandLineParserTests + { + [TestCase] + public void IsVerbTests() + { + new GitCommandLineParser("gits status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + + new GitCommandLineParser("git status --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true); + new GitCommandLineParser("git status").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(true); + new GitCommandLineParser("git statuses --no-idea").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git statuses").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + + new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git clean").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.Status).ShouldEqual(false); + + new GitCommandLineParser("git add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); + new GitCommandLineParser("git checkout").IsVerb(GitCommandLineParser.Verbs.Checkout).ShouldEqual(true); + new GitCommandLineParser("git commit").IsVerb(GitCommandLineParser.Verbs.Commit).ShouldEqual(true); + new GitCommandLineParser("git mv").IsVerb(GitCommandLineParser.Verbs.Move).ShouldEqual(true); + new GitCommandLineParser("git reset").IsVerb(GitCommandLineParser.Verbs.Reset).ShouldEqual(true); + new GitCommandLineParser("git stage").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); + new GitCommandLineParser("git update-index").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(true); + new GitCommandLineParser("git updateindex").IsVerb(GitCommandLineParser.Verbs.UpdateIndex).ShouldEqual(false); + + new GitCommandLineParser("git add some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); + new GitCommandLineParser("git stage some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(true); + new GitCommandLineParser("git adds some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); + new GitCommandLineParser("git stages some/file/to/add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); + new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); + new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.AddOrStage).ShouldEqual(false); + new GitCommandLineParser("git adding add").IsVerb(GitCommandLineParser.Verbs.Other).ShouldEqual(true); + } + + [TestCase] + public void IsResetSoftOrMixedTests() + { + new GitCommandLineParser("gits reset --soft").IsResetSoftOrMixed().ShouldEqual(false); + + new GitCommandLineParser("git reset --soft").IsResetSoftOrMixed().ShouldEqual(true); + new GitCommandLineParser("git reset --mixed").IsResetSoftOrMixed().ShouldEqual(true); + new GitCommandLineParser("git reset").IsResetSoftOrMixed().ShouldEqual(true); + + new GitCommandLineParser("git reset --hard").IsResetSoftOrMixed().ShouldEqual(false); + new GitCommandLineParser("git reset --keep").IsResetSoftOrMixed().ShouldEqual(false); + new GitCommandLineParser("git reset --merge").IsResetSoftOrMixed().ShouldEqual(false); + + new GitCommandLineParser("git checkout").IsResetSoftOrMixed().ShouldEqual(false); + new GitCommandLineParser("git status").IsResetSoftOrMixed().ShouldEqual(false); + } + + [TestCase] + public void IsCheckoutWithFilePathsTests() + { + new GitCommandLineParser("gits checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(false); + + new GitCommandLineParser("git checkout branch -- file").IsCheckoutWithFilePaths().ShouldEqual(true); + new GitCommandLineParser("git checkout branch -- file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true); + new GitCommandLineParser("git checkout HEAD -- file").IsCheckoutWithFilePaths().ShouldEqual(true); + + new GitCommandLineParser("git checkout HEAD file").IsCheckoutWithFilePaths().ShouldEqual(true); + new GitCommandLineParser("git checkout HEAD file1 file2").IsCheckoutWithFilePaths().ShouldEqual(true); + + new GitCommandLineParser("git checkout branch file").IsCheckoutWithFilePaths().ShouldEqual(false); + new GitCommandLineParser("git checkout branch").IsCheckoutWithFilePaths().ShouldEqual(false); + new GitCommandLineParser("git checkout HEAD").IsCheckoutWithFilePaths().ShouldEqual(false); + + new GitCommandLineParser("git checkout -b topic").IsCheckoutWithFilePaths().ShouldEqual(false); + + new GitCommandLineParser("git checkout -b topic --").IsCheckoutWithFilePaths().ShouldEqual(false); + new GitCommandLineParser("git checkout HEAD --").IsCheckoutWithFilePaths().ShouldEqual(false); + new GitCommandLineParser("git checkout HEAD -- ").IsCheckoutWithFilePaths().ShouldEqual(false); + } + + [TestCase] + public void IsSerializedStatusTests() + { + new GitCommandLineParser("git status --serialized=some/file").IsSerializedStatus().ShouldEqual(true); + new GitCommandLineParser("git status --serialized").IsSerializedStatus().ShouldEqual(true); + + new GitCommandLineParser("git checkout branch -- file").IsSerializedStatus().ShouldEqual(false); + new GitCommandLineParser("git status").IsSerializedStatus().ShouldEqual(false); + new GitCommandLineParser("git checkout --serialized").IsSerializedStatus().ShouldEqual(false); + new GitCommandLineParser("git checkout --serialized=some/file").IsSerializedStatus().ShouldEqual(false); + new GitCommandLineParser("gits status --serialized=some/file").IsSerializedStatus().ShouldEqual(false); + } + } +} diff --git a/Scalar.UnitTests/Common/GitConfigHelperTests.cs b/Scalar.UnitTests/Common/GitConfigHelperTests.cs index 9b828ab58a..b73d11b421 100644 --- a/Scalar.UnitTests/Common/GitConfigHelperTests.cs +++ b/Scalar.UnitTests/Common/GitConfigHelperTests.cs @@ -1,173 +1,173 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.Linq; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class GitConfigHelperTests - { - [TestCase] - public void SanitizeEmptyString() - { - string outputString; - GitConfigHelper.TrySanitizeConfigFileLine(string.Empty, out outputString).ShouldEqual(false); - } - - [TestCase] - public void SanitizePureWhiteSpace() - { - string outputString; - GitConfigHelper.TrySanitizeConfigFileLine(" ", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine(" \t\t ", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine(" \t\t\n\n ", out outputString).ShouldEqual(false); - } - - [TestCase] - public void SanitizeComment() - { - string outputString; - GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment ", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment #", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine("## This is a comment ##", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine(" ## This is a comment ## ", out outputString).ShouldEqual(false); - GitConfigHelper.TrySanitizeConfigFileLine("\t ## This is a comment ## \t ", out outputString).ShouldEqual(false); - } - - [TestCase] - public void TrimWhitspace() - { - string outputString; - GitConfigHelper.TrySanitizeConfigFileLine(" // ", out outputString).ShouldEqual(true); - outputString.ShouldEqual("//"); - - GitConfigHelper.TrySanitizeConfigFileLine(" /* ", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/*"); - - GitConfigHelper.TrySanitizeConfigFileLine(" /A ", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - - GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - - GitConfigHelper.TrySanitizeConfigFileLine(" \t /A \t", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - } - - [TestCase] - public void TrimTrailingComment() - { - string outputString; - GitConfigHelper.TrySanitizeConfigFileLine(" // # Trailing comment!", out outputString).ShouldEqual(true); - outputString.ShouldEqual("//"); - - GitConfigHelper.TrySanitizeConfigFileLine(" /* # Trailing comment!", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/*"); - - GitConfigHelper.TrySanitizeConfigFileLine(" /A # Trailing comment!", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - - GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t # Trailing comment! \t", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - - GitConfigHelper.TrySanitizeConfigFileLine(" \t /A \t # Trailing comment!", out outputString).ShouldEqual(true); - outputString.ShouldEqual("/A"); - } - - [TestCase] - public void ParseKeyValuesTest() - { - string input = @" -core.scalar=true -gc.auto=0 -section.key=value1 -section.key= value2 -section.key =value3 -section.key = value4 -section.KEY=value5 -section.empty= -"; - Dictionary result = GitConfigHelper.ParseKeyValues(input); - - result.Count.ShouldEqual(4); - result["core.scalar"].Values.Single().ShouldEqual("true"); - result["gc.auto"].Values.Single().ShouldEqual("0"); - result["section.key"].Values.Count.ShouldEqual(5); - result["section.key"].Values.ShouldContain(v => v == "value1"); - result["section.key"].Values.ShouldContain(v => v == "value2"); - result["section.key"].Values.ShouldContain(v => v == "value3"); - result["section.key"].Values.ShouldContain(v => v == "value4"); - result["section.key"].Values.ShouldContain(v => v == "value5"); - result["section.empty"].Values.Single().ShouldEqual(string.Empty); - } - - [TestCase] - public void ParseSpaceSeparatedKeyValuesTest() - { - string input = @" -core.scalar true -gc.auto 0 -section.key value1 -section.key value2 -section.key value3 -section.key value4 -section.KEY value5" + -"\nsection.empty "; - - Dictionary result = GitConfigHelper.ParseKeyValues(input, ' '); - - result.Count.ShouldEqual(4); - result["core.scalar"].Values.Single().ShouldEqual("true"); - result["gc.auto"].Values.Single().ShouldEqual("0"); - result["section.key"].Values.Count.ShouldEqual(5); - result["section.key"].Values.ShouldContain(v => v == "value1"); - result["section.key"].Values.ShouldContain(v => v == "value2"); - result["section.key"].Values.ShouldContain(v => v == "value3"); - result["section.key"].Values.ShouldContain(v => v == "value4"); - result["section.key"].Values.ShouldContain(v => v == "value5"); - result["section.empty"].Values.Single().ShouldEqual(string.Empty); - } - - [TestCase] - public void GetSettingsTest() - { - string fileContents = @" -[core] - scalar = true -[gc] - auto = 0 -[section] - key1 = 1 - key2 = 2 - key3 = 3 -[notsection] - keyN1 = N1 - keyN2 = N2 - keyN3 = N3 -[section] -[section] - key4 = 4 - key5 = 5 -[section] - key6 = 6 - key7 = - = emptyKey"; - - Dictionary result = GitConfigHelper.GetSettings(fileContents.Split('\r', '\n'), "Section"); - - int expectedCount = 7; // empty keys will not be included. - result.Count.ShouldEqual(expectedCount); - - // Verify keyN = N - for (int i = 1; i <= expectedCount - 1; i++) - { - result["key" + i.ToString()].Values.ShouldContain(v => v == i.ToString()); - } - - // Verify empty value - result["key7"].Values.Single().ShouldEqual(string.Empty); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.Linq; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class GitConfigHelperTests + { + [TestCase] + public void SanitizeEmptyString() + { + string outputString; + GitConfigHelper.TrySanitizeConfigFileLine(string.Empty, out outputString).ShouldEqual(false); + } + + [TestCase] + public void SanitizePureWhiteSpace() + { + string outputString; + GitConfigHelper.TrySanitizeConfigFileLine(" ", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine(" \t\t ", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine(" \t\t\n\n ", out outputString).ShouldEqual(false); + } + + [TestCase] + public void SanitizeComment() + { + string outputString; + GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment ", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine("# This is a comment #", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine("## This is a comment ##", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine(" ## This is a comment ## ", out outputString).ShouldEqual(false); + GitConfigHelper.TrySanitizeConfigFileLine("\t ## This is a comment ## \t ", out outputString).ShouldEqual(false); + } + + [TestCase] + public void TrimWhitspace() + { + string outputString; + GitConfigHelper.TrySanitizeConfigFileLine(" // ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("//"); + + GitConfigHelper.TrySanitizeConfigFileLine(" /* ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/*"); + + GitConfigHelper.TrySanitizeConfigFileLine(" /A ", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigHelper.TrySanitizeConfigFileLine(" \t /A \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + } + + [TestCase] + public void TrimTrailingComment() + { + string outputString; + GitConfigHelper.TrySanitizeConfigFileLine(" // # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("//"); + + GitConfigHelper.TrySanitizeConfigFileLine(" /* # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/*"); + + GitConfigHelper.TrySanitizeConfigFileLine(" /A # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigHelper.TrySanitizeConfigFileLine("\t /A \t # Trailing comment! \t", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + + GitConfigHelper.TrySanitizeConfigFileLine(" \t /A \t # Trailing comment!", out outputString).ShouldEqual(true); + outputString.ShouldEqual("/A"); + } + + [TestCase] + public void ParseKeyValuesTest() + { + string input = @" +core.scalar=true +gc.auto=0 +section.key=value1 +section.key= value2 +section.key =value3 +section.key = value4 +section.KEY=value5 +section.empty= +"; + Dictionary result = GitConfigHelper.ParseKeyValues(input); + + result.Count.ShouldEqual(4); + result["core.scalar"].Values.Single().ShouldEqual("true"); + result["gc.auto"].Values.Single().ShouldEqual("0"); + result["section.key"].Values.Count.ShouldEqual(5); + result["section.key"].Values.ShouldContain(v => v == "value1"); + result["section.key"].Values.ShouldContain(v => v == "value2"); + result["section.key"].Values.ShouldContain(v => v == "value3"); + result["section.key"].Values.ShouldContain(v => v == "value4"); + result["section.key"].Values.ShouldContain(v => v == "value5"); + result["section.empty"].Values.Single().ShouldEqual(string.Empty); + } + + [TestCase] + public void ParseSpaceSeparatedKeyValuesTest() + { + string input = @" +core.scalar true +gc.auto 0 +section.key value1 +section.key value2 +section.key value3 +section.key value4 +section.KEY value5" + +"\nsection.empty "; + + Dictionary result = GitConfigHelper.ParseKeyValues(input, ' '); + + result.Count.ShouldEqual(4); + result["core.scalar"].Values.Single().ShouldEqual("true"); + result["gc.auto"].Values.Single().ShouldEqual("0"); + result["section.key"].Values.Count.ShouldEqual(5); + result["section.key"].Values.ShouldContain(v => v == "value1"); + result["section.key"].Values.ShouldContain(v => v == "value2"); + result["section.key"].Values.ShouldContain(v => v == "value3"); + result["section.key"].Values.ShouldContain(v => v == "value4"); + result["section.key"].Values.ShouldContain(v => v == "value5"); + result["section.empty"].Values.Single().ShouldEqual(string.Empty); + } + + [TestCase] + public void GetSettingsTest() + { + string fileContents = @" +[core] + scalar = true +[gc] + auto = 0 +[section] + key1 = 1 + key2 = 2 + key3 = 3 +[notsection] + keyN1 = N1 + keyN2 = N2 + keyN3 = N3 +[section] +[section] + key4 = 4 + key5 = 5 +[section] + key6 = 6 + key7 = + = emptyKey"; + + Dictionary result = GitConfigHelper.GetSettings(fileContents.Split('\r', '\n'), "Section"); + + int expectedCount = 7; // empty keys will not be included. + result.Count.ShouldEqual(expectedCount); + + // Verify keyN = N + for (int i = 1; i <= expectedCount - 1; i++) + { + result["key" + i.ToString()].Values.ShouldContain(v => v == i.ToString()); + } + + // Verify empty value + result["key7"].Values.Single().ShouldEqual(string.Empty); + } + } +} diff --git a/Scalar.UnitTests/Common/GitObjectsTests.cs b/Scalar.UnitTests/Common/GitObjectsTests.cs index d2575e3db4..14c198e7e0 100644 --- a/Scalar.UnitTests/Common/GitObjectsTests.cs +++ b/Scalar.UnitTests/Common/GitObjectsTests.cs @@ -1,29 +1,29 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class GitObjectsTests - { - [TestCase] - public void IsLooseObjectsDirectory_ValidDirectories() - { - GitObjects.IsLooseObjectsDirectory("BB").ShouldBeTrue(); - GitObjects.IsLooseObjectsDirectory("bb").ShouldBeTrue(); - GitObjects.IsLooseObjectsDirectory("A7").ShouldBeTrue(); - GitObjects.IsLooseObjectsDirectory("55").ShouldBeTrue(); - } - - [TestCase] - public void IsLooseObjectsDirectory_InvalidDirectories() - { - GitObjects.IsLooseObjectsDirectory("K7").ShouldBeFalse(); - GitObjects.IsLooseObjectsDirectory("A-").ShouldBeFalse(); - GitObjects.IsLooseObjectsDirectory("?B").ShouldBeFalse(); - GitObjects.IsLooseObjectsDirectory("BBB").ShouldBeFalse(); - GitObjects.IsLooseObjectsDirectory("B-B").ShouldBeFalse(); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class GitObjectsTests + { + [TestCase] + public void IsLooseObjectsDirectory_ValidDirectories() + { + GitObjects.IsLooseObjectsDirectory("BB").ShouldBeTrue(); + GitObjects.IsLooseObjectsDirectory("bb").ShouldBeTrue(); + GitObjects.IsLooseObjectsDirectory("A7").ShouldBeTrue(); + GitObjects.IsLooseObjectsDirectory("55").ShouldBeTrue(); + } + + [TestCase] + public void IsLooseObjectsDirectory_InvalidDirectories() + { + GitObjects.IsLooseObjectsDirectory("K7").ShouldBeFalse(); + GitObjects.IsLooseObjectsDirectory("A-").ShouldBeFalse(); + GitObjects.IsLooseObjectsDirectory("?B").ShouldBeFalse(); + GitObjects.IsLooseObjectsDirectory("BBB").ShouldBeFalse(); + GitObjects.IsLooseObjectsDirectory("B-B").ShouldBeFalse(); + } + } +} diff --git a/Scalar.UnitTests/Common/GitPathConverterTests.cs b/Scalar.UnitTests/Common/GitPathConverterTests.cs index 938fe64981..cc527e6597 100644 --- a/Scalar.UnitTests/Common/GitPathConverterTests.cs +++ b/Scalar.UnitTests/Common/GitPathConverterTests.cs @@ -1,56 +1,56 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class GitPathConverterTests - { - private const string OctetEncoded = @"\330\261\331\212\331\204\331\214\330\243\331\203\330\252\331\210\330\250\330\261\303\273\331\205\330\247\330\261\330\263\330\243\330\272\330\263\330\267\330\263\302\272\331\260\331\260\333\202\331\227\331\222\333\265\330\261\331\212\331\204\331\214\330\243\331\203"; - private const string Utf8Encoded = @"ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك"; - private const string TestPath = @"/Scalar/"; - - [TestCase] - public void NullFilepathTest() - { - GitPathConverter.ConvertPathOctetsToUtf8(null).ShouldEqual(null); - } - - [TestCase] - public void EmptyFilepathTest() - { - GitPathConverter.ConvertPathOctetsToUtf8(string.Empty).ShouldEqual(string.Empty); - } - - [TestCase] - public void FilepathWithoutOctets() - { - GitPathConverter.ConvertPathOctetsToUtf8(TestPath + "test.cs").ShouldEqual(TestPath + "test.cs"); - } - - [TestCase] - public void FilepathWithoutOctetsAsFilename() - { - GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded).ShouldEqual(TestPath + Utf8Encoded); - } - - [TestCase] - public void FilepathWithoutOctetsAsFilenameNoExtension() - { - GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + ".txt"); - } - - [TestCase] - public void FilepathWithoutOctetsAsFolder() - { - GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + "/file.txt").ShouldEqual(TestPath + Utf8Encoded + "/file.txt"); - } - - [TestCase] - public void FilepathWithoutOctetsAsFileAndFolder() - { - GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + TestPath + Utf8Encoded + ".txt"); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class GitPathConverterTests + { + private const string OctetEncoded = @"\330\261\331\212\331\204\331\214\330\243\331\203\330\252\331\210\330\250\330\261\303\273\331\205\330\247\330\261\330\263\330\243\330\272\330\263\330\267\330\263\302\272\331\260\331\260\333\202\331\227\331\222\333\265\330\261\331\212\331\204\331\214\330\243\331\203"; + private const string Utf8Encoded = @"ريلٌأكتوبرûمارسأغسطسºٰٰۂْٗ۵ريلٌأك"; + private const string TestPath = @"/Scalar/"; + + [TestCase] + public void NullFilepathTest() + { + GitPathConverter.ConvertPathOctetsToUtf8(null).ShouldEqual(null); + } + + [TestCase] + public void EmptyFilepathTest() + { + GitPathConverter.ConvertPathOctetsToUtf8(string.Empty).ShouldEqual(string.Empty); + } + + [TestCase] + public void FilepathWithoutOctets() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + "test.cs").ShouldEqual(TestPath + "test.cs"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFilename() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded).ShouldEqual(TestPath + Utf8Encoded); + } + + [TestCase] + public void FilepathWithoutOctetsAsFilenameNoExtension() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + ".txt"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFolder() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + "/file.txt").ShouldEqual(TestPath + Utf8Encoded + "/file.txt"); + } + + [TestCase] + public void FilepathWithoutOctetsAsFileAndFolder() + { + GitPathConverter.ConvertPathOctetsToUtf8(TestPath + OctetEncoded + TestPath + OctetEncoded + ".txt").ShouldEqual(TestPath + Utf8Encoded + TestPath + Utf8Encoded + ".txt"); + } + } +} diff --git a/Scalar.UnitTests/Common/GitVersionTests.cs b/Scalar.UnitTests/Common/GitVersionTests.cs index fc68dff602..fcd0bdd42a 100644 --- a/Scalar.UnitTests/Common/GitVersionTests.cs +++ b/Scalar.UnitTests/Common/GitVersionTests.cs @@ -1,221 +1,221 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Tests.Should; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class GitVersionTests - { - [TestCase] - public void TryParseInstallerName() - { - this.ParseAndValidateInstallerVersion("Git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); - this.ParseAndValidateInstallerVersion("git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); - this.ParseAndValidateInstallerVersion("Git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); - } - - [TestCase] - public void Version_Data_Null_Returns_False() - { - GitVersion version; - bool success = GitVersion.TryParseVersion(null, out version); - success.ShouldEqual(false); - } - - [TestCase] - public void Version_Data_Empty_Returns_False() - { - GitVersion version; - bool success = GitVersion.TryParseVersion(string.Empty, out version); - success.ShouldEqual(false); - } - - [TestCase] - public void Version_Data_Not_Enough_Numbers_Returns_False() - { - GitVersion version; - bool success = GitVersion.TryParseVersion("2.0.1.test", out version); - success.ShouldEqual(false); - } - - [TestCase] - public void Version_Data_Too_Many_Numbers_Returns_True() - { - GitVersion version; - bool success = GitVersion.TryParseVersion("2.0.1.test.1.4.3.6", out version); - success.ShouldEqual(true); - } - - [TestCase] - public void Version_Data_Valid_Returns_True() - { - GitVersion version; - bool success = GitVersion.TryParseVersion("2.0.1.test.1.2", out version); - success.ShouldEqual(true); - } - - [TestCase] - public void Compare_Different_Platforms_Returns_False() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test1", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Equal() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(true); - } - - [TestCase] - public void Compare_Version_Major_Less() - { - GitVersion version1 = new GitVersion(0, 2, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(true); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Major_Greater() - { - GitVersion version1 = new GitVersion(2, 2, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Minor_Less() - { - GitVersion version1 = new GitVersion(1, 1, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(true); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Minor_Greater() - { - GitVersion version1 = new GitVersion(1, 3, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Build_Less() - { - GitVersion version1 = new GitVersion(1, 2, 2, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(true); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Build_Greater() - { - GitVersion version1 = new GitVersion(1, 2, 4, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Revision_Less() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 3, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(true); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_Revision_Greater() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 5, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_MinorRevision_Less() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 2); - - version1.IsLessThan(version2).ShouldEqual(true); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Compare_Version_MinorRevision_Greater() - { - GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 2); - GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); - - version1.IsLessThan(version2).ShouldEqual(false); - version1.IsEqualTo(version2).ShouldEqual(false); - } - - [TestCase] - public void Allow_Blank_Minor_Revision() - { - GitVersion version; - GitVersion.TryParseVersion("1.2.3.test.4", out version).ShouldEqual(true); - - version.Major.ShouldEqual(1); - version.Minor.ShouldEqual(2); - version.Build.ShouldEqual(3); - version.Platform.ShouldEqual("test"); - version.Revision.ShouldEqual(4); - version.MinorRevision.ShouldEqual(0); - } - - [TestCase] - public void Allow_Invalid_Minor_Revision() - { - GitVersion version; - GitVersion.TryParseVersion("1.2.3.test.4.notint", out version).ShouldEqual(true); - - version.Major.ShouldEqual(1); - version.Minor.ShouldEqual(2); - version.Build.ShouldEqual(3); - version.Platform.ShouldEqual("test"); - version.Revision.ShouldEqual(4); - version.MinorRevision.ShouldEqual(0); - } - - private void ParseAndValidateInstallerVersion(string installerName) - { - GitVersion version; - bool success = GitVersion.TryParseInstallerName(installerName, ScalarPlatform.Instance.Constants.InstallerExtension, out version); - success.ShouldBeTrue(); - - version.Major.ShouldEqual(1); - version.Minor.ShouldEqual(2); - version.Build.ShouldEqual(3); - version.Platform.ShouldEqual("scalar"); - version.Revision.ShouldEqual(4); - version.MinorRevision.ShouldEqual(5); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Tests.Should; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class GitVersionTests + { + [TestCase] + public void TryParseInstallerName() + { + this.ParseAndValidateInstallerVersion("Git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); + this.ParseAndValidateInstallerVersion("git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); + this.ParseAndValidateInstallerVersion("Git-1.2.3.scalar.4.5.gb16030b-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); + } + + [TestCase] + public void Version_Data_Null_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParseVersion(null, out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Empty_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParseVersion(string.Empty, out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Not_Enough_Numbers_Returns_False() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1.test", out version); + success.ShouldEqual(false); + } + + [TestCase] + public void Version_Data_Too_Many_Numbers_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1.test.1.4.3.6", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Version_Data_Valid_Returns_True() + { + GitVersion version; + bool success = GitVersion.TryParseVersion("2.0.1.test.1.2", out version); + success.ShouldEqual(true); + } + + [TestCase] + public void Compare_Different_Platforms_Returns_False() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test1", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Equal() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(true); + } + + [TestCase] + public void Compare_Version_Major_Less() + { + GitVersion version1 = new GitVersion(0, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Major_Greater() + { + GitVersion version1 = new GitVersion(2, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Minor_Less() + { + GitVersion version1 = new GitVersion(1, 1, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Minor_Greater() + { + GitVersion version1 = new GitVersion(1, 3, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Build_Less() + { + GitVersion version1 = new GitVersion(1, 2, 2, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Build_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 4, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Revision_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 3, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_Revision_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 5, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_MinorRevision_Less() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 1); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 2); + + version1.IsLessThan(version2).ShouldEqual(true); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Compare_Version_MinorRevision_Greater() + { + GitVersion version1 = new GitVersion(1, 2, 3, "test", 4, 2); + GitVersion version2 = new GitVersion(1, 2, 3, "test", 4, 1); + + version1.IsLessThan(version2).ShouldEqual(false); + version1.IsEqualTo(version2).ShouldEqual(false); + } + + [TestCase] + public void Allow_Blank_Minor_Revision() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3.test.4", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.Platform.ShouldEqual("test"); + version.Revision.ShouldEqual(4); + version.MinorRevision.ShouldEqual(0); + } + + [TestCase] + public void Allow_Invalid_Minor_Revision() + { + GitVersion version; + GitVersion.TryParseVersion("1.2.3.test.4.notint", out version).ShouldEqual(true); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.Platform.ShouldEqual("test"); + version.Revision.ShouldEqual(4); + version.MinorRevision.ShouldEqual(0); + } + + private void ParseAndValidateInstallerVersion(string installerName) + { + GitVersion version; + bool success = GitVersion.TryParseInstallerName(installerName, ScalarPlatform.Instance.Constants.InstallerExtension, out version); + success.ShouldBeTrue(); + + version.Major.ShouldEqual(1); + version.Minor.ShouldEqual(2); + version.Build.ShouldEqual(3); + version.Platform.ShouldEqual("scalar"); + version.Revision.ShouldEqual(4); + version.MinorRevision.ShouldEqual(5); + } + } +} diff --git a/Scalar.UnitTests/Common/InstallManifestTests.cs b/Scalar.UnitTests/Common/InstallManifestTests.cs index d909db5ebb..6f3496ba9a 100644 --- a/Scalar.UnitTests/Common/InstallManifestTests.cs +++ b/Scalar.UnitTests/Common/InstallManifestTests.cs @@ -1,140 +1,140 @@ -using Newtonsoft.Json; -using NUnit.Framework; -using Scalar.Common.NuGetUpgrade; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class JsonInstallManifestTests - { - private static int manifestEntryCount = 0; - - [TestCase] - public void CanReadExpectedJsonString() - { - string installManifestJsonString = -@" -{ - ""Version"" : ""1"", - ""PlatformInstallManifests"" : { - ""Windows"": { - ""InstallActions"": [ - { - ""Name"" : ""Git"", - ""Version"" : ""2.19.0.1.34"", - ""InstallerRelativePath"" : ""Installers\\Windows\\G4W\\Git-2.19.0.scalar.1.34.gc7fb556-64-bit.exe"", - ""Args"" : ""/VERYSILENT /CLOSEAPPLICATIONS"" - }, - { - ""Name"" : ""PostGitInstall script"", - ""InstallerRelativePath"" : ""Installers\\Windows\\GSD\\postinstall.ps1"" - }, - ] - } - } -} -"; - InstallManifest installManifest = InstallManifest.FromJsonString(installManifestJsonString); - - installManifest.ShouldNotBeNull(); - InstallManifestPlatform platformInstallManifest = installManifest.PlatformInstallManifests[InstallManifest.WindowsPlatformKey]; - platformInstallManifest.ShouldNotBeNull(); - platformInstallManifest.InstallActions.Count.ShouldEqual(2); - - this.VerifyInstallActionInfo( - platformInstallManifest.InstallActions[0], - "Git", - "2.19.0.1.34", - "/VERYSILENT /CLOSEAPPLICATIONS", - "Installers\\Windows\\G4W\\Git-2.19.0.scalar.1.34.gc7fb556-64-bit.exe"); - - this.VerifyInstallActionInfo( - platformInstallManifest.InstallActions[1], - "PostGitInstall script", - null, - null, - "Installers\\Windows\\GSD\\postinstall.ps1"); - } - - [TestCase] - public void CanDeserializeAndSerializeInstallManifest() - { - List entries = new List() - { - this.CreateInstallActionInfo(), - this.CreateInstallActionInfo() - }; - - InstallManifest installManifest = new InstallManifest(); - installManifest.AddPlatformInstallManifest(InstallManifest.WindowsPlatformKey, entries); - - JsonSerializer serializer = new JsonSerializer(); - - using (MemoryStream ms = new MemoryStream()) - using (StreamWriter streamWriter = new StreamWriter(ms)) - using (JsonWriter jsWriter = new JsonTextWriter(streamWriter)) - { - string output = JsonConvert.SerializeObject(installManifest); - serializer.Serialize(jsWriter, installManifest); - jsWriter.Flush(); - - ms.Seek(0, SeekOrigin.Begin); - - StreamReader streamReader = new StreamReader(ms); - InstallManifest deserializedInstallManifest = InstallManifest.FromJson(streamReader); - - this.VerifyInstallManifestsAreEqual(installManifest, deserializedInstallManifest); - } - } - - private InstallActionInfo CreateInstallActionInfo() - { - int entrySuffix = manifestEntryCount++; - return new InstallActionInfo( - name: $"Installer{entrySuffix}", - version: $"1.{entrySuffix}.1.2", - args: $"/nodowngrade{entrySuffix}", - installerRelativePath: $"installers/installer1{entrySuffix}", - command: string.Empty); - } - - private void VerifyInstallManifestsAreEqual(InstallManifest expected, InstallManifest actual) - { - actual.PlatformInstallManifests.Count.ShouldEqual(expected.PlatformInstallManifests.Count, $"The number of platforms ({actual.PlatformInstallManifests.Count}) do not match the expected number of platforms ({expected.PlatformInstallManifests.Count})."); - - foreach (KeyValuePair kvp in expected.PlatformInstallManifests) - { - this.VerifyPlatformManifestsAreEqual(kvp.Value, actual.PlatformInstallManifests[kvp.Key]); - } - } - - private void VerifyInstallActionInfo( - InstallActionInfo actualEntry, - string expectedName, - string expectedVersion, - string expectedArgs, - string expectedInstallerRelativePath) - { - actualEntry.Name.ShouldEqual(expectedName, "InstallActionInfo name does not match expected value"); - actualEntry.Version.ShouldEqual(expectedVersion, "InstallActionInfo version does not match expected value"); - actualEntry.Args.ShouldEqual(expectedArgs, "InstallActionInfo Args does not match expected value"); - actualEntry.InstallerRelativePath.ShouldEqual(expectedInstallerRelativePath, "InstallActionInfo InstallerRelativePath does not match expected value"); - } - - private void VerifyPlatformManifestsAreEqual(InstallManifestPlatform expected, InstallManifestPlatform actual) - { - actual.InstallActions.Count.ShouldEqual(expected.InstallActions.Count, $"The number of platforms ({actual.InstallActions.Count}) do not match the expected number of platforms ({expected.InstallActions.Count})."); - - for (int i = 0; i < actual.InstallActions.Count; i++) - { - actual.InstallActions[i].Version.ShouldEqual(expected.InstallActions[i].Version); - actual.InstallActions[i].Args.ShouldEqual(expected.InstallActions[i].Args); - actual.InstallActions[i].Name.ShouldEqual(expected.InstallActions[i].Name); - actual.InstallActions[i].InstallerRelativePath.ShouldEqual(expected.InstallActions[i].InstallerRelativePath); - } - } - } -} +using Newtonsoft.Json; +using NUnit.Framework; +using Scalar.Common.NuGetUpgrade; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class JsonInstallManifestTests + { + private static int manifestEntryCount = 0; + + [TestCase] + public void CanReadExpectedJsonString() + { + string installManifestJsonString = +@" +{ + ""Version"" : ""1"", + ""PlatformInstallManifests"" : { + ""Windows"": { + ""InstallActions"": [ + { + ""Name"" : ""Git"", + ""Version"" : ""2.19.0.1.34"", + ""InstallerRelativePath"" : ""Installers\\Windows\\G4W\\Git-2.19.0.scalar.1.34.gc7fb556-64-bit.exe"", + ""Args"" : ""/VERYSILENT /CLOSEAPPLICATIONS"" + }, + { + ""Name"" : ""PostGitInstall script"", + ""InstallerRelativePath"" : ""Installers\\Windows\\GSD\\postinstall.ps1"" + }, + ] + } + } +} +"; + InstallManifest installManifest = InstallManifest.FromJsonString(installManifestJsonString); + + installManifest.ShouldNotBeNull(); + InstallManifestPlatform platformInstallManifest = installManifest.PlatformInstallManifests[InstallManifest.WindowsPlatformKey]; + platformInstallManifest.ShouldNotBeNull(); + platformInstallManifest.InstallActions.Count.ShouldEqual(2); + + this.VerifyInstallActionInfo( + platformInstallManifest.InstallActions[0], + "Git", + "2.19.0.1.34", + "/VERYSILENT /CLOSEAPPLICATIONS", + "Installers\\Windows\\G4W\\Git-2.19.0.scalar.1.34.gc7fb556-64-bit.exe"); + + this.VerifyInstallActionInfo( + platformInstallManifest.InstallActions[1], + "PostGitInstall script", + null, + null, + "Installers\\Windows\\GSD\\postinstall.ps1"); + } + + [TestCase] + public void CanDeserializeAndSerializeInstallManifest() + { + List entries = new List() + { + this.CreateInstallActionInfo(), + this.CreateInstallActionInfo() + }; + + InstallManifest installManifest = new InstallManifest(); + installManifest.AddPlatformInstallManifest(InstallManifest.WindowsPlatformKey, entries); + + JsonSerializer serializer = new JsonSerializer(); + + using (MemoryStream ms = new MemoryStream()) + using (StreamWriter streamWriter = new StreamWriter(ms)) + using (JsonWriter jsWriter = new JsonTextWriter(streamWriter)) + { + string output = JsonConvert.SerializeObject(installManifest); + serializer.Serialize(jsWriter, installManifest); + jsWriter.Flush(); + + ms.Seek(0, SeekOrigin.Begin); + + StreamReader streamReader = new StreamReader(ms); + InstallManifest deserializedInstallManifest = InstallManifest.FromJson(streamReader); + + this.VerifyInstallManifestsAreEqual(installManifest, deserializedInstallManifest); + } + } + + private InstallActionInfo CreateInstallActionInfo() + { + int entrySuffix = manifestEntryCount++; + return new InstallActionInfo( + name: $"Installer{entrySuffix}", + version: $"1.{entrySuffix}.1.2", + args: $"/nodowngrade{entrySuffix}", + installerRelativePath: $"installers/installer1{entrySuffix}", + command: string.Empty); + } + + private void VerifyInstallManifestsAreEqual(InstallManifest expected, InstallManifest actual) + { + actual.PlatformInstallManifests.Count.ShouldEqual(expected.PlatformInstallManifests.Count, $"The number of platforms ({actual.PlatformInstallManifests.Count}) do not match the expected number of platforms ({expected.PlatformInstallManifests.Count})."); + + foreach (KeyValuePair kvp in expected.PlatformInstallManifests) + { + this.VerifyPlatformManifestsAreEqual(kvp.Value, actual.PlatformInstallManifests[kvp.Key]); + } + } + + private void VerifyInstallActionInfo( + InstallActionInfo actualEntry, + string expectedName, + string expectedVersion, + string expectedArgs, + string expectedInstallerRelativePath) + { + actualEntry.Name.ShouldEqual(expectedName, "InstallActionInfo name does not match expected value"); + actualEntry.Version.ShouldEqual(expectedVersion, "InstallActionInfo version does not match expected value"); + actualEntry.Args.ShouldEqual(expectedArgs, "InstallActionInfo Args does not match expected value"); + actualEntry.InstallerRelativePath.ShouldEqual(expectedInstallerRelativePath, "InstallActionInfo InstallerRelativePath does not match expected value"); + } + + private void VerifyPlatformManifestsAreEqual(InstallManifestPlatform expected, InstallManifestPlatform actual) + { + actual.InstallActions.Count.ShouldEqual(expected.InstallActions.Count, $"The number of platforms ({actual.InstallActions.Count}) do not match the expected number of platforms ({expected.InstallActions.Count})."); + + for (int i = 0; i < actual.InstallActions.Count; i++) + { + actual.InstallActions[i].Version.ShouldEqual(expected.InstallActions[i].Version); + actual.InstallActions[i].Args.ShouldEqual(expected.InstallActions[i].Args); + actual.InstallActions[i].Name.ShouldEqual(expected.InstallActions[i].Name); + actual.InstallActions[i].InstallerRelativePath.ShouldEqual(expected.InstallActions[i].InstallerRelativePath); + } + } + } +} diff --git a/Scalar.UnitTests/Common/JsonTracerTests.cs b/Scalar.UnitTests/Common/JsonTracerTests.cs index c7b0e06374..ddeaaf18f4 100644 --- a/Scalar.UnitTests/Common/JsonTracerTests.cs +++ b/Scalar.UnitTests/Common/JsonTracerTests.cs @@ -1,133 +1,133 @@ -using NUnit.Framework; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common.Tracing; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class JsonTracerTests - { - [TestCase] - public void EventsAreFilteredByVerbosity() - { - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByVerbosity1", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Informational, Keywords.Any)) - { - tracer.AddEventListener(listener); - - tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); - - tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); - listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); - } - - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByVerbosity2", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) - { - tracer.AddEventListener(listener); - - tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); - - tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); - } - } - - [TestCase] - public void EventsAreFilteredByKeyword() - { - // Network filters all but network out - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword1", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Network)) - { - tracer.AddEventListener(listener); - - tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); - - tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); - listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); - } - - // Any filters nothing out - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword2", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) - { - tracer.AddEventListener(listener); - - tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); - - tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); - listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); - } - - // None filters everything out (including events marked as none) - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword3", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.None)) - { - tracer.AddEventListener(listener); - - tracer.RelatedEvent(EventLevel.Informational, "ShouldNotReceive", metadata: null, keyword: Keywords.Network); - listener.EventNamesRead.ShouldBeEmpty(); - - tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoNotReceive", metadata: null); - listener.EventNamesRead.ShouldBeEmpty(); - } - } - - [TestCase] - public void EventMetadataWithKeywordsIsOptional() - { - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventMetadataWithKeywordsIsOptional", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) - { - tracer.AddEventListener(listener); - - tracer.RelatedWarning(metadata: null, message: string.Empty, keywords: Keywords.Telemetry); - listener.EventNamesRead.ShouldContain(x => x.Equals("Warning")); - - tracer.RelatedError(metadata: null, message: string.Empty, keywords: Keywords.Telemetry); - listener.EventNamesRead.ShouldContain(x => x.Equals("Error")); - } - } - - [TestCase] - public void StartEventDoesNotDispatchTelemetry() - { - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "StartEventDoesNotDispatchTelemetry", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry)) - { - tracer.AddEventListener(listener); - - using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null)) - { - listener.EventNamesRead.ShouldBeEmpty(); - - activity.Stop(null); - listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity")); - } - } - } - - [TestCase] - public void StopEventIsDispatchedOnDispose() - { - using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "StopEventIsDispatchedOnDispose", disableTelemetry: true)) - using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry)) - { - tracer.AddEventListener(listener); - - using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null)) - { - listener.EventNamesRead.ShouldBeEmpty(); - } - - listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity")); - } - } - } -} +using NUnit.Framework; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common.Tracing; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class JsonTracerTests + { + [TestCase] + public void EventsAreFilteredByVerbosity() + { + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByVerbosity1", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Informational, Keywords.Any)) + { + tracer.AddEventListener(listener); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); + listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); + } + + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByVerbosity2", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) + { + tracer.AddEventListener(listener); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); + } + } + + [TestCase] + public void EventsAreFilteredByKeyword() + { + // Network filters all but network out + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword1", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Network)) + { + tracer.AddEventListener(listener); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldNotReceive", metadata: null); + listener.EventNamesRead.ShouldNotContain(name => name.Equals("ShouldNotReceive")); + } + + // Any filters nothing out + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword2", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) + { + tracer.AddEventListener(listener); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldReceive")); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoReceive", metadata: null); + listener.EventNamesRead.ShouldContain(name => name.Equals("ShouldAlsoReceive")); + } + + // None filters everything out (including events marked as none) + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventsAreFilteredByKeyword3", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.None)) + { + tracer.AddEventListener(listener); + + tracer.RelatedEvent(EventLevel.Informational, "ShouldNotReceive", metadata: null, keyword: Keywords.Network); + listener.EventNamesRead.ShouldBeEmpty(); + + tracer.RelatedEvent(EventLevel.Verbose, "ShouldAlsoNotReceive", metadata: null); + listener.EventNamesRead.ShouldBeEmpty(); + } + } + + [TestCase] + public void EventMetadataWithKeywordsIsOptional() + { + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "EventMetadataWithKeywordsIsOptional", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Any)) + { + tracer.AddEventListener(listener); + + tracer.RelatedWarning(metadata: null, message: string.Empty, keywords: Keywords.Telemetry); + listener.EventNamesRead.ShouldContain(x => x.Equals("Warning")); + + tracer.RelatedError(metadata: null, message: string.Empty, keywords: Keywords.Telemetry); + listener.EventNamesRead.ShouldContain(x => x.Equals("Error")); + } + } + + [TestCase] + public void StartEventDoesNotDispatchTelemetry() + { + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "StartEventDoesNotDispatchTelemetry", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry)) + { + tracer.AddEventListener(listener); + + using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null)) + { + listener.EventNamesRead.ShouldBeEmpty(); + + activity.Stop(null); + listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity")); + } + } + } + + [TestCase] + public void StopEventIsDispatchedOnDispose() + { + using (JsonTracer tracer = new JsonTracer("Microsoft-Scalar-Test", "StopEventIsDispatchedOnDispose", disableTelemetry: true)) + using (MockListener listener = new MockListener(EventLevel.Verbose, Keywords.Telemetry)) + { + tracer.AddEventListener(listener); + + using (ITracer activity = tracer.StartActivity("TestActivity", EventLevel.Informational, Keywords.Telemetry, null)) + { + listener.EventNamesRead.ShouldBeEmpty(); + } + + listener.EventNamesRead.ShouldContain(x => x.Equals("TestActivity")); + } + } + } +} diff --git a/Scalar.UnitTests/Common/LibGit2RepoInvokerTests.cs b/Scalar.UnitTests/Common/LibGit2RepoInvokerTests.cs index 34e6c8e45a..b7af4ab75b 100644 --- a/Scalar.UnitTests/Common/LibGit2RepoInvokerTests.cs +++ b/Scalar.UnitTests/Common/LibGit2RepoInvokerTests.cs @@ -1,204 +1,204 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using System.Collections.Concurrent; -using System.Threading; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class LibGit2RepoInvokerTests - { - private MockTracer tracer; - private LibGit2RepoInvoker invoker; - private int numConstructors; - private int numDisposals; - - public BlockingCollection DisposalTriggers { get; set; } - - [SetUp] - public void Setup() - { - this.invoker?.Dispose(); - - this.tracer = new MockTracer(); - this.numConstructors = 0; - this.numDisposals = 0; - this.DisposalTriggers = new BlockingCollection(); - - this.invoker = new LibGit2RepoInvoker(this.tracer, this.CreateRepo); - } - - [TestCase] - public void DoesCreateRepoOnConstruction() - { - this.numConstructors.ShouldEqual(1); - } - - [TestCase] - public void CreatedByInitializeAfterClosed() - { - this.numDisposals.ShouldEqual(0); - this.numConstructors.ShouldEqual(1); - - this.invoker.DisposeSharedRepo(); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(1); - - this.invoker.InitializeSharedRepo(); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(2); - - // This should not create another repo - this.invoker.TryInvoke(repo => { return true; }, out bool result); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(2); - } - - [TestCase] - public void CreatesOnInvokeAfterClosed() - { - this.numConstructors.ShouldEqual(1); - - this.invoker.DisposeSharedRepo(); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(1); - - this.invoker.TryInvoke(repo => { return true; }, out bool result); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(2); - - // This should not create another repo - this.invoker.InitializeSharedRepo(); - - this.numDisposals.ShouldEqual(1); - this.numConstructors.ShouldEqual(2); - } - - [TestCase] - public void DoesNotCreateMultipleRepos() - { - this.numConstructors.ShouldEqual(1); - - this.invoker.TryInvoke(repo => { return true; }, out bool result); - result.ShouldEqual(true); - this.numConstructors.ShouldEqual(1); - - this.invoker.TryInvoke(repo => { return true; }, out result); - result.ShouldEqual(true); - this.numConstructors.ShouldEqual(1); - - this.invoker.InitializeSharedRepo(); - this.numConstructors.ShouldEqual(1); - } - - [TestCase] - public void DoesNotCreateRepoAfterDisposal() - { - this.numConstructors.ShouldEqual(1); - this.invoker.Dispose(); - this.invoker.TryInvoke(repo => { return true; }, out bool result); - result.ShouldEqual(false); - this.numConstructors.ShouldEqual(1); - } - - [TestCase] - public void DisposesSharedRepo() - { - this.numConstructors.ShouldEqual(1); - this.numDisposals.ShouldEqual(0); - - this.invoker.TryInvoke(repo => { return true; }, out bool result); - result.ShouldEqual(true); - this.numConstructors.ShouldEqual(1); - - this.invoker.Dispose(); - this.numConstructors.ShouldEqual(1); - this.numDisposals.ShouldEqual(1); - } - - [TestCase] - public void UsesOnlyOneRepoMultipleThreads() - { - this.numConstructors.ShouldEqual(1); - - Thread[] threads = new Thread[10]; - BlockingCollection threadStarted = new BlockingCollection(); - BlockingCollection allowNextThreadToContinue = new BlockingCollection(); - - for (int i = 0; i < threads.Length; i++) - { - threads[i] = new Thread(() => - { - this.invoker.TryInvoke( - repo => - { - threadStarted.Add(new object()); - allowNextThreadToContinue.Take(); - - // Give the timer an opportunity to fire - Thread.Sleep(2); - - allowNextThreadToContinue.Add(new object()); - return true; - }, - out bool result); - result.ShouldEqual(true); - this.numConstructors.ShouldEqual(1); - }); - } - - // Ensure all threads are started before letting them continue - for (int i = 0; i < threads.Length; i++) - { - threads[i].Start(); - threadStarted.Take(); - } - - allowNextThreadToContinue.Add(new object()); - - for (int i = 0; i < threads.Length; i++) - { - threads[i].Join(); - } - - this.numConstructors.ShouldEqual(1); - } - - private LibGit2Repo CreateRepo() - { - Interlocked.Increment(ref this.numConstructors); - return new MockLibGit2Repo(this); - } - - private class MockLibGit2Repo : LibGit2Repo - { - private readonly LibGit2RepoInvokerTests parent; - - public MockLibGit2Repo(LibGit2RepoInvokerTests parent) - { - this.parent = parent; - } - - public override bool ObjectExists(string sha) - { - return false; - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - Interlocked.Increment(ref this.parent.numDisposals); - this.parent.DisposalTriggers.Add(new object()); - } - } - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using System.Collections.Concurrent; +using System.Threading; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class LibGit2RepoInvokerTests + { + private MockTracer tracer; + private LibGit2RepoInvoker invoker; + private int numConstructors; + private int numDisposals; + + public BlockingCollection DisposalTriggers { get; set; } + + [SetUp] + public void Setup() + { + this.invoker?.Dispose(); + + this.tracer = new MockTracer(); + this.numConstructors = 0; + this.numDisposals = 0; + this.DisposalTriggers = new BlockingCollection(); + + this.invoker = new LibGit2RepoInvoker(this.tracer, this.CreateRepo); + } + + [TestCase] + public void DoesCreateRepoOnConstruction() + { + this.numConstructors.ShouldEqual(1); + } + + [TestCase] + public void CreatedByInitializeAfterClosed() + { + this.numDisposals.ShouldEqual(0); + this.numConstructors.ShouldEqual(1); + + this.invoker.DisposeSharedRepo(); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(1); + + this.invoker.InitializeSharedRepo(); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(2); + + // This should not create another repo + this.invoker.TryInvoke(repo => { return true; }, out bool result); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(2); + } + + [TestCase] + public void CreatesOnInvokeAfterClosed() + { + this.numConstructors.ShouldEqual(1); + + this.invoker.DisposeSharedRepo(); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(1); + + this.invoker.TryInvoke(repo => { return true; }, out bool result); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(2); + + // This should not create another repo + this.invoker.InitializeSharedRepo(); + + this.numDisposals.ShouldEqual(1); + this.numConstructors.ShouldEqual(2); + } + + [TestCase] + public void DoesNotCreateMultipleRepos() + { + this.numConstructors.ShouldEqual(1); + + this.invoker.TryInvoke(repo => { return true; }, out bool result); + result.ShouldEqual(true); + this.numConstructors.ShouldEqual(1); + + this.invoker.TryInvoke(repo => { return true; }, out result); + result.ShouldEqual(true); + this.numConstructors.ShouldEqual(1); + + this.invoker.InitializeSharedRepo(); + this.numConstructors.ShouldEqual(1); + } + + [TestCase] + public void DoesNotCreateRepoAfterDisposal() + { + this.numConstructors.ShouldEqual(1); + this.invoker.Dispose(); + this.invoker.TryInvoke(repo => { return true; }, out bool result); + result.ShouldEqual(false); + this.numConstructors.ShouldEqual(1); + } + + [TestCase] + public void DisposesSharedRepo() + { + this.numConstructors.ShouldEqual(1); + this.numDisposals.ShouldEqual(0); + + this.invoker.TryInvoke(repo => { return true; }, out bool result); + result.ShouldEqual(true); + this.numConstructors.ShouldEqual(1); + + this.invoker.Dispose(); + this.numConstructors.ShouldEqual(1); + this.numDisposals.ShouldEqual(1); + } + + [TestCase] + public void UsesOnlyOneRepoMultipleThreads() + { + this.numConstructors.ShouldEqual(1); + + Thread[] threads = new Thread[10]; + BlockingCollection threadStarted = new BlockingCollection(); + BlockingCollection allowNextThreadToContinue = new BlockingCollection(); + + for (int i = 0; i < threads.Length; i++) + { + threads[i] = new Thread(() => + { + this.invoker.TryInvoke( + repo => + { + threadStarted.Add(new object()); + allowNextThreadToContinue.Take(); + + // Give the timer an opportunity to fire + Thread.Sleep(2); + + allowNextThreadToContinue.Add(new object()); + return true; + }, + out bool result); + result.ShouldEqual(true); + this.numConstructors.ShouldEqual(1); + }); + } + + // Ensure all threads are started before letting them continue + for (int i = 0; i < threads.Length; i++) + { + threads[i].Start(); + threadStarted.Take(); + } + + allowNextThreadToContinue.Add(new object()); + + for (int i = 0; i < threads.Length; i++) + { + threads[i].Join(); + } + + this.numConstructors.ShouldEqual(1); + } + + private LibGit2Repo CreateRepo() + { + Interlocked.Increment(ref this.numConstructors); + return new MockLibGit2Repo(this); + } + + private class MockLibGit2Repo : LibGit2Repo + { + private readonly LibGit2RepoInvokerTests parent; + + public MockLibGit2Repo(LibGit2RepoInvokerTests parent) + { + this.parent = parent; + } + + public override bool ObjectExists(string sha) + { + return false; + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + Interlocked.Increment(ref this.parent.numDisposals); + this.parent.DisposalTriggers.Add(new object()); + } + } + } + } +} diff --git a/Scalar.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs b/Scalar.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs index 618db0ca4b..5031bf61e1 100644 --- a/Scalar.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs +++ b/Scalar.UnitTests/Common/NamedPipeStreamReaderWriterTests.cs @@ -1,111 +1,111 @@ -using NUnit.Framework; -using Scalar.Common.NamedPipes; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using System.IO; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class NamedPipeStreamReaderWriterTests - { - private MemoryStream stream; - private NamedPipeStreamWriter streamWriter; - private NamedPipeStreamReader streamReader; - - [SetUp] - public void Setup() - { - this.stream = new MemoryStream(); - this.streamWriter = new NamedPipeStreamWriter(this.stream); - this.streamReader = new NamedPipeStreamReader(this.stream); - } - - [Test] - public void CanWriteAndReadMessages() - { - string firstMessage = @"This is a new message"; - this.TestTransmitMessage(firstMessage); - - string secondMessage = @"This is another message"; - this.TestTransmitMessage(secondMessage); - - string thirdMessage = @"This is the third message in a series of messages"; - this.TestTransmitMessage(thirdMessage); - - string longMessage = new string('T', 1024 * 5); - this.TestTransmitMessage(longMessage); - } - - [Test] - [Category(CategoryConstants.ExceptionExpected)] - public void ReadingPartialMessgeThrows() - { - byte[] bytes = System.Text.Encoding.ASCII.GetBytes("This is a partial message"); - - this.stream.Write(bytes, 0, bytes.Length); - this.stream.Seek(0, SeekOrigin.Begin); - - Assert.Throws(() => this.streamReader.ReadMessage()); - } - - [Test] - public void CanSendMessagesWithNewLines() - { - string messageWithNewLines = "This is a \nstringwith\nnewlines"; - this.TestTransmitMessage(messageWithNewLines); - } - - [Test] - public void CanSendMultipleMessagesSequentially() - { - string[] messages = new string[] - { - "This is a new message", - "This is another message", - "This is the third message in a series of messages" - }; - - this.TestTransmitMessages(messages); - } - - private void TestTransmitMessage(string message) - { - long pos = this.ReadStreamPosition(); - this.streamWriter.WriteMessage(message); - - this.SetStreamPosition(pos); - - string readMessage = this.streamReader.ReadMessage(); - readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent."); - } - - private void TestTransmitMessages(string[] messages) - { - long pos = this.ReadStreamPosition(); - - foreach (string message in messages) - { - this.streamWriter.WriteMessage(message); - } - - this.SetStreamPosition(pos); - - foreach (string message in messages) - { - string readMessage = this.streamReader.ReadMessage(); - readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent."); - } - } - - private long ReadStreamPosition() - { - return this.stream.Position; - } - - private void SetStreamPosition(long position) - { - this.stream.Seek(position, SeekOrigin.Begin); - } - } -} +using NUnit.Framework; +using Scalar.Common.NamedPipes; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using System.IO; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class NamedPipeStreamReaderWriterTests + { + private MemoryStream stream; + private NamedPipeStreamWriter streamWriter; + private NamedPipeStreamReader streamReader; + + [SetUp] + public void Setup() + { + this.stream = new MemoryStream(); + this.streamWriter = new NamedPipeStreamWriter(this.stream); + this.streamReader = new NamedPipeStreamReader(this.stream); + } + + [Test] + public void CanWriteAndReadMessages() + { + string firstMessage = @"This is a new message"; + this.TestTransmitMessage(firstMessage); + + string secondMessage = @"This is another message"; + this.TestTransmitMessage(secondMessage); + + string thirdMessage = @"This is the third message in a series of messages"; + this.TestTransmitMessage(thirdMessage); + + string longMessage = new string('T', 1024 * 5); + this.TestTransmitMessage(longMessage); + } + + [Test] + [Category(CategoryConstants.ExceptionExpected)] + public void ReadingPartialMessgeThrows() + { + byte[] bytes = System.Text.Encoding.ASCII.GetBytes("This is a partial message"); + + this.stream.Write(bytes, 0, bytes.Length); + this.stream.Seek(0, SeekOrigin.Begin); + + Assert.Throws(() => this.streamReader.ReadMessage()); + } + + [Test] + public void CanSendMessagesWithNewLines() + { + string messageWithNewLines = "This is a \nstringwith\nnewlines"; + this.TestTransmitMessage(messageWithNewLines); + } + + [Test] + public void CanSendMultipleMessagesSequentially() + { + string[] messages = new string[] + { + "This is a new message", + "This is another message", + "This is the third message in a series of messages" + }; + + this.TestTransmitMessages(messages); + } + + private void TestTransmitMessage(string message) + { + long pos = this.ReadStreamPosition(); + this.streamWriter.WriteMessage(message); + + this.SetStreamPosition(pos); + + string readMessage = this.streamReader.ReadMessage(); + readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent."); + } + + private void TestTransmitMessages(string[] messages) + { + long pos = this.ReadStreamPosition(); + + foreach (string message in messages) + { + this.streamWriter.WriteMessage(message); + } + + this.SetStreamPosition(pos); + + foreach (string message in messages) + { + string readMessage = this.streamReader.ReadMessage(); + readMessage.ShouldEqual(message, "The message read from the stream reader is not the same as the message that was sent."); + } + } + + private long ReadStreamPosition() + { + return this.stream.Position; + } + + private void SetStreamPosition(long position) + { + this.stream.Seek(position, SeekOrigin.Begin); + } + } +} diff --git a/Scalar.UnitTests/Common/NuGetUpgrade/NuGetUpgraderTests.cs b/Scalar.UnitTests/Common/NuGetUpgrade/NuGetUpgraderTests.cs index b826600db6..8d50ee5539 100644 --- a/Scalar.UnitTests/Common/NuGetUpgrade/NuGetUpgraderTests.cs +++ b/Scalar.UnitTests/Common/NuGetUpgrade/NuGetUpgraderTests.cs @@ -1,455 +1,455 @@ -using Moq; -using NuGet.Packaging.Core; -using NuGet.Protocol.Core.Types; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.NuGetUpgrade; -using Scalar.Common.Tracing; -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.NuGetUpgrade -{ - [TestFixture] - public class NuGetUpgraderTests - { - protected const string OlderVersion = "1.0.1185.0"; - protected const string CurrentVersion = "1.5.1185.0"; - protected const string NewerVersion = "1.6.1185.0"; - protected const string NewerVersion2 = "1.7.1185.0"; - - protected const string NuGetFeedUrl = "https://pkgs.dev.azure.com/contoso/packages"; - protected const string NuGetFeedName = "feedNameValue"; - - protected static Exception httpRequestAuthException = new System.Net.Http.HttpRequestException("Response status code does not indicate success: 401 (Unauthorized)."); - protected static Exception fatalProtocolAuthException = new FatalProtocolException("Unable to load the service index for source.", httpRequestAuthException); - - protected static Exception[] networkAuthFailures = - { - httpRequestAuthException, - fatalProtocolAuthException - }; - - protected NuGetUpgrader upgrader; - protected MockTracer tracer; - - protected NuGetUpgrader.NuGetUpgraderConfig upgraderConfig; - - protected Mock mockNuGetFeed; - protected MockFileSystem mockFileSystem; - protected Mock mockCredentialManager; - protected ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; - - protected string downloadDirectoryPath = Path.Combine( - $"mock:{Path.DirectorySeparatorChar}", - ProductUpgraderInfo.UpgradeDirectoryName, - ProductUpgraderInfo.DownloadDirectory); - - protected delegate void DownloadPackageAsyncCallback(PackageIdentity packageIdentity); - - public virtual ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformStrategy() - { - return new MockProductUpgraderPlatformStrategy(this.mockFileSystem, this.tracer); - } - - [SetUp] - public void SetUp() - { - this.upgraderConfig = new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); - - this.tracer = new MockTracer(); - - this.mockNuGetFeed = new Mock( - NuGetFeedUrl, - NuGetFeedName, - this.downloadDirectoryPath, - null, - ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, - this.tracer); - this.mockNuGetFeed.Setup(feed => feed.SetCredentials(It.IsAny())); - - this.mockFileSystem = new MockFileSystem( - new MockDirectory( - Path.GetDirectoryName(this.downloadDirectoryPath), - new[] { new MockDirectory(this.downloadDirectoryPath, null, null) }, - null)); - - this.mockCredentialManager = new Mock(); - string credentialManagerString = "value"; - string emptyString = string.Empty; - this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny(), It.IsAny(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true); - - this.productUpgraderPlatformStrategy = this.CreateProductUpgraderPlatformStrategy(); - - this.upgrader = new NuGetUpgrader( - CurrentVersion, - this.tracer, - false, - false, - this.mockFileSystem, - this.upgraderConfig, - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object, - this.productUpgraderPlatformStrategy); - } - - [TearDown] - public void TearDown() - { - this.mockNuGetFeed.Object.Dispose(); - this.tracer.Dispose(); - } - - [TestCase] - public void TryQueryNewestVersion_NewVersionAvailable() - { - Version newVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - // Assert that we found the newer version - success.ShouldBeTrue(); - newVersion.ShouldNotBeNull(); - newVersion.ShouldEqual(new Version(NewerVersion)); - message.ShouldNotBeNull(); - } - - [TestCase] - public void TryQueryNewestVersion_MultipleNewVersionsAvailable() - { - Version newVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion2)), - }; - - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - // Assert that we found the newest version - success.ShouldBeTrue(); - newVersion.ShouldNotBeNull(); - newVersion.ShouldEqual(new Version(NewerVersion2)); - message.ShouldNotBeNull(); - } - - [TestCase] - public void TryQueryNewestVersion_NoNewerVersionsAvailable() - { - Version newVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(OlderVersion)), - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - }; - - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - // Assert that no new version was returned - success.ShouldBeTrue(); - newVersion.ShouldBeNull(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryQueryNewestVersion_Exception() - { - Version newVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(OlderVersion)), - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - }; - - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).Throws(new Exception("Network Error")); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - // Assert that no new version was returned - success.ShouldBeFalse(); - newVersion.ShouldBeNull(); - message.ShouldNotBeNull(); - message.Any().ShouldBeTrue(); - } - - [TestCase] - public void CanDownloadNewestVersion() - { - Version actualNewestVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); - this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(true); - - bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); - - // Assert that no new version was returned - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); - - bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); - downloadSuccessful.ShouldBeTrue(); - this.upgrader.DownloadedPackagePath.ShouldEqual(testDownloadPath); - this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(It.IsAny()), Times.Once()); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void DownloadNewestVersion_HandleException() - { - Version newVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.IsAny())).Throws(new Exception("Network Error")); - this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(true); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - newVersion.ShouldNotBeNull(); - - bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); - downloadSuccessful.ShouldBeFalse(); - } - - [TestCase] - public void AttemptingToDownloadBeforeQueryingFails() - { - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - - string downloadPath = "c:\\test_download_path"; - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(downloadPath); - - bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); - downloadSuccessful.ShouldBeFalse(); - } - - [TestCase] - public void TestUpgradeAllowed() - { - // Properly Configured NuGet config - NuGetUpgrader.NuGetUpgraderConfig nuGetUpgraderConfig = - new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); - - NuGetUpgrader nuGetUpgrader = new NuGetUpgrader( - CurrentVersion, - this.tracer, - false, - false, - this.mockFileSystem, - nuGetUpgraderConfig, - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object, - this.productUpgraderPlatformStrategy); - - nuGetUpgrader.UpgradeAllowed(out _).ShouldBeTrue("NuGetUpgrader config is complete: upgrade should be allowed."); - - // Empty FeedURL - nuGetUpgraderConfig = - new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, string.Empty, NuGetFeedName); - - nuGetUpgrader = new NuGetUpgrader( - CurrentVersion, - this.tracer, - false, - false, - this.mockFileSystem, - nuGetUpgraderConfig, - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object, - this.productUpgraderPlatformStrategy); - - nuGetUpgrader.UpgradeAllowed(out string _).ShouldBeFalse("Upgrade without FeedURL configured should not be allowed."); - - // Empty packageFeedName - nuGetUpgraderConfig = - new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, string.Empty); - - // Empty packageFeedName - nuGetUpgrader = new NuGetUpgrader( - CurrentVersion, - this.tracer, - false, - false, - this.mockFileSystem, - nuGetUpgraderConfig, - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object, - this.productUpgraderPlatformStrategy); - - nuGetUpgrader.UpgradeAllowed(out string _).ShouldBeFalse("Upgrade without FeedName configured should not be allowed."); - } - - [TestCaseSource("networkAuthFailures")] - public void QueryNewestVersionReacquiresCredentialsOnAuthFailure(Exception exception) - { - Version actualNewestVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - this.mockNuGetFeed.SetupSequence(foo => foo.QueryFeedAsync(It.IsAny())) - .Throws(exception) - .ReturnsAsync(availablePackages); - - // Setup the credential manager - string emptyString = string.Empty; - this.mockCredentialManager.Setup(foo => foo.TryDeleteCredential(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out emptyString)).Returns(true); - - bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); - - // Verify expectations - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); - - this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.QueryFeedAsync(It.IsAny()), Times.Exactly(2)); - - string outString = string.Empty; - this.mockCredentialManager.Verify(credentialManager => credentialManager.TryGetCredential(It.IsAny(), It.IsAny(), out outString, out outString, out outString), Times.Exactly(2)); - this.mockCredentialManager.Verify(credentialManager => credentialManager.TryDeleteCredential(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out outString), Times.Exactly(1)); - } - - [TestCase] - public void WellKnownArgumentTokensReplaced() - { - string logDirectory = "mock:\\test_log_directory"; - string noTokenSourceString = "/arg no_token log_directory installation_id"; - NuGetUpgrader.ReplaceArgTokens(noTokenSourceString, "unique_id", logDirectory, "installerBase").ShouldEqual(noTokenSourceString, "String with no tokens should not be modifed"); - - string sourceStringWithTokens = "/arg /log {log_directory}_{installation_id}_{installer_base_path}"; - string expectedProcessedString = "/arg /log " + logDirectory + "_unique_id_installerBase"; - NuGetUpgrader.ReplaceArgTokens(sourceStringWithTokens, "unique_id", logDirectory, "installerBase").ShouldEqual(expectedProcessedString, "expected tokens have not been replaced"); - } - - [TestCase] - public void DownloadFailsOnNuGetPackageVerificationFailure() - { - Version actualNewestVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - - string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))) - .Callback(new DownloadPackageAsyncCallback( - (packageIdentity) => this.mockFileSystem.WriteAllText(testDownloadPath, "Package contents that will fail validation"))) - .ReturnsAsync(testDownloadPath); - this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(false); - - bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); - - bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); - this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(this.upgrader.DownloadedPackagePath), Times.Once()); - downloadSuccessful.ShouldBeFalse("Failure to verify NuGet package should cause download to fail."); - this.mockFileSystem.FileExists(testDownloadPath).ShouldBeFalse("VerifyPackage should delete invalid packages"); - } - - [TestCase] - public void DoNotVerifyNuGetPackageWhenNoVerifyIsSpecified() - { - NuGetUpgrader.NuGetUpgraderConfig nuGetUpgraderConfig = - new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); - - NuGetUpgrader nuGetUpgrader = new NuGetUpgrader( - CurrentVersion, - this.tracer, - false, - true, - this.mockFileSystem, - nuGetUpgraderConfig, - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object, - this.productUpgraderPlatformStrategy); - - Version actualNewestVersion; - string message; - List availablePackages = new List() - { - this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), - this.GeneratePackageSeachMetadata(new Version(NewerVersion)), - }; - - IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); - - string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); - this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); - this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); - this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(false); - - bool success = nuGetUpgrader.TryQueryNewestVersion(out actualNewestVersion, out message); - success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); - actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); - - bool downloadSuccessful = nuGetUpgrader.TryDownloadNewestVersion(out message); - this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(It.IsAny()), Times.Never()); - downloadSuccessful.ShouldBeTrue("Should be able to download package with verification issues when noVerify is specified"); - } - - protected IPackageSearchMetadata GeneratePackageSeachMetadata(Version version) - { - Mock mockPackageSearchMetaData = new Mock(); - NuGet.Versioning.NuGetVersion nuGetVersion = new NuGet.Versioning.NuGetVersion(version); - mockPackageSearchMetaData.Setup(foo => foo.Identity).Returns(new NuGet.Packaging.Core.PackageIdentity("generatedPackedId", nuGetVersion)); - - return mockPackageSearchMetaData.Object; - } - } -} +using Moq; +using NuGet.Packaging.Core; +using NuGet.Protocol.Core.Types; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.NuGetUpgrade; +using Scalar.Common.Tracing; +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.NuGetUpgrade +{ + [TestFixture] + public class NuGetUpgraderTests + { + protected const string OlderVersion = "1.0.1185.0"; + protected const string CurrentVersion = "1.5.1185.0"; + protected const string NewerVersion = "1.6.1185.0"; + protected const string NewerVersion2 = "1.7.1185.0"; + + protected const string NuGetFeedUrl = "https://pkgs.dev.azure.com/contoso/packages"; + protected const string NuGetFeedName = "feedNameValue"; + + protected static Exception httpRequestAuthException = new System.Net.Http.HttpRequestException("Response status code does not indicate success: 401 (Unauthorized)."); + protected static Exception fatalProtocolAuthException = new FatalProtocolException("Unable to load the service index for source.", httpRequestAuthException); + + protected static Exception[] networkAuthFailures = + { + httpRequestAuthException, + fatalProtocolAuthException + }; + + protected NuGetUpgrader upgrader; + protected MockTracer tracer; + + protected NuGetUpgrader.NuGetUpgraderConfig upgraderConfig; + + protected Mock mockNuGetFeed; + protected MockFileSystem mockFileSystem; + protected Mock mockCredentialManager; + protected ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; + + protected string downloadDirectoryPath = Path.Combine( + $"mock:{Path.DirectorySeparatorChar}", + ProductUpgraderInfo.UpgradeDirectoryName, + ProductUpgraderInfo.DownloadDirectory); + + protected delegate void DownloadPackageAsyncCallback(PackageIdentity packageIdentity); + + public virtual ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformStrategy() + { + return new MockProductUpgraderPlatformStrategy(this.mockFileSystem, this.tracer); + } + + [SetUp] + public void SetUp() + { + this.upgraderConfig = new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); + + this.tracer = new MockTracer(); + + this.mockNuGetFeed = new Mock( + NuGetFeedUrl, + NuGetFeedName, + this.downloadDirectoryPath, + null, + ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, + this.tracer); + this.mockNuGetFeed.Setup(feed => feed.SetCredentials(It.IsAny())); + + this.mockFileSystem = new MockFileSystem( + new MockDirectory( + Path.GetDirectoryName(this.downloadDirectoryPath), + new[] { new MockDirectory(this.downloadDirectoryPath, null, null) }, + null)); + + this.mockCredentialManager = new Mock(); + string credentialManagerString = "value"; + string emptyString = string.Empty; + this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny(), It.IsAny(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true); + + this.productUpgraderPlatformStrategy = this.CreateProductUpgraderPlatformStrategy(); + + this.upgrader = new NuGetUpgrader( + CurrentVersion, + this.tracer, + false, + false, + this.mockFileSystem, + this.upgraderConfig, + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object, + this.productUpgraderPlatformStrategy); + } + + [TearDown] + public void TearDown() + { + this.mockNuGetFeed.Object.Dispose(); + this.tracer.Dispose(); + } + + [TestCase] + public void TryQueryNewestVersion_NewVersionAvailable() + { + Version newVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + // Assert that we found the newer version + success.ShouldBeTrue(); + newVersion.ShouldNotBeNull(); + newVersion.ShouldEqual(new Version(NewerVersion)); + message.ShouldNotBeNull(); + } + + [TestCase] + public void TryQueryNewestVersion_MultipleNewVersionsAvailable() + { + Version newVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion2)), + }; + + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + // Assert that we found the newest version + success.ShouldBeTrue(); + newVersion.ShouldNotBeNull(); + newVersion.ShouldEqual(new Version(NewerVersion2)); + message.ShouldNotBeNull(); + } + + [TestCase] + public void TryQueryNewestVersion_NoNewerVersionsAvailable() + { + Version newVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(OlderVersion)), + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + }; + + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + // Assert that no new version was returned + success.ShouldBeTrue(); + newVersion.ShouldBeNull(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryQueryNewestVersion_Exception() + { + Version newVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(OlderVersion)), + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + }; + + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).Throws(new Exception("Network Error")); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + // Assert that no new version was returned + success.ShouldBeFalse(); + newVersion.ShouldBeNull(); + message.ShouldNotBeNull(); + message.Any().ShouldBeTrue(); + } + + [TestCase] + public void CanDownloadNewestVersion() + { + Version actualNewestVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); + this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(true); + + bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); + + // Assert that no new version was returned + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); + + bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); + downloadSuccessful.ShouldBeTrue(); + this.upgrader.DownloadedPackagePath.ShouldEqual(testDownloadPath); + this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(It.IsAny()), Times.Once()); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void DownloadNewestVersion_HandleException() + { + Version newVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(It.IsAny())).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.IsAny())).Throws(new Exception("Network Error")); + this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(true); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + newVersion.ShouldNotBeNull(); + + bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); + downloadSuccessful.ShouldBeFalse(); + } + + [TestCase] + public void AttemptingToDownloadBeforeQueryingFails() + { + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + + string downloadPath = "c:\\test_download_path"; + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(downloadPath); + + bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); + downloadSuccessful.ShouldBeFalse(); + } + + [TestCase] + public void TestUpgradeAllowed() + { + // Properly Configured NuGet config + NuGetUpgrader.NuGetUpgraderConfig nuGetUpgraderConfig = + new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); + + NuGetUpgrader nuGetUpgrader = new NuGetUpgrader( + CurrentVersion, + this.tracer, + false, + false, + this.mockFileSystem, + nuGetUpgraderConfig, + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object, + this.productUpgraderPlatformStrategy); + + nuGetUpgrader.UpgradeAllowed(out _).ShouldBeTrue("NuGetUpgrader config is complete: upgrade should be allowed."); + + // Empty FeedURL + nuGetUpgraderConfig = + new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, string.Empty, NuGetFeedName); + + nuGetUpgrader = new NuGetUpgrader( + CurrentVersion, + this.tracer, + false, + false, + this.mockFileSystem, + nuGetUpgraderConfig, + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object, + this.productUpgraderPlatformStrategy); + + nuGetUpgrader.UpgradeAllowed(out string _).ShouldBeFalse("Upgrade without FeedURL configured should not be allowed."); + + // Empty packageFeedName + nuGetUpgraderConfig = + new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, string.Empty); + + // Empty packageFeedName + nuGetUpgrader = new NuGetUpgrader( + CurrentVersion, + this.tracer, + false, + false, + this.mockFileSystem, + nuGetUpgraderConfig, + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object, + this.productUpgraderPlatformStrategy); + + nuGetUpgrader.UpgradeAllowed(out string _).ShouldBeFalse("Upgrade without FeedName configured should not be allowed."); + } + + [TestCaseSource("networkAuthFailures")] + public void QueryNewestVersionReacquiresCredentialsOnAuthFailure(Exception exception) + { + Version actualNewestVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + this.mockNuGetFeed.SetupSequence(foo => foo.QueryFeedAsync(It.IsAny())) + .Throws(exception) + .ReturnsAsync(availablePackages); + + // Setup the credential manager + string emptyString = string.Empty; + this.mockCredentialManager.Setup(foo => foo.TryDeleteCredential(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out emptyString)).Returns(true); + + bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); + + // Verify expectations + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); + + this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.QueryFeedAsync(It.IsAny()), Times.Exactly(2)); + + string outString = string.Empty; + this.mockCredentialManager.Verify(credentialManager => credentialManager.TryGetCredential(It.IsAny(), It.IsAny(), out outString, out outString, out outString), Times.Exactly(2)); + this.mockCredentialManager.Verify(credentialManager => credentialManager.TryDeleteCredential(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), out outString), Times.Exactly(1)); + } + + [TestCase] + public void WellKnownArgumentTokensReplaced() + { + string logDirectory = "mock:\\test_log_directory"; + string noTokenSourceString = "/arg no_token log_directory installation_id"; + NuGetUpgrader.ReplaceArgTokens(noTokenSourceString, "unique_id", logDirectory, "installerBase").ShouldEqual(noTokenSourceString, "String with no tokens should not be modifed"); + + string sourceStringWithTokens = "/arg /log {log_directory}_{installation_id}_{installer_base_path}"; + string expectedProcessedString = "/arg /log " + logDirectory + "_unique_id_installerBase"; + NuGetUpgrader.ReplaceArgTokens(sourceStringWithTokens, "unique_id", logDirectory, "installerBase").ShouldEqual(expectedProcessedString, "expected tokens have not been replaced"); + } + + [TestCase] + public void DownloadFailsOnNuGetPackageVerificationFailure() + { + Version actualNewestVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + + string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))) + .Callback(new DownloadPackageAsyncCallback( + (packageIdentity) => this.mockFileSystem.WriteAllText(testDownloadPath, "Package contents that will fail validation"))) + .ReturnsAsync(testDownloadPath); + this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(false); + + bool success = this.upgrader.TryQueryNewestVersion(out actualNewestVersion, out message); + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); + + bool downloadSuccessful = this.upgrader.TryDownloadNewestVersion(out message); + this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(this.upgrader.DownloadedPackagePath), Times.Once()); + downloadSuccessful.ShouldBeFalse("Failure to verify NuGet package should cause download to fail."); + this.mockFileSystem.FileExists(testDownloadPath).ShouldBeFalse("VerifyPackage should delete invalid packages"); + } + + [TestCase] + public void DoNotVerifyNuGetPackageWhenNoVerifyIsSpecified() + { + NuGetUpgrader.NuGetUpgraderConfig nuGetUpgraderConfig = + new NuGetUpgrader.NuGetUpgraderConfig(this.tracer, null, NuGetFeedUrl, NuGetFeedName); + + NuGetUpgrader nuGetUpgrader = new NuGetUpgrader( + CurrentVersion, + this.tracer, + false, + true, + this.mockFileSystem, + nuGetUpgraderConfig, + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object, + this.productUpgraderPlatformStrategy); + + Version actualNewestVersion; + string message; + List availablePackages = new List() + { + this.GeneratePackageSeachMetadata(new Version(CurrentVersion)), + this.GeneratePackageSeachMetadata(new Version(NewerVersion)), + }; + + IPackageSearchMetadata newestAvailableVersion = availablePackages.Last(); + + string testDownloadPath = Path.Combine(this.downloadDirectoryPath, "testNuget.zip"); + this.mockNuGetFeed.Setup(foo => foo.QueryFeedAsync(NuGetFeedName)).ReturnsAsync(availablePackages); + this.mockNuGetFeed.Setup(foo => foo.DownloadPackageAsync(It.Is(packageIdentity => packageIdentity == newestAvailableVersion.Identity))).ReturnsAsync(testDownloadPath); + this.mockNuGetFeed.Setup(foo => foo.VerifyPackage(It.IsAny())).Returns(false); + + bool success = nuGetUpgrader.TryQueryNewestVersion(out actualNewestVersion, out message); + success.ShouldBeTrue($"Expecting TryQueryNewestVersion to have completed sucessfully. Error: {message}"); + actualNewestVersion.ShouldEqual(newestAvailableVersion.Identity.Version.Version, "Actual new version does not match expected new version."); + + bool downloadSuccessful = nuGetUpgrader.TryDownloadNewestVersion(out message); + this.mockNuGetFeed.Verify(nuGetFeed => nuGetFeed.VerifyPackage(It.IsAny()), Times.Never()); + downloadSuccessful.ShouldBeTrue("Should be able to download package with verification issues when noVerify is specified"); + } + + protected IPackageSearchMetadata GeneratePackageSeachMetadata(Version version) + { + Mock mockPackageSearchMetaData = new Mock(); + NuGet.Versioning.NuGetVersion nuGetVersion = new NuGet.Versioning.NuGetVersion(version); + mockPackageSearchMetaData.Setup(foo => foo.Identity).Returns(new NuGet.Packaging.Core.PackageIdentity("generatedPackedId", nuGetVersion)); + + return mockPackageSearchMetaData.Object; + } + } +} diff --git a/Scalar.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs b/Scalar.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs index 95596aa89a..6dd1c4edc0 100644 --- a/Scalar.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs +++ b/Scalar.UnitTests/Common/NuGetUpgrade/OrgNuGetUpgraderTests.cs @@ -1,188 +1,188 @@ -using Moq; -using Moq.Protected; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.NuGetUpgrade; -using Scalar.Common.Tracing; -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.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.UnitTests.Common.NuGetUpgrade -{ - [TestFixture] - public class OrgNuGetUpgraderTests - { - private const string CurrentVersion = "1.5.1185.0"; - private const string NewerVersion = "1.6.1185.0"; - - private const string DefaultUpgradeFeedPackageName = "package"; - private const string DefaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/"; - private const string DefaultOrgInfoServerUrl = "https://www.contoso.com"; - private const string DefaultRing = "slow"; - - private OrgNuGetUpgrader upgrader; - - private MockTracer tracer; - - private OrgNuGetUpgrader.OrgNuGetUpgraderConfig upgraderConfig; - - private Mock mockNuGetFeed; - private MockFileSystem mockFileSystem; - private Mock mockCredentialManager; - private Mock httpMessageHandlerMock; - - private string downloadDirectoryPath = Path.Combine( - $"mock:{Path.DirectorySeparatorChar}", - ProductUpgraderInfo.UpgradeDirectoryName, - ProductUpgraderInfo.DownloadDirectory); - - private interface IHttpMessageHandlerProtectedMembers - { - Task SendAsync(HttpRequestMessage message, CancellationToken token); - } - - public static IEnumerable NetworkFailureCases() - { - yield return new HttpRequestException("Response status code does not indicate success: 401: (Unauthorized)"); - yield return new TaskCanceledException("Task canceled"); - } - - [SetUp] - public void SetUp() - { - MockLocalScalarConfig mockGvfsConfig = new MockLocalScalarConfigBuilder( - DefaultRing, - DefaultUpgradeFeedUrl, - DefaultUpgradeFeedPackageName, - DefaultOrgInfoServerUrl) - .WithUpgradeRing() - .WithUpgradeFeedPackageName() - .WithUpgradeFeedUrl() - .WithOrgInfoServerUrl() - .Build(); - - this.upgraderConfig = new OrgNuGetUpgrader.OrgNuGetUpgraderConfig(this.tracer, mockGvfsConfig); - this.upgraderConfig.TryLoad(out _); - - this.tracer = new MockTracer(); - - this.mockNuGetFeed = new Mock( - DefaultUpgradeFeedUrl, - DefaultUpgradeFeedPackageName, - this.downloadDirectoryPath, - null, - ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, - this.tracer); - - this.mockFileSystem = new MockFileSystem( - new MockDirectory( - Path.GetDirectoryName(this.downloadDirectoryPath), - new[] { new MockDirectory(this.downloadDirectoryPath, null, null) }, - null)); - - this.mockCredentialManager = new Mock(); - string credentialManagerString = "value"; - string emptyString = string.Empty; - this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny(), It.IsAny(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true); - - this.httpMessageHandlerMock = new Mock(); - - this.httpMessageHandlerMock.Protected().As() - .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(this.ConstructResponseContent(NewerVersion)) - }); - - HttpClient httpClient = new HttpClient(this.httpMessageHandlerMock.Object); - - this.upgrader = new OrgNuGetUpgrader( - CurrentVersion, - this.tracer, - this.mockFileSystem, - httpClient, - false, - false, - this.upgraderConfig, - "windows", - this.mockNuGetFeed.Object, - this.mockCredentialManager.Object); - } - - [TestCase] - public void SupportsAnonymousQuery() - { - this.upgrader.SupportsAnonymousVersionQuery.ShouldBeTrue(); - } - - [TestCase] - public void TryQueryNewestVersion() - { - Version newVersion; - string message; - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - success.ShouldBeTrue(); - newVersion.ShouldNotBeNull(); - newVersion.ShouldEqual(new Version(NewerVersion)); - message.ShouldNotBeNull(); - message.ShouldEqual($"New version {OrgNuGetUpgraderTests.NewerVersion} is available."); - } - - [TestCaseSource("NetworkFailureCases")] - public void HandlesNetworkErrors(Exception ex) - { - Version newVersion; - string message; - - this.httpMessageHandlerMock.Protected().As() - .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) - .ThrowsAsync(ex); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - success.ShouldBeFalse(); - newVersion.ShouldBeNull(); - message.ShouldNotBeNull(); - message.ShouldContain("Network error"); - } - - [TestCase] - public void HandlesEmptyVersion() - { - Version newVersion; - string message; - - this.httpMessageHandlerMock.Protected().As() - .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) - .ReturnsAsync(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(this.ConstructResponseContent(string.Empty)) - }); - - bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); - - success.ShouldBeTrue(); - newVersion.ShouldBeNull(); - message.ShouldNotBeNull(); - message.ShouldContain("No versions available"); - } - - private string ConstructResponseContent(string version) - { - return $"{{\"version\" : \"{version}\"}} "; - } - } -} +using Moq; +using Moq.Protected; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.NuGetUpgrade; +using Scalar.Common.Tracing; +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.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.UnitTests.Common.NuGetUpgrade +{ + [TestFixture] + public class OrgNuGetUpgraderTests + { + private const string CurrentVersion = "1.5.1185.0"; + private const string NewerVersion = "1.6.1185.0"; + + private const string DefaultUpgradeFeedPackageName = "package"; + private const string DefaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/"; + private const string DefaultOrgInfoServerUrl = "https://www.contoso.com"; + private const string DefaultRing = "slow"; + + private OrgNuGetUpgrader upgrader; + + private MockTracer tracer; + + private OrgNuGetUpgrader.OrgNuGetUpgraderConfig upgraderConfig; + + private Mock mockNuGetFeed; + private MockFileSystem mockFileSystem; + private Mock mockCredentialManager; + private Mock httpMessageHandlerMock; + + private string downloadDirectoryPath = Path.Combine( + $"mock:{Path.DirectorySeparatorChar}", + ProductUpgraderInfo.UpgradeDirectoryName, + ProductUpgraderInfo.DownloadDirectory); + + private interface IHttpMessageHandlerProtectedMembers + { + Task SendAsync(HttpRequestMessage message, CancellationToken token); + } + + public static IEnumerable NetworkFailureCases() + { + yield return new HttpRequestException("Response status code does not indicate success: 401: (Unauthorized)"); + yield return new TaskCanceledException("Task canceled"); + } + + [SetUp] + public void SetUp() + { + MockLocalScalarConfig mockGvfsConfig = new MockLocalScalarConfigBuilder( + DefaultRing, + DefaultUpgradeFeedUrl, + DefaultUpgradeFeedPackageName, + DefaultOrgInfoServerUrl) + .WithUpgradeRing() + .WithUpgradeFeedPackageName() + .WithUpgradeFeedUrl() + .WithOrgInfoServerUrl() + .Build(); + + this.upgraderConfig = new OrgNuGetUpgrader.OrgNuGetUpgraderConfig(this.tracer, mockGvfsConfig); + this.upgraderConfig.TryLoad(out _); + + this.tracer = new MockTracer(); + + this.mockNuGetFeed = new Mock( + DefaultUpgradeFeedUrl, + DefaultUpgradeFeedPackageName, + this.downloadDirectoryPath, + null, + ScalarPlatform.Instance.UnderConstruction.SupportsNuGetEncryption, + this.tracer); + + this.mockFileSystem = new MockFileSystem( + new MockDirectory( + Path.GetDirectoryName(this.downloadDirectoryPath), + new[] { new MockDirectory(this.downloadDirectoryPath, null, null) }, + null)); + + this.mockCredentialManager = new Mock(); + string credentialManagerString = "value"; + string emptyString = string.Empty; + this.mockCredentialManager.Setup(foo => foo.TryGetCredential(It.IsAny(), It.IsAny(), out credentialManagerString, out credentialManagerString, out credentialManagerString)).Returns(true); + + this.httpMessageHandlerMock = new Mock(); + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(this.ConstructResponseContent(NewerVersion)) + }); + + HttpClient httpClient = new HttpClient(this.httpMessageHandlerMock.Object); + + this.upgrader = new OrgNuGetUpgrader( + CurrentVersion, + this.tracer, + this.mockFileSystem, + httpClient, + false, + false, + this.upgraderConfig, + "windows", + this.mockNuGetFeed.Object, + this.mockCredentialManager.Object); + } + + [TestCase] + public void SupportsAnonymousQuery() + { + this.upgrader.SupportsAnonymousVersionQuery.ShouldBeTrue(); + } + + [TestCase] + public void TryQueryNewestVersion() + { + Version newVersion; + string message; + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeTrue(); + newVersion.ShouldNotBeNull(); + newVersion.ShouldEqual(new Version(NewerVersion)); + message.ShouldNotBeNull(); + message.ShouldEqual($"New version {OrgNuGetUpgraderTests.NewerVersion} is available."); + } + + [TestCaseSource("NetworkFailureCases")] + public void HandlesNetworkErrors(Exception ex) + { + Version newVersion; + string message; + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(ex); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeFalse(); + newVersion.ShouldBeNull(); + message.ShouldNotBeNull(); + message.ShouldContain("Network error"); + } + + [TestCase] + public void HandlesEmptyVersion() + { + Version newVersion; + string message; + + this.httpMessageHandlerMock.Protected().As() + .Setup(m => m.SendAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(this.ConstructResponseContent(string.Empty)) + }); + + bool success = this.upgrader.TryQueryNewestVersion(out newVersion, out message); + + success.ShouldBeTrue(); + newVersion.ShouldBeNull(); + message.ShouldNotBeNull(); + message.ShouldContain("No versions available"); + } + + private string ConstructResponseContent(string version) + { + return $"{{\"version\" : \"{version}\"}} "; + } + } +} diff --git a/Scalar.UnitTests/Common/OrgInfoApiClientTests.cs b/Scalar.UnitTests/Common/OrgInfoApiClientTests.cs index f695870e0a..8330fac62b 100644 --- a/Scalar.UnitTests/Common/OrgInfoApiClientTests.cs +++ b/Scalar.UnitTests/Common/OrgInfoApiClientTests.cs @@ -1,110 +1,110 @@ -using Moq; -using Moq.Protected; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class OrgInfoServerTests - { - public static List TestOrgInfo = new List() - { - new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "fast", Version = "1.2.3.1" }, - new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "slow", Version = "1.2.3.2" }, - new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "fast", Version = "1.2.3.3" }, - new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "slow", Version = "1.2.3.4" }, - new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "fast", Version = "1.2.3.5" }, - new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "slow", Version = "1.2.3.6" }, - new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "fast", Version = "1.2.3.7" }, - new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "slow", Version = "1.2.3.8" }, - }; - - private string baseUrl = "https://www.contoso.com"; - - private interface IHttpMessageHandlerProtectedMembers - { - Task SendAsync(HttpRequestMessage message, CancellationToken token); - } - - [TestCaseSource("TestOrgInfo")] - public void QueryNewestVersionWithParams(OrgInfo orgInfo) - { - Mock handlerMock = new Mock(MockBehavior.Strict); - - handlerMock.Protected().As() - .Setup(m => m.SendAsync(It.Is(request => this.UriMatches(request.RequestUri, this.baseUrl, orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring)), It.IsAny())) - .ReturnsAsync(new HttpResponseMessage() - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(this.ConstructResponseContent(orgInfo.Version)) - }); - - HttpClient httpClient = new HttpClient(handlerMock.Object); - - OrgInfoApiClient upgradeChecker = new OrgInfoApiClient(httpClient, this.baseUrl); - Version version = upgradeChecker.QueryNewestVersion(orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring); - - version.ShouldEqual(new Version(orgInfo.Version)); - - handlerMock.VerifyAll(); - } - - private bool UriMatches(Uri uri, string baseUrl, string expectedOrgName, string expectedPlatform, string expectedRing) - { - bool hostMatches = uri.Host.Equals(baseUrl); - - Dictionary queryParams = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (string param in uri.Query.Substring(1).Split('&')) - { - string[] fields = param.Split('='); - string key = fields[0]; - string value = fields[1]; - - queryParams.Add(key, value); - } - - if (queryParams.Count != 3) - { - return false; - } - - if (!queryParams.TryGetValue("Organization", out string orgName) || !string.Equals(orgName, expectedOrgName, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (!queryParams.TryGetValue("platform", out string platform) || !string.Equals(platform, expectedPlatform, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (!queryParams.TryGetValue("ring", out string ring) || !string.Equals(ring, expectedRing, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - return true; - } - - private string ConstructResponseContent(string version) - { - return $"{{\"version\" : \"{version}\"}} "; - } - - public class OrgInfo - { - public string OrgName { get; set; } - public string Ring { get; set; } - public string Platform { get; set; } - public string Version { get; set; } - } - } -} +using Moq; +using Moq.Protected; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class OrgInfoServerTests + { + public static List TestOrgInfo = new List() + { + new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "fast", Version = "1.2.3.1" }, + new OrgInfo() { OrgName = "org1", Platform = "windows", Ring = "slow", Version = "1.2.3.2" }, + new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "fast", Version = "1.2.3.3" }, + new OrgInfo() { OrgName = "org1", Platform = "macOS", Ring = "slow", Version = "1.2.3.4" }, + new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "fast", Version = "1.2.3.5" }, + new OrgInfo() { OrgName = "org2", Platform = "windows", Ring = "slow", Version = "1.2.3.6" }, + new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "fast", Version = "1.2.3.7" }, + new OrgInfo() { OrgName = "org2", Platform = "macOS", Ring = "slow", Version = "1.2.3.8" }, + }; + + private string baseUrl = "https://www.contoso.com"; + + private interface IHttpMessageHandlerProtectedMembers + { + Task SendAsync(HttpRequestMessage message, CancellationToken token); + } + + [TestCaseSource("TestOrgInfo")] + public void QueryNewestVersionWithParams(OrgInfo orgInfo) + { + Mock handlerMock = new Mock(MockBehavior.Strict); + + handlerMock.Protected().As() + .Setup(m => m.SendAsync(It.Is(request => this.UriMatches(request.RequestUri, this.baseUrl, orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring)), It.IsAny())) + .ReturnsAsync(new HttpResponseMessage() + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(this.ConstructResponseContent(orgInfo.Version)) + }); + + HttpClient httpClient = new HttpClient(handlerMock.Object); + + OrgInfoApiClient upgradeChecker = new OrgInfoApiClient(httpClient, this.baseUrl); + Version version = upgradeChecker.QueryNewestVersion(orgInfo.OrgName, orgInfo.Platform, orgInfo.Ring); + + version.ShouldEqual(new Version(orgInfo.Version)); + + handlerMock.VerifyAll(); + } + + private bool UriMatches(Uri uri, string baseUrl, string expectedOrgName, string expectedPlatform, string expectedRing) + { + bool hostMatches = uri.Host.Equals(baseUrl); + + Dictionary queryParams = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string param in uri.Query.Substring(1).Split('&')) + { + string[] fields = param.Split('='); + string key = fields[0]; + string value = fields[1]; + + queryParams.Add(key, value); + } + + if (queryParams.Count != 3) + { + return false; + } + + if (!queryParams.TryGetValue("Organization", out string orgName) || !string.Equals(orgName, expectedOrgName, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!queryParams.TryGetValue("platform", out string platform) || !string.Equals(platform, expectedPlatform, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!queryParams.TryGetValue("ring", out string ring) || !string.Equals(ring, expectedRing, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + return true; + } + + private string ConstructResponseContent(string version) + { + return $"{{\"version\" : \"{version}\"}} "; + } + + public class OrgInfo + { + public string OrgName { get; set; } + public string Ring { get; set; } + public string Platform { get; set; } + public string Version { get; set; } + } + } +} diff --git a/Scalar.UnitTests/Common/PathsTests.cs b/Scalar.UnitTests/Common/PathsTests.cs index 2c762e50af..70f9906fc6 100644 --- a/Scalar.UnitTests/Common/PathsTests.cs +++ b/Scalar.UnitTests/Common/PathsTests.cs @@ -1,35 +1,35 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System.Runtime.InteropServices; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class PathsTests - { - [TestCase] - public void CanConvertOSPathToGitFormat() - { - string systemPath; - string expectedGitPath; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - systemPath = @"C:\This\is\a\path"; - expectedGitPath = @"C:/This/is/a/path"; - } - else - { - systemPath = @"/This/is/a/path"; - expectedGitPath = systemPath; - } - - string actualTransformedPath = Paths.ConvertPathToGitFormat(systemPath); - actualTransformedPath.ShouldEqual(expectedGitPath); - - string doubleTransformedPath = Paths.ConvertPathToGitFormat(actualTransformedPath); - doubleTransformedPath.ShouldEqual(expectedGitPath); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System.Runtime.InteropServices; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class PathsTests + { + [TestCase] + public void CanConvertOSPathToGitFormat() + { + string systemPath; + string expectedGitPath; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + systemPath = @"C:\This\is\a\path"; + expectedGitPath = @"C:/This/is/a/path"; + } + else + { + systemPath = @"/This/is/a/path"; + expectedGitPath = systemPath; + } + + string actualTransformedPath = Paths.ConvertPathToGitFormat(systemPath); + actualTransformedPath.ShouldEqual(expectedGitPath); + + string doubleTransformedPath = Paths.ConvertPathToGitFormat(actualTransformedPath); + doubleTransformedPath.ShouldEqual(expectedGitPath); + } + } +} diff --git a/Scalar.UnitTests/Common/PhysicalFileSystemDeleteTests.cs b/Scalar.UnitTests/Common/PhysicalFileSystemDeleteTests.cs index d1f2fdc63d..24479d4c45 100644 --- a/Scalar.UnitTests/Common/PhysicalFileSystemDeleteTests.cs +++ b/Scalar.UnitTests/Common/PhysicalFileSystemDeleteTests.cs @@ -1,310 +1,310 @@ -using NUnit.Framework; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Common; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class PhysicalFileSystemDeleteTests - { - [TestCase] - public void TryDeleteFileDeletesFile() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - - fileSystem.TryDeleteFile(path); - fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file"); - } - - [TestCase] - public void TryDeleteFileSetsAttributesToNormalBeforeDeletingFile() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem( - new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }, - allFilesExist: false, - noOpDelete: true); - - fileSystem.TryDeleteFile(path); - fileSystem.ExistingFiles.ContainsKey(path).ShouldBeTrue("DeleteTestsFileSystem is configured as no-op delete, file should still be present"); - fileSystem.ExistingFiles[path].ShouldEqual(FileAttributes.Normal, "TryDeleteFile should set attributes to Normal before deleting"); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryDeleteFileReturnsTrueWhenSetAttributesFailsToFindFile() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem( - Enumerable.Empty>(), - allFilesExist: true, - noOpDelete: false); - - fileSystem.TryDeleteFile(path).ShouldEqual(true, "TryDeleteFile should return true when SetAttributes throws FileNotFoundException"); - } - - [TestCase] - public void TryDeleteFileReturnsNullExceptionOnSuccess() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - - Exception e = new Exception(); - fileSystem.TryDeleteFile(path, out e); - fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file"); - e.ShouldBeNull("Exception should be null when TryDeleteFile succeeds"); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryDeleteFileReturnsThrownException() - { - string path = "mock:\\file.txt"; - Exception deleteException = new IOException(); - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = deleteException; - - Exception e; - fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on IOException"); - ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception"); - - deleteException = new UnauthorizedAccessException(); - fileSystem.DeleteException = deleteException; - fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on UnauthorizedAccessException"); - ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception"); - } - - [TestCase] - public void TryDeleteFileDoesNotUpdateMetadataOnSuccess() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - - EventMetadata metadata = new EventMetadata(); - fileSystem.TryDeleteFile(path, "metadataKey", metadata).ShouldBeTrue("TryDeleteFile should succeed"); - metadata.ShouldBeEmpty("TryDeleteFile should not update metadata on success"); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryDeleteFileUpdatesMetadataOnFailure() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = new IOException(); - - EventMetadata metadata = new EventMetadata(); - fileSystem.TryDeleteFile(path, "testKey", metadata).ShouldBeFalse("TryDeleteFile should fail when IOException is thrown"); - metadata.ContainsKey("testKey_DeleteFailed").ShouldBeTrue(); - metadata["testKey_DeleteFailed"].ShouldEqual("true"); - metadata.ContainsKey("testKey_DeleteException").ShouldBeTrue(); - metadata["testKey_DeleteException"].ShouldBeOfType().ShouldContain("IOException"); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryWaitForDeleteSucceedsAfterFailures() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = new IOException(); - - fileSystem.MaxDeleteFileExceptions = 5; - fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue(); - fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1); - - fileSystem.ExistingFiles.Add(path, FileAttributes.ReadOnly); - fileSystem.DeleteFileCallCount = 0; - fileSystem.MaxDeleteFileExceptions = 9; - fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue(); - fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryWaitForDeleteFailsAfterMaxRetries() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = new IOException(); - - int maxRetries = 10; - fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse(); - fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); - - fileSystem.DeleteFileCallCount = 0; - fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse(); - fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); - - fileSystem.DeleteFileCallCount = 0; - fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: 0, retryLoggingThreshold: 1).ShouldBeFalse(); - fileSystem.DeleteFileCallCount.ShouldEqual(1); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryWaitForDeleteAlwaysLogsFirstAndLastFailure() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = new IOException(); - - MockTracer mockTracer = new MockTracer(); - int maxRetries = 10; - fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1000).ShouldBeFalse(); - fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); - - mockTracer.RelatedWarningEvents.Count.ShouldEqual(2, "There should be two warning events, the first and last"); - mockTracer.RelatedWarningEvents[0].ShouldContain( - new[] - { - "Failed to delete file, retrying ...", - "\"failureCount\":1", - "IOException" - }); - mockTracer.RelatedWarningEvents[1].ShouldContain( - new[] - { - "Failed to delete file.", - "\"failureCount\":11", - "IOException" - }); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void TryWaitForDeleteLogsAtSpecifiedInterval() - { - string path = "mock:\\file.txt"; - DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); - fileSystem.DeleteException = new IOException(); - - MockTracer mockTracer = new MockTracer(); - int maxRetries = 10; - fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 3).ShouldBeFalse(); - fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); - - mockTracer.RelatedWarningEvents.Count.ShouldEqual(5, "There should be five warning events, the first and last, and the 4th, 7th, and 10th"); - mockTracer.RelatedWarningEvents[0].ShouldContain( - new[] - { - "Failed to delete file, retrying ...", - "\"failureCount\":1", - "IOException" - }); - - mockTracer.RelatedWarningEvents[1].ShouldContain( - new[] - { - "Failed to delete file, retrying ...", - "\"failureCount\":4", - "IOException" - }); - mockTracer.RelatedWarningEvents[2].ShouldContain( - new[] - { - "Failed to delete file, retrying ...", - "\"failureCount\":7", - "IOException" - }); - mockTracer.RelatedWarningEvents[3].ShouldContain( - new[] - { - "Failed to delete file, retrying ...", - "\"failureCount\":10", - "IOException" - }); - mockTracer.RelatedWarningEvents[4].ShouldContain( - new[] - { - "Failed to delete file.", - "\"failureCount\":11", - "IOException" - }); - } - - private class DeleteTestsFileSystem : PhysicalFileSystem - { - private bool allFilesExist; - private bool noOpDelete; - - public DeleteTestsFileSystem( - IEnumerable> existingFiles, - bool allFilesExist = false, - bool noOpDelete = false) - { - this.ExistingFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (KeyValuePair kvp in existingFiles) - { - this.ExistingFiles[kvp.Key] = kvp.Value; - } - - this.allFilesExist = allFilesExist; - this.noOpDelete = noOpDelete; - this.DeleteFileCallCount = 0; - this.MaxDeleteFileExceptions = -1; - } - - public Dictionary ExistingFiles { get; private set; } - public Exception DeleteException { get; set; } - public int MaxDeleteFileExceptions { get; set; } - public int DeleteFileCallCount { get; set; } - - public override bool FileExists(string path) - { - if (this.allFilesExist) - { - return true; - } - - return this.ExistingFiles.ContainsKey(path); - } - - public override void SetAttributes(string path, FileAttributes fileAttributes) - { - if (this.ExistingFiles.ContainsKey(path)) - { - this.ExistingFiles[path] = fileAttributes; - } - else - { - throw new FileNotFoundException(); - } - } - - public override void DeleteFile(string path) - { - this.DeleteFileCallCount++; - - if (!this.noOpDelete) - { - if (this.DeleteException != null && - (this.MaxDeleteFileExceptions == -1 || this.MaxDeleteFileExceptions >= this.DeleteFileCallCount)) - { - throw this.DeleteException; - } - - if (this.ExistingFiles.ContainsKey(path)) - { - if ((this.ExistingFiles[path] & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) - { - throw new UnauthorizedAccessException(); - } - else - { - this.ExistingFiles.Remove(path); - } - } - } - } - } - } -} +using NUnit.Framework; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class PhysicalFileSystemDeleteTests + { + [TestCase] + public void TryDeleteFileDeletesFile() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + + fileSystem.TryDeleteFile(path); + fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file"); + } + + [TestCase] + public void TryDeleteFileSetsAttributesToNormalBeforeDeletingFile() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem( + new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }, + allFilesExist: false, + noOpDelete: true); + + fileSystem.TryDeleteFile(path); + fileSystem.ExistingFiles.ContainsKey(path).ShouldBeTrue("DeleteTestsFileSystem is configured as no-op delete, file should still be present"); + fileSystem.ExistingFiles[path].ShouldEqual(FileAttributes.Normal, "TryDeleteFile should set attributes to Normal before deleting"); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryDeleteFileReturnsTrueWhenSetAttributesFailsToFindFile() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem( + Enumerable.Empty>(), + allFilesExist: true, + noOpDelete: false); + + fileSystem.TryDeleteFile(path).ShouldEqual(true, "TryDeleteFile should return true when SetAttributes throws FileNotFoundException"); + } + + [TestCase] + public void TryDeleteFileReturnsNullExceptionOnSuccess() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + + Exception e = new Exception(); + fileSystem.TryDeleteFile(path, out e); + fileSystem.ExistingFiles.ContainsKey(path).ShouldBeFalse("DeleteUtils failed to delete file"); + e.ShouldBeNull("Exception should be null when TryDeleteFile succeeds"); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryDeleteFileReturnsThrownException() + { + string path = "mock:\\file.txt"; + Exception deleteException = new IOException(); + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = deleteException; + + Exception e; + fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on IOException"); + ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception"); + + deleteException = new UnauthorizedAccessException(); + fileSystem.DeleteException = deleteException; + fileSystem.TryDeleteFile(path, out e).ShouldBeFalse("TryDeleteFile should fail on UnauthorizedAccessException"); + ReferenceEquals(e, deleteException).ShouldBeTrue("TryDeleteFile should return the thrown exception"); + } + + [TestCase] + public void TryDeleteFileDoesNotUpdateMetadataOnSuccess() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + + EventMetadata metadata = new EventMetadata(); + fileSystem.TryDeleteFile(path, "metadataKey", metadata).ShouldBeTrue("TryDeleteFile should succeed"); + metadata.ShouldBeEmpty("TryDeleteFile should not update metadata on success"); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryDeleteFileUpdatesMetadataOnFailure() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = new IOException(); + + EventMetadata metadata = new EventMetadata(); + fileSystem.TryDeleteFile(path, "testKey", metadata).ShouldBeFalse("TryDeleteFile should fail when IOException is thrown"); + metadata.ContainsKey("testKey_DeleteFailed").ShouldBeTrue(); + metadata["testKey_DeleteFailed"].ShouldEqual("true"); + metadata.ContainsKey("testKey_DeleteException").ShouldBeTrue(); + metadata["testKey_DeleteException"].ShouldBeOfType().ShouldContain("IOException"); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryWaitForDeleteSucceedsAfterFailures() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = new IOException(); + + fileSystem.MaxDeleteFileExceptions = 5; + fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue(); + fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1); + + fileSystem.ExistingFiles.Add(path, FileAttributes.ReadOnly); + fileSystem.DeleteFileCallCount = 0; + fileSystem.MaxDeleteFileExceptions = 9; + fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: 10, retryLoggingThreshold: 1).ShouldBeTrue(); + fileSystem.DeleteFileCallCount.ShouldEqual(fileSystem.MaxDeleteFileExceptions + 1); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryWaitForDeleteFailsAfterMaxRetries() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = new IOException(); + + int maxRetries = 10; + fileSystem.TryWaitForDelete(null, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse(); + fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); + + fileSystem.DeleteFileCallCount = 0; + fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: maxRetries, retryLoggingThreshold: 1).ShouldBeFalse(); + fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); + + fileSystem.DeleteFileCallCount = 0; + fileSystem.TryWaitForDelete(null, path, retryDelayMs: 1, maxRetries: 0, retryLoggingThreshold: 1).ShouldBeFalse(); + fileSystem.DeleteFileCallCount.ShouldEqual(1); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryWaitForDeleteAlwaysLogsFirstAndLastFailure() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = new IOException(); + + MockTracer mockTracer = new MockTracer(); + int maxRetries = 10; + fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 1000).ShouldBeFalse(); + fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); + + mockTracer.RelatedWarningEvents.Count.ShouldEqual(2, "There should be two warning events, the first and last"); + mockTracer.RelatedWarningEvents[0].ShouldContain( + new[] + { + "Failed to delete file, retrying ...", + "\"failureCount\":1", + "IOException" + }); + mockTracer.RelatedWarningEvents[1].ShouldContain( + new[] + { + "Failed to delete file.", + "\"failureCount\":11", + "IOException" + }); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void TryWaitForDeleteLogsAtSpecifiedInterval() + { + string path = "mock:\\file.txt"; + DeleteTestsFileSystem fileSystem = new DeleteTestsFileSystem(new[] { new KeyValuePair(path, FileAttributes.ReadOnly) }); + fileSystem.DeleteException = new IOException(); + + MockTracer mockTracer = new MockTracer(); + int maxRetries = 10; + fileSystem.TryWaitForDelete(mockTracer, path, retryDelayMs: 0, maxRetries: maxRetries, retryLoggingThreshold: 3).ShouldBeFalse(); + fileSystem.DeleteFileCallCount.ShouldEqual(maxRetries + 1); + + mockTracer.RelatedWarningEvents.Count.ShouldEqual(5, "There should be five warning events, the first and last, and the 4th, 7th, and 10th"); + mockTracer.RelatedWarningEvents[0].ShouldContain( + new[] + { + "Failed to delete file, retrying ...", + "\"failureCount\":1", + "IOException" + }); + + mockTracer.RelatedWarningEvents[1].ShouldContain( + new[] + { + "Failed to delete file, retrying ...", + "\"failureCount\":4", + "IOException" + }); + mockTracer.RelatedWarningEvents[2].ShouldContain( + new[] + { + "Failed to delete file, retrying ...", + "\"failureCount\":7", + "IOException" + }); + mockTracer.RelatedWarningEvents[3].ShouldContain( + new[] + { + "Failed to delete file, retrying ...", + "\"failureCount\":10", + "IOException" + }); + mockTracer.RelatedWarningEvents[4].ShouldContain( + new[] + { + "Failed to delete file.", + "\"failureCount\":11", + "IOException" + }); + } + + private class DeleteTestsFileSystem : PhysicalFileSystem + { + private bool allFilesExist; + private bool noOpDelete; + + public DeleteTestsFileSystem( + IEnumerable> existingFiles, + bool allFilesExist = false, + bool noOpDelete = false) + { + this.ExistingFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (KeyValuePair kvp in existingFiles) + { + this.ExistingFiles[kvp.Key] = kvp.Value; + } + + this.allFilesExist = allFilesExist; + this.noOpDelete = noOpDelete; + this.DeleteFileCallCount = 0; + this.MaxDeleteFileExceptions = -1; + } + + public Dictionary ExistingFiles { get; private set; } + public Exception DeleteException { get; set; } + public int MaxDeleteFileExceptions { get; set; } + public int DeleteFileCallCount { get; set; } + + public override bool FileExists(string path) + { + if (this.allFilesExist) + { + return true; + } + + return this.ExistingFiles.ContainsKey(path); + } + + public override void SetAttributes(string path, FileAttributes fileAttributes) + { + if (this.ExistingFiles.ContainsKey(path)) + { + this.ExistingFiles[path] = fileAttributes; + } + else + { + throw new FileNotFoundException(); + } + } + + public override void DeleteFile(string path) + { + this.DeleteFileCallCount++; + + if (!this.noOpDelete) + { + if (this.DeleteException != null && + (this.MaxDeleteFileExceptions == -1 || this.MaxDeleteFileExceptions >= this.DeleteFileCallCount)) + { + throw this.DeleteException; + } + + if (this.ExistingFiles.ContainsKey(path)) + { + if ((this.ExistingFiles[path] & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) + { + throw new UnauthorizedAccessException(); + } + else + { + this.ExistingFiles.Remove(path); + } + } + } + } + } + } +} diff --git a/Scalar.UnitTests/Common/RefLogEntryTests.cs b/Scalar.UnitTests/Common/RefLogEntryTests.cs index 93cc086125..86b3ef970c 100644 --- a/Scalar.UnitTests/Common/RefLogEntryTests.cs +++ b/Scalar.UnitTests/Common/RefLogEntryTests.cs @@ -1,63 +1,63 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class RefLogEntryTests - { - [TestCase] - public void ParsesValidRefLog() - { - const string SourceSha = "0000000000000000000000000000000000000000"; - const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4"; - const string Reason = "clone: from https://repourl"; - string testLine = string.Format("{0} {1} author 1478738341 -0800\t{2}", SourceSha, TargetSha, Reason); - - RefLogEntry output; - RefLogEntry.TryParse(testLine, out output).ShouldEqual(true); - - output.ShouldNotBeNull(); - output.SourceSha.ShouldEqual(SourceSha); - output.TargetSha.ShouldEqual(TargetSha); - output.Reason.ShouldEqual(Reason); - } - - [TestCase] - public void FailsForMissingReason() - { - const string SourceSha = "0000000000000000000000000000000000000000"; - const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4"; - string testLine = string.Format("{0} {1} author 1478738341 -0800", SourceSha, TargetSha); - - RefLogEntry output; - RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); - - output.ShouldBeNull(); - } - - [TestCase] - public void FailsForMissingTargetSha() - { - const string SourceSha = "0000000000000000000000000000000000000000"; - string testLine = string.Format("{0} ", SourceSha); - - RefLogEntry output; - RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); - - output.ShouldBeNull(); - } - - [TestCase] - public void FailsForNull() - { - string testLine = null; - - RefLogEntry output; - RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); - - output.ShouldBeNull(); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class RefLogEntryTests + { + [TestCase] + public void ParsesValidRefLog() + { + const string SourceSha = "0000000000000000000000000000000000000000"; + const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4"; + const string Reason = "clone: from https://repourl"; + string testLine = string.Format("{0} {1} author 1478738341 -0800\t{2}", SourceSha, TargetSha, Reason); + + RefLogEntry output; + RefLogEntry.TryParse(testLine, out output).ShouldEqual(true); + + output.ShouldNotBeNull(); + output.SourceSha.ShouldEqual(SourceSha); + output.TargetSha.ShouldEqual(TargetSha); + output.Reason.ShouldEqual(Reason); + } + + [TestCase] + public void FailsForMissingReason() + { + const string SourceSha = "0000000000000000000000000000000000000000"; + const string TargetSha = "d249e0fea84484eb105d52174cf326958ee87ab4"; + string testLine = string.Format("{0} {1} author 1478738341 -0800", SourceSha, TargetSha); + + RefLogEntry output; + RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); + + output.ShouldBeNull(); + } + + [TestCase] + public void FailsForMissingTargetSha() + { + const string SourceSha = "0000000000000000000000000000000000000000"; + string testLine = string.Format("{0} ", SourceSha); + + RefLogEntry output; + RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); + + output.ShouldBeNull(); + } + + [TestCase] + public void FailsForNull() + { + string testLine = null; + + RefLogEntry output; + RefLogEntry.TryParse(testLine, out output).ShouldEqual(false); + + output.ShouldBeNull(); + } + } +} diff --git a/Scalar.UnitTests/Common/RetryBackoffTests.cs b/Scalar.UnitTests/Common/RetryBackoffTests.cs index fdf662e2e9..5150434ccf 100644 --- a/Scalar.UnitTests/Common/RetryBackoffTests.cs +++ b/Scalar.UnitTests/Common/RetryBackoffTests.cs @@ -1,83 +1,83 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System; -using System.Threading; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class RetryBackoffTests - { - [TestCase] - public void CalculateBackoffReturnsZeroForFirstAttempt() - { - int failedAttempt = 1; - int maxBackoff = 300; - - RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff).ShouldEqual(0); - } - - [TestCase] - public void CalculateBackoff() - { - int failedAttempt = 2; - int maxBackoff = 300; - - double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); - - backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); - - ++failedAttempt; - backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); - - backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); - } - - [TestCase] - public void CalculateBackoffThatWouldExceedMaxBackoff() - { - int failedAttempt = 30; - int maxBackoff = 300; - double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); - } - - [TestCase] - public void CalculateBackoffAcrossMultipleThreads() - { - int failedAttempt = 2; - int maxBackoff = 300; - int numThreads = 10; - - Thread[] calcThreads = new Thread[numThreads]; - - for (int i = 0; i < numThreads; i++) - { - calcThreads[i] = new Thread( - () => - { - double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); - this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); - }); - - calcThreads[i].Start(); - } - - for (int i = 0; i < calcThreads.Length; i++) - { - calcThreads[i].Join(); - } - } - - private void ValidateBackoff(double backoff, int failedAttempt, double maxBackoff, double exponentialBackoffBase) - { - backoff.ShouldBeAtLeast(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * .9); - backoff.ShouldBeAtMost(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * 1.1); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System; +using System.Threading; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class RetryBackoffTests + { + [TestCase] + public void CalculateBackoffReturnsZeroForFirstAttempt() + { + int failedAttempt = 1; + int maxBackoff = 300; + + RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff).ShouldEqual(0); + } + + [TestCase] + public void CalculateBackoff() + { + int failedAttempt = 2; + int maxBackoff = 300; + + double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); + + backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); + + ++failedAttempt; + backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); + + backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase + 1); + } + + [TestCase] + public void CalculateBackoffThatWouldExceedMaxBackoff() + { + int failedAttempt = 30; + int maxBackoff = 300; + double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); + } + + [TestCase] + public void CalculateBackoffAcrossMultipleThreads() + { + int failedAttempt = 2; + int maxBackoff = 300; + int numThreads = 10; + + Thread[] calcThreads = new Thread[numThreads]; + + for (int i = 0; i < numThreads; i++) + { + calcThreads[i] = new Thread( + () => + { + double backoff = RetryBackoff.CalculateBackoffSeconds(failedAttempt, maxBackoff); + this.ValidateBackoff(backoff, failedAttempt, maxBackoff, RetryBackoff.DefaultExponentialBackoffBase); + }); + + calcThreads[i].Start(); + } + + for (int i = 0; i < calcThreads.Length; i++) + { + calcThreads[i].Join(); + } + } + + private void ValidateBackoff(double backoff, int failedAttempt, double maxBackoff, double exponentialBackoffBase) + { + backoff.ShouldBeAtLeast(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * .9); + backoff.ShouldBeAtMost(Math.Min(Math.Pow(exponentialBackoffBase, failedAttempt), maxBackoff) * 1.1); + } + } +} diff --git a/Scalar.UnitTests/Common/RetryConfigTests.cs b/Scalar.UnitTests/Common/RetryConfigTests.cs index c3807b7c40..fe716d397c 100644 --- a/Scalar.UnitTests/Common/RetryConfigTests.cs +++ b/Scalar.UnitTests/Common/RetryConfigTests.cs @@ -1,111 +1,111 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; -using System; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class RetryConfigTests - { - private const string ReadConfigFailureMessage = "Failed to read config"; - [TestCase] - public void TryLoadConfigFailsWhenGitFailsToReadConfig() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); - error.ShouldContain(ReadConfigFailureMessage); - } - - [TestCase] - public void TryLoadConfigUsesDefaultValuesWhenEntriesNotInConfig() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); - error.ShouldEqual(string.Empty); - config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries); - config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); - config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds)); - } - - [TestCase] - public void TryLoadConfigUsesDefaultValuesWhenEntriesAreBlank() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); - error.ShouldEqual(string.Empty); - config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries); - config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); - config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds)); - } - - [TestCase] - public void TryLoadConfigEnforcesMinimumValuesOnMaxRetries() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result("30", string.Empty, GitProcess.Result.SuccessCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); - error.ShouldContain("Invalid value -1 for setting scalar.max-retries, value must be greater than or equal to 0"); - } - - [TestCase] - public void TryLoadConfigEnforcesMinimumValuesOnTimeout() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result("3", string.Empty, GitProcess.Result.SuccessCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); - error.ShouldContain("Invalid value -1 for setting scalar.timeout-seconds, value must be greater than or equal to 0"); - } - - [TestCase] - public void TryLoadConfigUsesConfiguredValues() - { - int maxRetries = RetryConfig.DefaultMaxRetries + 1; - int timeoutSeconds = RetryConfig.DefaultTimeoutSeconds + 1; - - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(maxRetries.ToString(), string.Empty, GitProcess.Result.SuccessCode)); - gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(timeoutSeconds.ToString(), string.Empty, GitProcess.Result.SuccessCode)); - - RetryConfig config; - string error; - RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); - error.ShouldEqual(string.Empty); - config.MaxRetries.ShouldEqual(maxRetries); - config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); - config.Timeout.ShouldEqual(TimeSpan.FromSeconds(timeoutSeconds)); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; +using System; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class RetryConfigTests + { + private const string ReadConfigFailureMessage = "Failed to read config"; + [TestCase] + public void TryLoadConfigFailsWhenGitFailsToReadConfig() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, ReadConfigFailureMessage, GitProcess.Result.GenericFailureCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); + error.ShouldContain(ReadConfigFailureMessage); + } + + [TestCase] + public void TryLoadConfigUsesDefaultValuesWhenEntriesNotInConfig() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); + error.ShouldEqual(string.Empty); + config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries); + config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); + config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds)); + } + + [TestCase] + public void TryLoadConfigUsesDefaultValuesWhenEntriesAreBlank() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); + error.ShouldEqual(string.Empty); + config.MaxRetries.ShouldEqual(RetryConfig.DefaultMaxRetries); + config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); + config.Timeout.ShouldEqual(TimeSpan.FromSeconds(RetryConfig.DefaultTimeoutSeconds)); + } + + [TestCase] + public void TryLoadConfigEnforcesMinimumValuesOnMaxRetries() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result("30", string.Empty, GitProcess.Result.SuccessCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); + error.ShouldContain("Invalid value -1 for setting scalar.max-retries, value must be greater than or equal to 0"); + } + + [TestCase] + public void TryLoadConfigEnforcesMinimumValuesOnTimeout() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result("3", string.Empty, GitProcess.Result.SuccessCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result("-1", string.Empty, GitProcess.Result.SuccessCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(false); + error.ShouldContain("Invalid value -1 for setting scalar.timeout-seconds, value must be greater than or equal to 0"); + } + + [TestCase] + public void TryLoadConfigUsesConfiguredValues() + { + int maxRetries = RetryConfig.DefaultMaxRetries + 1; + int timeoutSeconds = RetryConfig.DefaultTimeoutSeconds + 1; + + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.max-retries", () => new GitProcess.Result(maxRetries.ToString(), string.Empty, GitProcess.Result.SuccessCode)); + gitProcess.SetExpectedCommandResult("config scalar.timeout-seconds", () => new GitProcess.Result(timeoutSeconds.ToString(), string.Empty, GitProcess.Result.SuccessCode)); + + RetryConfig config; + string error; + RetryConfig.TryLoadFromGitConfig(tracer, gitProcess, out config, out error).ShouldEqual(true); + error.ShouldEqual(string.Empty); + config.MaxRetries.ShouldEqual(maxRetries); + config.MaxAttempts.ShouldEqual(config.MaxRetries + 1); + config.Timeout.ShouldEqual(TimeSpan.FromSeconds(timeoutSeconds)); + } + } +} diff --git a/Scalar.UnitTests/Common/RetryWrapperTests.cs b/Scalar.UnitTests/Common/RetryWrapperTests.cs index 1e14f92b83..051b69c75f 100644 --- a/Scalar.UnitTests/Common/RetryWrapperTests.cs +++ b/Scalar.UnitTests/Common/RetryWrapperTests.cs @@ -1,237 +1,237 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class RetryWrapperTests - { - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void WillRetryOnIOException() - { - const int ExpectedTries = 5; - - RetryWrapper dut = new RetryWrapper(ExpectedTries, CancellationToken.None, exponentialBackoffBase: 0); - - int actualTries = 0; - RetryWrapper.InvocationResult output = dut.Invoke( - tryCount => - { - actualTries++; - throw new IOException(); - }); - - output.Succeeded.ShouldEqual(false); - actualTries.ShouldEqual(ExpectedTries); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void WillNotRetryForGenericExceptions() - { - const int MaxTries = 5; - - RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); - - Assert.Throws( - () => - { - RetryWrapper.InvocationResult output = dut.Invoke(tryCount => { throw new Exception(); }); - }); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void WillNotMakeAnyAttemptWhenInitiallyCanceled() - { - const int MaxTries = 5; - int actualTries = 0; - - RetryWrapper dut = new RetryWrapper(MaxTries, new CancellationToken(canceled: true), exponentialBackoffBase: 0); - - Assert.Throws( - () => - { - RetryWrapper.InvocationResult output = dut.Invoke(tryCount => - { - ++actualTries; - return new RetryWrapper.CallbackResult(true); - }); - }); - - actualTries.ShouldEqual(0); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void WillNotRetryForWhenCanceledDuringAttempts() - { - const int MaxTries = 5; - int actualTries = 0; - int expectedTries = 3; - - using (CancellationTokenSource tokenSource = new CancellationTokenSource()) - { - RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 0); - - Assert.Throws( - () => - { - RetryWrapper.InvocationResult output = dut.Invoke(tryCount => - { - ++actualTries; - - if (actualTries == expectedTries) - { - tokenSource.Cancel(); - } - - return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); - }); - }); - - actualTries.ShouldEqual(expectedTries); - } - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void WillNotRetryWhenCancelledDuringBackoff() - { - const int MaxTries = 5; - int actualTries = 0; - int expectedTries = 2; // 2 because RetryWrapper does not wait after the first failure - - using (CancellationTokenSource tokenSource = new CancellationTokenSource()) - { - RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 300); - - Task.Run(() => - { - // Wait 3 seconds and cancel - Thread.Sleep(1000 * 3); - tokenSource.Cancel(); - }); - - Assert.Throws( - () => - { - RetryWrapper.InvocationResult output = dut.Invoke(tryCount => - { - ++actualTries; - return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); - }); - }); - - actualTries.ShouldEqual(expectedTries); - } - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void OnFailureIsCalledWhenEventHandlerAttached() - { - const int MaxTries = 5; - const int ExpectedFailures = 5; - - RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); - - int actualFailures = 0; - dut.OnFailure += errorArgs => actualFailures++; - - RetryWrapper.InvocationResult output = dut.Invoke( - tryCount => - { - throw new IOException(); - }); - - output.Succeeded.ShouldEqual(false); - actualFailures.ShouldEqual(ExpectedFailures); - } - - [TestCase] - public void OnSuccessIsOnlyCalledOnce() - { - const int MaxTries = 5; - const int ExpectedFailures = 0; - const int ExpectedTries = 1; - - RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); - - int actualFailures = 0; - dut.OnFailure += errorArgs => actualFailures++; - - int actualTries = 0; - RetryWrapper.InvocationResult output = dut.Invoke( - tryCount => - { - actualTries++; - return new RetryWrapper.CallbackResult(true); - }); - - output.Succeeded.ShouldEqual(true); - output.Result.ShouldEqual(true); - actualTries.ShouldEqual(ExpectedTries); - actualFailures.ShouldEqual(ExpectedFailures); - } - - [TestCase] - public void WillNotRetryWhenNotRequested() - { - const int MaxTries = 5; - const int ExpectedFailures = 1; - const int ExpectedTries = 1; - - RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); - - int actualFailures = 0; - dut.OnFailure += errorArgs => actualFailures++; - - int actualTries = 0; - RetryWrapper.InvocationResult output = dut.Invoke( - tryCount => - { - actualTries++; - return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: false); - }); - - output.Succeeded.ShouldEqual(false); - output.Result.ShouldEqual(false); - actualTries.ShouldEqual(ExpectedTries); - actualFailures.ShouldEqual(ExpectedFailures); - } - - [TestCase] - public void WillRetryWhenRequested() - { - const int MaxTries = 5; - const int ExpectedFailures = 5; - const int ExpectedTries = 5; - - RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); - - int actualFailures = 0; - dut.OnFailure += errorArgs => actualFailures++; - - int actualTries = 0; - RetryWrapper.InvocationResult output = dut.Invoke( - tryCount => - { - actualTries++; - return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); - }); - - output.Succeeded.ShouldEqual(false); - output.Result.ShouldEqual(false); - actualTries.ShouldEqual(ExpectedTries); - actualFailures.ShouldEqual(ExpectedFailures); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class RetryWrapperTests + { + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void WillRetryOnIOException() + { + const int ExpectedTries = 5; + + RetryWrapper dut = new RetryWrapper(ExpectedTries, CancellationToken.None, exponentialBackoffBase: 0); + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.Invoke( + tryCount => + { + actualTries++; + throw new IOException(); + }); + + output.Succeeded.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void WillNotRetryForGenericExceptions() + { + const int MaxTries = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); + + Assert.Throws( + () => + { + RetryWrapper.InvocationResult output = dut.Invoke(tryCount => { throw new Exception(); }); + }); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void WillNotMakeAnyAttemptWhenInitiallyCanceled() + { + const int MaxTries = 5; + int actualTries = 0; + + RetryWrapper dut = new RetryWrapper(MaxTries, new CancellationToken(canceled: true), exponentialBackoffBase: 0); + + Assert.Throws( + () => + { + RetryWrapper.InvocationResult output = dut.Invoke(tryCount => + { + ++actualTries; + return new RetryWrapper.CallbackResult(true); + }); + }); + + actualTries.ShouldEqual(0); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void WillNotRetryForWhenCanceledDuringAttempts() + { + const int MaxTries = 5; + int actualTries = 0; + int expectedTries = 3; + + using (CancellationTokenSource tokenSource = new CancellationTokenSource()) + { + RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 0); + + Assert.Throws( + () => + { + RetryWrapper.InvocationResult output = dut.Invoke(tryCount => + { + ++actualTries; + + if (actualTries == expectedTries) + { + tokenSource.Cancel(); + } + + return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); + }); + }); + + actualTries.ShouldEqual(expectedTries); + } + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void WillNotRetryWhenCancelledDuringBackoff() + { + const int MaxTries = 5; + int actualTries = 0; + int expectedTries = 2; // 2 because RetryWrapper does not wait after the first failure + + using (CancellationTokenSource tokenSource = new CancellationTokenSource()) + { + RetryWrapper dut = new RetryWrapper(MaxTries, tokenSource.Token, exponentialBackoffBase: 300); + + Task.Run(() => + { + // Wait 3 seconds and cancel + Thread.Sleep(1000 * 3); + tokenSource.Cancel(); + }); + + Assert.Throws( + () => + { + RetryWrapper.InvocationResult output = dut.Invoke(tryCount => + { + ++actualTries; + return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); + }); + }); + + actualTries.ShouldEqual(expectedTries); + } + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void OnFailureIsCalledWhenEventHandlerAttached() + { + const int MaxTries = 5; + const int ExpectedFailures = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + RetryWrapper.InvocationResult output = dut.Invoke( + tryCount => + { + throw new IOException(); + }); + + output.Succeeded.ShouldEqual(false); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void OnSuccessIsOnlyCalledOnce() + { + const int MaxTries = 5; + const int ExpectedFailures = 0; + const int ExpectedTries = 1; + + RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.Invoke( + tryCount => + { + actualTries++; + return new RetryWrapper.CallbackResult(true); + }); + + output.Succeeded.ShouldEqual(true); + output.Result.ShouldEqual(true); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void WillNotRetryWhenNotRequested() + { + const int MaxTries = 5; + const int ExpectedFailures = 1; + const int ExpectedTries = 1; + + RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.Invoke( + tryCount => + { + actualTries++; + return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: false); + }); + + output.Succeeded.ShouldEqual(false); + output.Result.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + + [TestCase] + public void WillRetryWhenRequested() + { + const int MaxTries = 5; + const int ExpectedFailures = 5; + const int ExpectedTries = 5; + + RetryWrapper dut = new RetryWrapper(MaxTries, CancellationToken.None, exponentialBackoffBase: 0); + + int actualFailures = 0; + dut.OnFailure += errorArgs => actualFailures++; + + int actualTries = 0; + RetryWrapper.InvocationResult output = dut.Invoke( + tryCount => + { + actualTries++; + return new RetryWrapper.CallbackResult(new Exception("Test"), shouldRetry: true); + }); + + output.Succeeded.ShouldEqual(false); + output.Result.ShouldEqual(false); + actualTries.ShouldEqual(ExpectedTries); + actualFailures.ShouldEqual(ExpectedFailures); + } + } +} diff --git a/Scalar.UnitTests/Common/SHA1UtilTests.cs b/Scalar.UnitTests/Common/SHA1UtilTests.cs index 892688ff39..1d0de87ef7 100644 --- a/Scalar.UnitTests/Common/SHA1UtilTests.cs +++ b/Scalar.UnitTests/Common/SHA1UtilTests.cs @@ -1,78 +1,78 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System.Text; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class SHA1UtilTests - { - private const string TestString = "c:\\Repos\\GVFS\\src\\.gittattributes"; - private const string TestResultSha1 = "ced5ad9680c1a05e9100680c2b3432de23bb7d6d"; - private const string TestResultHex = "633a5c5265706f735c475646535c7372635c2e6769747461747472696275746573"; - - [TestCase] - public void SHA1HashStringForUTF8String() - { - SHA1Util.SHA1HashStringForUTF8String(TestString).ShouldEqual(TestResultSha1); - } - - [TestCase] - public void HexStringFromBytes() - { - byte[] bytes = Encoding.UTF8.GetBytes(TestString); - SHA1Util.HexStringFromBytes(bytes).ShouldEqual(TestResultHex); - } - - [TestCase] - public void IsValidFullSHAIsFalseForEmptyString() - { - SHA1Util.IsValidShaFormat(string.Empty).ShouldEqual(false); - } - - [TestCase] - public void IsValidFullSHAIsFalseForHexStringsNot40Chars() - { - SHA1Util.IsValidShaFormat("1").ShouldEqual(false); - SHA1Util.IsValidShaFormat("9").ShouldEqual(false); - SHA1Util.IsValidShaFormat("A").ShouldEqual(false); - SHA1Util.IsValidShaFormat("a").ShouldEqual(false); - SHA1Util.IsValidShaFormat("f").ShouldEqual(false); - SHA1Util.IsValidShaFormat("f").ShouldEqual(false); - SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF").ShouldEqual(false); - SHA1Util.IsValidShaFormat("12345678901234567890123456789012345678901").ShouldEqual(false); - } - - [TestCase] - public void IsValidFullSHAFalseForNonHexStrings() - { - SHA1Util.IsValidShaFormat("@").ShouldEqual(false); - SHA1Util.IsValidShaFormat("g").ShouldEqual(false); - SHA1Util.IsValidShaFormat("G").ShouldEqual(false); - SHA1Util.IsValidShaFormat("~").ShouldEqual(false); - SHA1Util.IsValidShaFormat("_").ShouldEqual(false); - SHA1Util.IsValidShaFormat(".").ShouldEqual(false); - SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF.tmp").ShouldEqual(false); - SHA1Util.IsValidShaFormat("G1234567890abcdefABCDEF.tmp").ShouldEqual(false); - SHA1Util.IsValidShaFormat("_G1234567890abcdefABCDEF.tmp").ShouldEqual(false); - SHA1Util.IsValidShaFormat("@234567890123456789012345678901234567890").ShouldEqual(false); - SHA1Util.IsValidShaFormat("g234567890123456789012345678901234567890").ShouldEqual(false); - SHA1Util.IsValidShaFormat("G234567890123456789012345678901234567890").ShouldEqual(false); - SHA1Util.IsValidShaFormat("~234567890123456789012345678901234567890").ShouldEqual(false); - SHA1Util.IsValidShaFormat("_234567890123456789012345678901234567890").ShouldEqual(false); - SHA1Util.IsValidShaFormat(".234567890123456789012345678901234567890").ShouldEqual(false); - } - - [TestCase] - public void IsValidFullSHATrueForLength40HexStrings() - { - SHA1Util.IsValidShaFormat("1234567890123456789012345678901234567890").ShouldEqual(true); - SHA1Util.IsValidShaFormat("abcdef7890123456789012345678901234567890").ShouldEqual(true); - SHA1Util.IsValidShaFormat("ABCDEF7890123456789012345678901234567890").ShouldEqual(true); - SHA1Util.IsValidShaFormat("1234567890123456789012345678901234ABCDEF").ShouldEqual(true); - SHA1Util.IsValidShaFormat("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").ShouldEqual(true); - SHA1Util.IsValidShaFormat("ffffffffffffffffffffffffffffffffffffffff").ShouldEqual(true); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System.Text; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class SHA1UtilTests + { + private const string TestString = "c:\\Repos\\GVFS\\src\\.gittattributes"; + private const string TestResultSha1 = "ced5ad9680c1a05e9100680c2b3432de23bb7d6d"; + private const string TestResultHex = "633a5c5265706f735c475646535c7372635c2e6769747461747472696275746573"; + + [TestCase] + public void SHA1HashStringForUTF8String() + { + SHA1Util.SHA1HashStringForUTF8String(TestString).ShouldEqual(TestResultSha1); + } + + [TestCase] + public void HexStringFromBytes() + { + byte[] bytes = Encoding.UTF8.GetBytes(TestString); + SHA1Util.HexStringFromBytes(bytes).ShouldEqual(TestResultHex); + } + + [TestCase] + public void IsValidFullSHAIsFalseForEmptyString() + { + SHA1Util.IsValidShaFormat(string.Empty).ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHAIsFalseForHexStringsNot40Chars() + { + SHA1Util.IsValidShaFormat("1").ShouldEqual(false); + SHA1Util.IsValidShaFormat("9").ShouldEqual(false); + SHA1Util.IsValidShaFormat("A").ShouldEqual(false); + SHA1Util.IsValidShaFormat("a").ShouldEqual(false); + SHA1Util.IsValidShaFormat("f").ShouldEqual(false); + SHA1Util.IsValidShaFormat("f").ShouldEqual(false); + SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF").ShouldEqual(false); + SHA1Util.IsValidShaFormat("12345678901234567890123456789012345678901").ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHAFalseForNonHexStrings() + { + SHA1Util.IsValidShaFormat("@").ShouldEqual(false); + SHA1Util.IsValidShaFormat("g").ShouldEqual(false); + SHA1Util.IsValidShaFormat("G").ShouldEqual(false); + SHA1Util.IsValidShaFormat("~").ShouldEqual(false); + SHA1Util.IsValidShaFormat("_").ShouldEqual(false); + SHA1Util.IsValidShaFormat(".").ShouldEqual(false); + SHA1Util.IsValidShaFormat("1234567890abcdefABCDEF.tmp").ShouldEqual(false); + SHA1Util.IsValidShaFormat("G1234567890abcdefABCDEF.tmp").ShouldEqual(false); + SHA1Util.IsValidShaFormat("_G1234567890abcdefABCDEF.tmp").ShouldEqual(false); + SHA1Util.IsValidShaFormat("@234567890123456789012345678901234567890").ShouldEqual(false); + SHA1Util.IsValidShaFormat("g234567890123456789012345678901234567890").ShouldEqual(false); + SHA1Util.IsValidShaFormat("G234567890123456789012345678901234567890").ShouldEqual(false); + SHA1Util.IsValidShaFormat("~234567890123456789012345678901234567890").ShouldEqual(false); + SHA1Util.IsValidShaFormat("_234567890123456789012345678901234567890").ShouldEqual(false); + SHA1Util.IsValidShaFormat(".234567890123456789012345678901234567890").ShouldEqual(false); + } + + [TestCase] + public void IsValidFullSHATrueForLength40HexStrings() + { + SHA1Util.IsValidShaFormat("1234567890123456789012345678901234567890").ShouldEqual(true); + SHA1Util.IsValidShaFormat("abcdef7890123456789012345678901234567890").ShouldEqual(true); + SHA1Util.IsValidShaFormat("ABCDEF7890123456789012345678901234567890").ShouldEqual(true); + SHA1Util.IsValidShaFormat("1234567890123456789012345678901234ABCDEF").ShouldEqual(true); + SHA1Util.IsValidShaFormat("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").ShouldEqual(true); + SHA1Util.IsValidShaFormat("ffffffffffffffffffffffffffffffffffffffff").ShouldEqual(true); + } + } +} diff --git a/Scalar.UnitTests/Common/ScalarEnlistmentTests.cs b/Scalar.UnitTests/Common/ScalarEnlistmentTests.cs index fa73f4469f..9cee112740 100644 --- a/Scalar.UnitTests/Common/ScalarEnlistmentTests.cs +++ b/Scalar.UnitTests/Common/ScalarEnlistmentTests.cs @@ -1,51 +1,51 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Git; - -namespace Scalar.UnitTests.Common -{ - [TestFixture] - public class ScalarEnlistmentTests - { - private const string MountId = "85576f54f9ab4388bcdc19b4f6c17696"; - private const string EnlistmentId = "520dcf634ce34065a06abaa4010a256f"; - - [TestCase] - public void CanGetMountId() - { - TestScalarEnlistment enlistment = new TestScalarEnlistment(); - enlistment.GetMountId().ShouldEqual(MountId); - } - - [TestCase] - public void CanGetEnlistmentId() - { - TestScalarEnlistment enlistment = new TestScalarEnlistment(); - enlistment.GetEnlistmentId().ShouldEqual(EnlistmentId); - } - - private class TestScalarEnlistment : ScalarEnlistment - { - private MockGitProcess gitProcess; - - public TestScalarEnlistment() - : base("mock:\\path", "mock://repoUrl", "mock:\\git", authentication: null) - { - this.gitProcess = new MockGitProcess(); - this.gitProcess.SetExpectedCommandResult( - "config --local scalar.mount-id", - () => new GitProcess.Result(MountId, string.Empty, GitProcess.Result.SuccessCode)); - this.gitProcess.SetExpectedCommandResult( - "config --local scalar.enlistment-id", - () => new GitProcess.Result(EnlistmentId, string.Empty, GitProcess.Result.SuccessCode)); - } - - public override GitProcess CreateGitProcess() - { - return this.gitProcess; - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Git; + +namespace Scalar.UnitTests.Common +{ + [TestFixture] + public class ScalarEnlistmentTests + { + private const string MountId = "85576f54f9ab4388bcdc19b4f6c17696"; + private const string EnlistmentId = "520dcf634ce34065a06abaa4010a256f"; + + [TestCase] + public void CanGetMountId() + { + TestScalarEnlistment enlistment = new TestScalarEnlistment(); + enlistment.GetMountId().ShouldEqual(MountId); + } + + [TestCase] + public void CanGetEnlistmentId() + { + TestScalarEnlistment enlistment = new TestScalarEnlistment(); + enlistment.GetEnlistmentId().ShouldEqual(EnlistmentId); + } + + private class TestScalarEnlistment : ScalarEnlistment + { + private MockGitProcess gitProcess; + + public TestScalarEnlistment() + : base("mock:\\path", "mock://repoUrl", "mock:\\git", authentication: null) + { + this.gitProcess = new MockGitProcess(); + this.gitProcess.SetExpectedCommandResult( + "config --local scalar.mount-id", + () => new GitProcess.Result(MountId, string.Empty, GitProcess.Result.SuccessCode)); + this.gitProcess.SetExpectedCommandResult( + "config --local scalar.enlistment-id", + () => new GitProcess.Result(EnlistmentId, string.Empty, GitProcess.Result.SuccessCode)); + } + + public override GitProcess CreateGitProcess() + { + return this.gitProcess; + } + } + } +} diff --git a/Scalar.UnitTests/Common/TryCreateProductUpgraderTests.cs b/Scalar.UnitTests/Common/TryCreateProductUpgraderTests.cs index 1a17c7cf1a..bef04b8845 100644 --- a/Scalar.UnitTests/Common/TryCreateProductUpgraderTests.cs +++ b/Scalar.UnitTests/Common/TryCreateProductUpgraderTests.cs @@ -1,260 +1,260 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.NuGetUpgrade; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; - -namespace Scalar.UnitTests.Common -{ - public class TryCreateProductUpgradeTests - { - private static string defaultUpgradeFeedPackageName = "package"; - private static string defaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/"; - private static string defaultOrgInfoServerUrl = "https://www.contoso.com"; - private static string defaultRing = "slow"; - - private MockTracer tracer; - private Mock fileSystemMock; - private Mock credentialStoreMock; - - [SetUp] - public void Setup() - { - this.tracer = new MockTracer(); - - // It is important that creating a new Upgrader does not - // require credentials. We must be able to create an - // upgrader to query / check upgrade preconditions without - // requiring authorization. We create these mocks with - // strict behavior to validate methods on them are called - // unnecessarily. - this.credentialStoreMock = new Mock(MockBehavior.Strict); - this.fileSystemMock = new Mock(MockBehavior.Strict); - } - - [TearDown] - public void TearDown() - { - this.credentialStoreMock.VerifyAll(); - this.fileSystemMock.VerifyAll(); - } - - [TestCase] - public void CreatesNuGetUpgraderWhenConfigured() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeTrue(); - productUpgrader.ShouldNotBeNull(); - productUpgrader.ShouldBeOfType(); - error.ShouldBeNull(); - } - - [TestCase] - public void CreatesNuGetUpgraderWhenConfiguredWithNoRing() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() - .WithNoUpgradeRing() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeTrue(); - productUpgrader.ShouldNotBeNull(); - productUpgrader.ShouldBeOfType(); - error.ShouldBeNull(); - } - - [TestCase] - public void CreatesGitHubUpgraderWhenConfigured() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultGitHubConfigBuilder() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeTrue(); - productUpgrader.ShouldNotBeNull(); - productUpgrader.ShouldBeOfType(); - error.ShouldBeNull(); - } - - [TestCase] - public void CreatesOrgNuGetUpgrader() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeTrue(); - productUpgrader.ShouldNotBeNull(); - productUpgrader.ShouldBeOfType(); - error.ShouldBeNull(); - } - - [TestCase] - public void NoUpgraderWhenNuGetFeedMissing() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() - .WithNoUpgradeFeedUrl() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeFalse(); - productUpgrader.ShouldBeNull(); - error.ShouldNotBeNull(); - } - - [TestCase] - public void NoOrgUpgraderWhenNuGetPackNameMissing() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() - .WithNoUpgradeFeedPackageName() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeFalse(); - productUpgrader.ShouldBeNull(); - error.ShouldNotBeNull(); - } - - [TestCase] - public void NoOrgUpgraderWhenNuGetFeedMissing() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() - .WithNoUpgradeFeedUrl() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeFalse(); - productUpgrader.ShouldBeNull(); - error.ShouldNotBeNull(); - } - - [TestCase] - public void NoUpgraderWhenNuGetPackNameMissing() - { - MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() - .WithNoUpgradeFeedPackageName() - .Build(); - - bool success = ProductUpgrader.TryCreateUpgrader( - this.tracer, - this.fileSystemMock.Object, - scalarConfig, - this.credentialStoreMock.Object, - false, - false, - out ProductUpgrader productUpgrader, - out string error); - - success.ShouldBeFalse(); - productUpgrader.ShouldBeNull(); - error.ShouldNotBeNull(); - } - - private MockLocalScalarConfigBuilder ConstructDefaultMockNuGetConfigBuilder() - { - MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() - .WithUpgradeRing() - .WithUpgradeFeedPackageName() - .WithUpgradeFeedUrl(); - - return configBuilder; - } - - private MockLocalScalarConfigBuilder ConstructDefaultMockOrgNuGetConfigBuilder() - { - MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() - .WithUpgradeRing() - .WithUpgradeFeedPackageName() - .WithUpgradeFeedUrl() - .WithOrgInfoServerUrl(); - - return configBuilder; - } - - private MockLocalScalarConfigBuilder ConstructDefaultGitHubConfigBuilder() - { - MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() - .WithUpgradeRing(); - - return configBuilder; - } - - private MockLocalScalarConfigBuilder ConstructMockLocalScalarConfigBuilder() - { - return new MockLocalScalarConfigBuilder( - defaultRing, - defaultUpgradeFeedUrl, - defaultUpgradeFeedPackageName, - defaultOrgInfoServerUrl); - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.NuGetUpgrade; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; + +namespace Scalar.UnitTests.Common +{ + public class TryCreateProductUpgradeTests + { + private static string defaultUpgradeFeedPackageName = "package"; + private static string defaultUpgradeFeedUrl = "https://pkgs.dev.azure.com/contoso/"; + private static string defaultOrgInfoServerUrl = "https://www.contoso.com"; + private static string defaultRing = "slow"; + + private MockTracer tracer; + private Mock fileSystemMock; + private Mock credentialStoreMock; + + [SetUp] + public void Setup() + { + this.tracer = new MockTracer(); + + // It is important that creating a new Upgrader does not + // require credentials. We must be able to create an + // upgrader to query / check upgrade preconditions without + // requiring authorization. We create these mocks with + // strict behavior to validate methods on them are called + // unnecessarily. + this.credentialStoreMock = new Mock(MockBehavior.Strict); + this.fileSystemMock = new Mock(MockBehavior.Strict); + } + + [TearDown] + public void TearDown() + { + this.credentialStoreMock.VerifyAll(); + this.fileSystemMock.VerifyAll(); + } + + [TestCase] + public void CreatesNuGetUpgraderWhenConfigured() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeTrue(); + productUpgrader.ShouldNotBeNull(); + productUpgrader.ShouldBeOfType(); + error.ShouldBeNull(); + } + + [TestCase] + public void CreatesNuGetUpgraderWhenConfiguredWithNoRing() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() + .WithNoUpgradeRing() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeTrue(); + productUpgrader.ShouldNotBeNull(); + productUpgrader.ShouldBeOfType(); + error.ShouldBeNull(); + } + + [TestCase] + public void CreatesGitHubUpgraderWhenConfigured() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultGitHubConfigBuilder() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeTrue(); + productUpgrader.ShouldNotBeNull(); + productUpgrader.ShouldBeOfType(); + error.ShouldBeNull(); + } + + [TestCase] + public void CreatesOrgNuGetUpgrader() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeTrue(); + productUpgrader.ShouldNotBeNull(); + productUpgrader.ShouldBeOfType(); + error.ShouldBeNull(); + } + + [TestCase] + public void NoUpgraderWhenNuGetFeedMissing() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() + .WithNoUpgradeFeedUrl() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeFalse(); + productUpgrader.ShouldBeNull(); + error.ShouldNotBeNull(); + } + + [TestCase] + public void NoOrgUpgraderWhenNuGetPackNameMissing() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() + .WithNoUpgradeFeedPackageName() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeFalse(); + productUpgrader.ShouldBeNull(); + error.ShouldNotBeNull(); + } + + [TestCase] + public void NoOrgUpgraderWhenNuGetFeedMissing() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockOrgNuGetConfigBuilder() + .WithNoUpgradeFeedUrl() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeFalse(); + productUpgrader.ShouldBeNull(); + error.ShouldNotBeNull(); + } + + [TestCase] + public void NoUpgraderWhenNuGetPackNameMissing() + { + MockLocalScalarConfig scalarConfig = this.ConstructDefaultMockNuGetConfigBuilder() + .WithNoUpgradeFeedPackageName() + .Build(); + + bool success = ProductUpgrader.TryCreateUpgrader( + this.tracer, + this.fileSystemMock.Object, + scalarConfig, + this.credentialStoreMock.Object, + false, + false, + out ProductUpgrader productUpgrader, + out string error); + + success.ShouldBeFalse(); + productUpgrader.ShouldBeNull(); + error.ShouldNotBeNull(); + } + + private MockLocalScalarConfigBuilder ConstructDefaultMockNuGetConfigBuilder() + { + MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() + .WithUpgradeRing() + .WithUpgradeFeedPackageName() + .WithUpgradeFeedUrl(); + + return configBuilder; + } + + private MockLocalScalarConfigBuilder ConstructDefaultMockOrgNuGetConfigBuilder() + { + MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() + .WithUpgradeRing() + .WithUpgradeFeedPackageName() + .WithUpgradeFeedUrl() + .WithOrgInfoServerUrl(); + + return configBuilder; + } + + private MockLocalScalarConfigBuilder ConstructDefaultGitHubConfigBuilder() + { + MockLocalScalarConfigBuilder configBuilder = this.ConstructMockLocalScalarConfigBuilder() + .WithUpgradeRing(); + + return configBuilder; + } + + private MockLocalScalarConfigBuilder ConstructMockLocalScalarConfigBuilder() + { + return new MockLocalScalarConfigBuilder( + defaultRing, + defaultUpgradeFeedUrl, + defaultUpgradeFeedPackageName, + defaultOrgInfoServerUrl); + } + } +} diff --git a/Scalar.UnitTests/Data/caseChange.txt b/Scalar.UnitTests/Data/caseChange.txt index a0410abf71..408fb14be5 100644 --- a/Scalar.UnitTests/Data/caseChange.txt +++ b/Scalar.UnitTests/Data/caseChange.txt @@ -1,4 +1,4 @@ -:040000 000000 d813c8227132c3bf73c013f8913f207b4876b2bf 0000000000000000000000000000000000000000 D GVFLT_MultiThreadTest +:040000 000000 d813c8227132c3bf73c013f8913f207b4876b2bf 0000000000000000000000000000000000000000 D GVFLT_MultiThreadTest :040000 000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf 0000000000000000000000000000000000000000 D GVFLT_MultiThreadTest/OpenForReadsSameTime :100644 000000 eabe8d5ec569cc7e199e77411ad935f101414032 0000000000000000000000000000000000000000 D GVFLT_MultiThreadTest/OpenForReadsSameTime/test :040000 000000 1260ecb71f2be8eb92ea904c6dffa3e40eaaf1bf 0000000000000000000000000000000000000000 D GVFLT_MultiThreadTest/OpenForWritesSameTime diff --git a/Scalar.UnitTests/Git/GitAuthenticationTests.cs b/Scalar.UnitTests/Git/GitAuthenticationTests.cs index ab94d37b79..a0662a6a9f 100644 --- a/Scalar.UnitTests/Git/GitAuthenticationTests.cs +++ b/Scalar.UnitTests/Git/GitAuthenticationTests.cs @@ -1,287 +1,287 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; -using System.Linq; - -namespace Scalar.UnitTests.Git -{ - [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))] - public class GitAuthenticationTests - { - private const string CertificatePath = "certificatePath"; - private const string AzureDevOpsUseHttpPathString = "-c credential.\"https://dev.azure.com\".useHttpPath=true"; - - private readonly bool sslSettingsPresent; - - public GitAuthenticationTests(bool sslSettingsPresent) - { - this.sslSettingsPresent = sslSettingsPresent; - } - - [TestCase] - public void AuthShouldBackoffAfterFirstRetryFailure() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string authString; - string error; - - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential"); - - dut.RejectCredentials(tracer, authString); - dut.IsBackingOff.ShouldEqual(false, "Should not backoff after credentials initially rejected"); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration"); - dut.IsBackingOff.ShouldEqual(false, "Should not backoff after successfully getting credentials"); - - dut.RejectCredentials(tracer, authString); - dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff after rejecting credentials"); - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff"); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(2); - } - - [TestCase] - public void BackoffIsNotInEffectAfterSuccess() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string authString; - string error; - - for (int i = 0; i < 5; ++i) - { - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get credential on iteration " + i + ": " + error); - dut.RejectCredentials(tracer, authString); - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration " + i + ": " + error); - dut.ApproveCredentials(tracer, authString); - dut.IsBackingOff.ShouldEqual(false, "Should reset backoff after successfully refreshing credentials"); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(i + 1, $"Should have {i + 1} credentials rejection"); - gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(i + 1, $"Should have {i + 1} credential approvals"); - } - } - - [TestCase] - public void ContinuesToBackoffIfTryGetCredentialsFails() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string authString; - string error; - - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential"); - dut.RejectCredentials(tracer, authString); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - - gitProcess.ShouldFail = true; - - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "Succeeded despite GitProcess returning failure"); - dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials"); - - dut.RejectCredentials(tracer, authString); - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff"); - dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials"); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - } - - [TestCase] - public void TwoThreadsFailAtOnceStillRetriesOnce() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string authString; - string error; - - // Populate an initial PAT on two threads - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); - - // Simulate a 401 error on two threads - dut.RejectCredentials(tracer, authString); - dut.RejectCredentials(tracer, authString); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(authString); - - // Both threads should still be able to get a PAT for retry purposes - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "The second thread caused back off when it shouldn't"); - dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); - } - - [TestCase] - public void TwoThreadsInterleavingFailuresStillRetriesOnce() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string thread1Auth; - string thread1AuthRetry; - string thread2Auth; - string thread2AuthRetry; - string error; - - // Populate an initial PAT on two threads - dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); - dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); - - // Simulate a 401 error on one threads - dut.RejectCredentials(tracer, thread1Auth); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); - - // That thread then retries - dut.TryGetCredentials(tracer, out thread1AuthRetry, out error).ShouldEqual(true); - - // The second thread fails with the old PAT - dut.RejectCredentials(tracer, thread2Auth); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1, "Should not have rejected a second time"); - gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth, "Should only have rejected thread1's initial credential"); - - // The second thread should be able to get a PAT - dut.TryGetCredentials(tracer, out thread2AuthRetry, out error).ShouldEqual(true, error); - } - - [TestCase] - public void TwoThreadsInterleavingFailuresShouldntStompASuccess() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string thread1Auth; - string thread2Auth; - string error; - - // Populate an initial PAT on two threads - dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); - dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); - - // Simulate a 401 error on one threads - dut.RejectCredentials(tracer, thread1Auth); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); - - // That thread then retries and succeeds - dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); - dut.ApproveCredentials(tracer, thread1Auth); - gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialApprovals["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); - - // If the second thread fails with the old PAT, it shouldn't stomp the new PAT - dut.RejectCredentials(tracer, thread2Auth); - gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); - - // The second thread should be able to get a PAT - dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); - thread2Auth.ShouldEqual(thread1Auth, "The second thread stomp the first threads good auth string"); - } - - [TestCase] - public void DontDoubleStoreExistingCredential() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - string authString; - dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue(); - dut.ApproveCredentials(tracer, authString); - dut.ApproveCredentials(tracer, authString); - dut.ApproveCredentials(tracer, authString); - dut.ApproveCredentials(tracer, authString); - dut.ApproveCredentials(tracer, authString); - - gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialRejections.Count.ShouldEqual(0); - gitProcess.StoredCredentials.Count.ShouldEqual(1); - gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl"); - } - - [TestCase] - public void DontStoreDifferentCredentialFromCachedValue() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = this.GetGitProcess(); - - GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); - dut.TryInitializeAndRequireAuth(tracer, out _); - - // Get and store an initial value that will be cached - string authString; - dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue(); - dut.ApproveCredentials(tracer, authString); - - // Try and store a different value from the one that is cached - dut.ApproveCredentials(tracer, "different value"); - - gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); - gitProcess.CredentialRejections.Count.ShouldEqual(0); - gitProcess.StoredCredentials.Count.ShouldEqual(1); - gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl"); - } - - private MockGitProcess GetGitProcess() - { - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("config scalar.FunctionalTests.UserName", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); - gitProcess.SetExpectedCommandResult("config scalar.FunctionalTests.Password", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); - - if (this.sslSettingsPresent) - { - gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result($"http.sslCert {CertificatePath}\nhttp.sslCertPasswordProtected true\n\n", string.Empty, GitProcess.Result.SuccessCode)); - } - else - { - gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); - } - - int approvals = 0; - int rejections = 0; - gitProcess.SetExpectedCommandResult( - $"{AzureDevOpsUseHttpPathString} credential fill", - () => new GitProcess.Result("username=username\r\npassword=password" + rejections + "\r\n", string.Empty, GitProcess.Result.SuccessCode)); - - gitProcess.SetExpectedCommandResult( - $"{AzureDevOpsUseHttpPathString} credential approve", - () => - { - approvals++; - return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode); - }); - - gitProcess.SetExpectedCommandResult( - $"{AzureDevOpsUseHttpPathString} credential reject", - () => - { - rejections++; - return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode); - }); - return gitProcess; - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; +using System.Linq; + +namespace Scalar.UnitTests.Git +{ + [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))] + public class GitAuthenticationTests + { + private const string CertificatePath = "certificatePath"; + private const string AzureDevOpsUseHttpPathString = "-c credential.\"https://dev.azure.com\".useHttpPath=true"; + + private readonly bool sslSettingsPresent; + + public GitAuthenticationTests(bool sslSettingsPresent) + { + this.sslSettingsPresent = sslSettingsPresent; + } + + [TestCase] + public void AuthShouldBackoffAfterFirstRetryFailure() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string authString; + string error; + + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential"); + + dut.RejectCredentials(tracer, authString); + dut.IsBackingOff.ShouldEqual(false, "Should not backoff after credentials initially rejected"); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration"); + dut.IsBackingOff.ShouldEqual(false, "Should not backoff after successfully getting credentials"); + + dut.RejectCredentials(tracer, authString); + dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff after rejecting credentials"); + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff"); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(2); + } + + [TestCase] + public void BackoffIsNotInEffectAfterSuccess() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string authString; + string error; + + for (int i = 0; i < 5; ++i) + { + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get credential on iteration " + i + ": " + error); + dut.RejectCredentials(tracer, authString); + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to retry getting credential on iteration " + i + ": " + error); + dut.ApproveCredentials(tracer, authString); + dut.IsBackingOff.ShouldEqual(false, "Should reset backoff after successfully refreshing credentials"); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(i + 1, $"Should have {i + 1} credentials rejection"); + gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(i + 1, $"Should have {i + 1} credential approvals"); + } + } + + [TestCase] + public void ContinuesToBackoffIfTryGetCredentialsFails() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string authString; + string error; + + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "Failed to get initial credential"); + dut.RejectCredentials(tracer, authString); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + + gitProcess.ShouldFail = true; + + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "Succeeded despite GitProcess returning failure"); + dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials"); + + dut.RejectCredentials(tracer, authString); + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(false, "TryGetCredential should not succeed during backoff"); + dut.IsBackingOff.ShouldEqual(true, "Should continue to backoff if failed to get credentials"); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + } + + [TestCase] + public void TwoThreadsFailAtOnceStillRetriesOnce() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string authString; + string error; + + // Populate an initial PAT on two threads + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); + + // Simulate a 401 error on two threads + dut.RejectCredentials(tracer, authString); + dut.RejectCredentials(tracer, authString); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(authString); + + // Both threads should still be able to get a PAT for retry purposes + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true, "The second thread caused back off when it shouldn't"); + dut.TryGetCredentials(tracer, out authString, out error).ShouldEqual(true); + } + + [TestCase] + public void TwoThreadsInterleavingFailuresStillRetriesOnce() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string thread1Auth; + string thread1AuthRetry; + string thread2Auth; + string thread2AuthRetry; + string error; + + // Populate an initial PAT on two threads + dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); + dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); + + // Simulate a 401 error on one threads + dut.RejectCredentials(tracer, thread1Auth); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); + + // That thread then retries + dut.TryGetCredentials(tracer, out thread1AuthRetry, out error).ShouldEqual(true); + + // The second thread fails with the old PAT + dut.RejectCredentials(tracer, thread2Auth); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1, "Should not have rejected a second time"); + gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth, "Should only have rejected thread1's initial credential"); + + // The second thread should be able to get a PAT + dut.TryGetCredentials(tracer, out thread2AuthRetry, out error).ShouldEqual(true, error); + } + + [TestCase] + public void TwoThreadsInterleavingFailuresShouldntStompASuccess() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string thread1Auth; + string thread2Auth; + string error; + + // Populate an initial PAT on two threads + dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); + dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); + + // Simulate a 401 error on one threads + dut.RejectCredentials(tracer, thread1Auth); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialRejections["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); + + // That thread then retries and succeeds + dut.TryGetCredentials(tracer, out thread1Auth, out error).ShouldEqual(true); + dut.ApproveCredentials(tracer, thread1Auth); + gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialApprovals["mock://repoUrl"][0].BasicAuthString.ShouldEqual(thread1Auth); + + // If the second thread fails with the old PAT, it shouldn't stomp the new PAT + dut.RejectCredentials(tracer, thread2Auth); + gitProcess.CredentialRejections["mock://repoUrl"].Count.ShouldEqual(1); + + // The second thread should be able to get a PAT + dut.TryGetCredentials(tracer, out thread2Auth, out error).ShouldEqual(true); + thread2Auth.ShouldEqual(thread1Auth, "The second thread stomp the first threads good auth string"); + } + + [TestCase] + public void DontDoubleStoreExistingCredential() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + string authString; + dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue(); + dut.ApproveCredentials(tracer, authString); + dut.ApproveCredentials(tracer, authString); + dut.ApproveCredentials(tracer, authString); + dut.ApproveCredentials(tracer, authString); + dut.ApproveCredentials(tracer, authString); + + gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialRejections.Count.ShouldEqual(0); + gitProcess.StoredCredentials.Count.ShouldEqual(1); + gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl"); + } + + [TestCase] + public void DontStoreDifferentCredentialFromCachedValue() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = this.GetGitProcess(); + + GitAuthentication dut = new GitAuthentication(gitProcess, "mock://repoUrl"); + dut.TryInitializeAndRequireAuth(tracer, out _); + + // Get and store an initial value that will be cached + string authString; + dut.TryGetCredentials(tracer, out authString, out _).ShouldBeTrue(); + dut.ApproveCredentials(tracer, authString); + + // Try and store a different value from the one that is cached + dut.ApproveCredentials(tracer, "different value"); + + gitProcess.CredentialApprovals["mock://repoUrl"].Count.ShouldEqual(1); + gitProcess.CredentialRejections.Count.ShouldEqual(0); + gitProcess.StoredCredentials.Count.ShouldEqual(1); + gitProcess.StoredCredentials.Single().Key.ShouldEqual("mock://repoUrl"); + } + + private MockGitProcess GetGitProcess() + { + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("config scalar.FunctionalTests.UserName", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); + gitProcess.SetExpectedCommandResult("config scalar.FunctionalTests.Password", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.GenericFailureCode)); + + if (this.sslSettingsPresent) + { + gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result($"http.sslCert {CertificatePath}\nhttp.sslCertPasswordProtected true\n\n", string.Empty, GitProcess.Result.SuccessCode)); + } + else + { + gitProcess.SetExpectedCommandResult("config --get-urlmatch http mock://repoUrl", () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); + } + + int approvals = 0; + int rejections = 0; + gitProcess.SetExpectedCommandResult( + $"{AzureDevOpsUseHttpPathString} credential fill", + () => new GitProcess.Result("username=username\r\npassword=password" + rejections + "\r\n", string.Empty, GitProcess.Result.SuccessCode)); + + gitProcess.SetExpectedCommandResult( + $"{AzureDevOpsUseHttpPathString} credential approve", + () => + { + approvals++; + return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode); + }); + + gitProcess.SetExpectedCommandResult( + $"{AzureDevOpsUseHttpPathString} credential reject", + () => + { + rejections++; + return new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode); + }); + return gitProcess; + } + } +} diff --git a/Scalar.UnitTests/Git/GitProcessTests.cs b/Scalar.UnitTests/Git/GitProcessTests.cs index 9d6519d972..0ccdbd6093 100644 --- a/Scalar.UnitTests/Git/GitProcessTests.cs +++ b/Scalar.UnitTests/Git/GitProcessTests.cs @@ -1,257 +1,257 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; - -namespace Scalar.UnitTests.Git -{ - [TestFixture] - public class GitProcessTests - { - [TestCase] - public void TryKillRunningProcess_NeverRan() - { - GitProcess process = new GitProcess(new MockScalarEnlistment()); - process.TryKillRunningProcess(out string processName, out int exitCode, out string error).ShouldBeTrue(); - - processName.ShouldBeNull(); - exitCode.ShouldEqual(-1); - error.ShouldBeNull(); - } - - [TestCase] - public void ResultHasNoErrors() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - string.Empty, - 0); - - result.ExitCodeIsFailure.ShouldBeFalse(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ResultHasWarnings() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - "Warning: this is fine.\n", - 0); - - result.ExitCodeIsFailure.ShouldBeFalse(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ResultHasNonWarningErrors_SingleLine_AllWarnings() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - "warning: this line should not be considered an error", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ResultHasNonWarningErrors_Multiline_AllWarnings() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - @"warning: this line should not be considered an error -WARNING: neither should this.", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ResultHasNonWarningErrors_Multiline_EmptyLines() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - @" -warning: this is fine - -warning: this is too - -", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ResultHasNonWarningErrors_Singleline_AllErrors() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - "this is an error", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeTrue(); - } - - [TestCase] - public void ResultHasNonWarningErrors_Multiline_AllErrors() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - @"error1 -error2", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeTrue(); - } - - [TestCase] - public void ResultHasNonWarningErrors_Multiline_ErrorsAndWarnings() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - @"WARNING: this is fine -this is an error", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeTrue(); - } - - [TestCase] - public void ResultHasNonWarningErrors_TrailingWhitespace_Warning() - { - GitProcess.Result result = new GitProcess.Result( - string.Empty, - "Warning: this is fine\n", - 1); - - result.ExitCodeIsFailure.ShouldBeTrue(); - result.StderrContainsErrors().ShouldBeFalse(); - } - - [TestCase] - public void ConfigResult_TryParseAsString_DefaultIsNull() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, string.Empty, 1), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue(); - expectedValue.ShouldBeNull(); - } - - [TestCase] - public void ConfigResult_TryParseAsString_FailsWhenErrors() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, "errors", 1), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string _).ShouldBeFalse(); - } - - [TestCase] - public void ConfigResult_TryParseAsString_NullWhenUnsetAndWarnings() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, "warning: ignored", 1), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue(); - expectedValue.ShouldBeNull(); - } - - [TestCase] - public void ConfigResult_TryParseAsString_PassesThroughErrors() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, "--local can only be used inside a git repository", 1), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string error).ShouldBeFalse(); - error.Contains("--local").ShouldBeTrue(); - } - - [TestCase] - public void ConfigResult_TryParseAsString_RespectsDefaultOnFailure() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, string.Empty, 1), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue(); - expectedValue.ShouldEqual("default"); - } - - [TestCase] - public void ConfigResult_TryParseAsString_OverridesDefaultOnSuccess() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result("expected", string.Empty, 0), - "settingName"); - - result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue(); - expectedValue.ShouldEqual("expected"); - } - - [TestCase] - public void ConfigResult_TryParseAsInt_FailsWithErrors() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, "errors", 1), - "settingName"); - - result.TryParseAsInt(0, -1, out int value, out string error).ShouldBeFalse(); - } - - [TestCase] - public void ConfigResult_TryParseAsInt_DefaultWhenUnset() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result(string.Empty, string.Empty, 1), - "settingName"); - - result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); - value.ShouldEqual(1); - } - - [TestCase] - public void ConfigResult_TryParseAsInt_ParsesWhenNoError() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result("32", string.Empty, 0), - "settingName"); - - result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); - value.ShouldEqual(32); - } - - [TestCase] - public void ConfigResult_TryParseAsInt_ParsesWhenWarnings() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result("32", "warning: ignored", 0), - "settingName"); - - result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); - value.ShouldEqual(32); - } - - [TestCase] - public void ConfigResult_TryParseAsInt_ParsesWhenOutputIncludesWhitespace() - { - GitProcess.ConfigResult result = new GitProcess.ConfigResult( - new GitProcess.Result("\n\t 32\t\r\n", "warning: ignored", 0), - "settingName"); - - result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); - value.ShouldEqual(32); - } - } -} +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; + +namespace Scalar.UnitTests.Git +{ + [TestFixture] + public class GitProcessTests + { + [TestCase] + public void TryKillRunningProcess_NeverRan() + { + GitProcess process = new GitProcess(new MockScalarEnlistment()); + process.TryKillRunningProcess(out string processName, out int exitCode, out string error).ShouldBeTrue(); + + processName.ShouldBeNull(); + exitCode.ShouldEqual(-1); + error.ShouldBeNull(); + } + + [TestCase] + public void ResultHasNoErrors() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + string.Empty, + 0); + + result.ExitCodeIsFailure.ShouldBeFalse(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ResultHasWarnings() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + "Warning: this is fine.\n", + 0); + + result.ExitCodeIsFailure.ShouldBeFalse(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ResultHasNonWarningErrors_SingleLine_AllWarnings() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + "warning: this line should not be considered an error", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ResultHasNonWarningErrors_Multiline_AllWarnings() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + @"warning: this line should not be considered an error +WARNING: neither should this.", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ResultHasNonWarningErrors_Multiline_EmptyLines() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + @" +warning: this is fine + +warning: this is too + +", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ResultHasNonWarningErrors_Singleline_AllErrors() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + "this is an error", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeTrue(); + } + + [TestCase] + public void ResultHasNonWarningErrors_Multiline_AllErrors() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + @"error1 +error2", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeTrue(); + } + + [TestCase] + public void ResultHasNonWarningErrors_Multiline_ErrorsAndWarnings() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + @"WARNING: this is fine +this is an error", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeTrue(); + } + + [TestCase] + public void ResultHasNonWarningErrors_TrailingWhitespace_Warning() + { + GitProcess.Result result = new GitProcess.Result( + string.Empty, + "Warning: this is fine\n", + 1); + + result.ExitCodeIsFailure.ShouldBeTrue(); + result.StderrContainsErrors().ShouldBeFalse(); + } + + [TestCase] + public void ConfigResult_TryParseAsString_DefaultIsNull() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, string.Empty, 1), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue(); + expectedValue.ShouldBeNull(); + } + + [TestCase] + public void ConfigResult_TryParseAsString_FailsWhenErrors() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, "errors", 1), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string _).ShouldBeFalse(); + } + + [TestCase] + public void ConfigResult_TryParseAsString_NullWhenUnsetAndWarnings() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, "warning: ignored", 1), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string _).ShouldBeTrue(); + expectedValue.ShouldBeNull(); + } + + [TestCase] + public void ConfigResult_TryParseAsString_PassesThroughErrors() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, "--local can only be used inside a git repository", 1), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string error).ShouldBeFalse(); + error.Contains("--local").ShouldBeTrue(); + } + + [TestCase] + public void ConfigResult_TryParseAsString_RespectsDefaultOnFailure() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, string.Empty, 1), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue(); + expectedValue.ShouldEqual("default"); + } + + [TestCase] + public void ConfigResult_TryParseAsString_OverridesDefaultOnSuccess() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result("expected", string.Empty, 0), + "settingName"); + + result.TryParseAsString(out string expectedValue, out string _, "default").ShouldBeTrue(); + expectedValue.ShouldEqual("expected"); + } + + [TestCase] + public void ConfigResult_TryParseAsInt_FailsWithErrors() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, "errors", 1), + "settingName"); + + result.TryParseAsInt(0, -1, out int value, out string error).ShouldBeFalse(); + } + + [TestCase] + public void ConfigResult_TryParseAsInt_DefaultWhenUnset() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result(string.Empty, string.Empty, 1), + "settingName"); + + result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); + value.ShouldEqual(1); + } + + [TestCase] + public void ConfigResult_TryParseAsInt_ParsesWhenNoError() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result("32", string.Empty, 0), + "settingName"); + + result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); + value.ShouldEqual(32); + } + + [TestCase] + public void ConfigResult_TryParseAsInt_ParsesWhenWarnings() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result("32", "warning: ignored", 0), + "settingName"); + + result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); + value.ShouldEqual(32); + } + + [TestCase] + public void ConfigResult_TryParseAsInt_ParsesWhenOutputIncludesWhitespace() + { + GitProcess.ConfigResult result = new GitProcess.ConfigResult( + new GitProcess.Result("\n\t 32\t\r\n", "warning: ignored", 0), + "settingName"); + + result.TryParseAsInt(1, -1, out int value, out string error).ShouldBeTrue(); + value.ShouldEqual(32); + } + } +} diff --git a/Scalar.UnitTests/Git/ScalarGitObjectsTests.cs b/Scalar.UnitTests/Git/ScalarGitObjectsTests.cs index 9e6e578ca4..ae3df1e8fd 100644 --- a/Scalar.UnitTests/Git/ScalarGitObjectsTests.cs +++ b/Scalar.UnitTests/Git/ScalarGitObjectsTests.cs @@ -1,189 +1,189 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using Scalar.UnitTests.Mock.Git; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Reflection; -using System.Threading; - -namespace Scalar.UnitTests.Git -{ - [TestFixture] - public class ScalarGitObjectsTests - { - private const string ValidTestObjectFileContents = "421dc4df5e1de427e363b8acd9ddb2d41385dbdf"; - private const string TestEnlistmentRoot = "mock:\\src"; - private const string TestLocalCacheRoot = "mock:\\.scalar"; - private const string TestObjectRoot = "mock:\\.scalar\\gitObjectCache"; - - [TestCase] - public void SucceedsForNormalLookingLooseObjectDownloads() - { - MockFileSystemWithCallbacks fileSystem = new Mock.FileSystem.MockFileSystemWithCallbacks(); - fileSystem.OnFileExists = () => true; - fileSystem.OnOpenFileStream = (path, mode, access) => new MemoryStream(); - MockHttpGitObjects httpObjects = new MockHttpGitObjects(); - using (httpObjects.InputStream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(ValidTestObjectFileContents))) - { - httpObjects.MediaType = ScalarConstants.MediaTypes.LooseObjectMediaType; - ScalarGitObjects dut = this.CreateTestableScalarGitObjects(httpObjects, fileSystem); - - dut.TryDownloadAndSaveObject(ValidTestObjectFileContents, ScalarGitObjects.RequestSource.FileStreamCallback) - .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success); - } - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void FailsZeroByteLooseObjectsDownloads() - { - this.AssertRetryableExceptionOnDownload( - new MemoryStream(), - ScalarConstants.MediaTypes.LooseObjectMediaType, - gitObjects => gitObjects.TryDownloadAndSaveObject("aabbcc", ScalarGitObjects.RequestSource.FileStreamCallback)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void FailsNullByteLooseObjectsDownloads() - { - this.AssertRetryableExceptionOnDownload( - new MemoryStream(new byte[256]), - ScalarConstants.MediaTypes.LooseObjectMediaType, - gitObjects => gitObjects.TryDownloadAndSaveObject("aabbcc", ScalarGitObjects.RequestSource.FileStreamCallback)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void FailsZeroBytePackDownloads() - { - this.AssertRetryableExceptionOnDownload( - new MemoryStream(), - ScalarConstants.MediaTypes.PackFileMediaType, - gitObjects => gitObjects.TryDownloadCommit("object0")); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void FailsNullBytePackDownloads() - { - this.AssertRetryableExceptionOnDownload( - new MemoryStream(new byte[256]), - ScalarConstants.MediaTypes.PackFileMediaType, - gitObjects => gitObjects.TryDownloadCommit("object0")); - } - - private void AssertRetryableExceptionOnDownload( - MemoryStream inputStream, - string mediaType, - Action download) - { - MockHttpGitObjects httpObjects = new MockHttpGitObjects(); - httpObjects.InputStream = inputStream; - httpObjects.MediaType = mediaType; - MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks(); - - using (ReusableMemoryStream downloadDestination = new ReusableMemoryStream(string.Empty)) - { - fileSystem.OnFileExists = () => false; - fileSystem.OnOpenFileStream = (path, mode, access) => downloadDestination; - - ScalarGitObjects gitObjects = this.CreateTestableScalarGitObjects(httpObjects, fileSystem); - - Assert.Throws(() => download(gitObjects)); - inputStream.Dispose(); - } - } - - private ScalarGitObjects CreateTestableScalarGitObjects(MockHttpGitObjects httpObjects, MockFileSystemWithCallbacks fileSystem) - { - MockTracer tracer = new MockTracer(); - ScalarEnlistment enlistment = new ScalarEnlistment(TestEnlistmentRoot, "https://fakeRepoUrl", "fakeGitBinPath", authentication: null); - enlistment.InitializeCachePathsFromKey(TestLocalCacheRoot, TestObjectRoot); - GitRepo repo = new GitRepo(tracer, enlistment, fileSystem, () => new MockLibGit2Repo(tracer)); - - ScalarContext context = new ScalarContext(tracer, fileSystem, repo, enlistment); - ScalarGitObjects dut = new ScalarGitObjects(context, httpObjects); - return dut; - } - - private string GetDataPath(string fileName) - { - string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - return Path.Combine(workingDirectory, "Data", fileName); - } - - private class MockHttpGitObjects : GitObjectsHttpRequestor - { - public MockHttpGitObjects() - : this(new MockScalarEnlistment()) - { - } - - private MockHttpGitObjects(MockScalarEnlistment enlistment) - : base(new MockTracer(), enlistment, new MockCacheServerInfo(), new RetryConfig(maxRetries: 1)) - { - } - - public Stream InputStream { get; set; } - public string MediaType { get; set; } - - public static MemoryStream GetRandomStream(int size) - { - Random randy = new Random(0); - MemoryStream stream = new MemoryStream(); - byte[] buffer = new byte[size]; - - randy.NextBytes(buffer); - stream.Write(buffer, 0, buffer.Length); - - stream.Position = 0; - return stream; - } - - public override RetryWrapper.InvocationResult TryDownloadLooseObject( - string objectId, - bool retryOnFailure, - CancellationToken cancellationToken, - string requestSource, - Func.CallbackResult> onSuccess) - { - return this.TryDownloadObjects(new[] { objectId }, onSuccess, null, false); - } - - public override RetryWrapper.InvocationResult TryDownloadObjects( - IEnumerable objectIds, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - using (GitEndPointResponseData response = new GitEndPointResponseData( - HttpStatusCode.OK, - this.MediaType, - this.InputStream, - message: null, - onResponseDisposed: null)) - { - onSuccess(0, response); - } - - GitObjectTaskResult result = new GitObjectTaskResult(true); - return new RetryWrapper.InvocationResult(0, true, result); - } - - public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - } - } +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using Scalar.UnitTests.Mock.Git; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Reflection; +using System.Threading; + +namespace Scalar.UnitTests.Git +{ + [TestFixture] + public class ScalarGitObjectsTests + { + private const string ValidTestObjectFileContents = "421dc4df5e1de427e363b8acd9ddb2d41385dbdf"; + private const string TestEnlistmentRoot = "mock:\\src"; + private const string TestLocalCacheRoot = "mock:\\.scalar"; + private const string TestObjectRoot = "mock:\\.scalar\\gitObjectCache"; + + [TestCase] + public void SucceedsForNormalLookingLooseObjectDownloads() + { + MockFileSystemWithCallbacks fileSystem = new Mock.FileSystem.MockFileSystemWithCallbacks(); + fileSystem.OnFileExists = () => true; + fileSystem.OnOpenFileStream = (path, mode, access) => new MemoryStream(); + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + using (httpObjects.InputStream = new MemoryStream(System.Text.Encoding.ASCII.GetBytes(ValidTestObjectFileContents))) + { + httpObjects.MediaType = ScalarConstants.MediaTypes.LooseObjectMediaType; + ScalarGitObjects dut = this.CreateTestableScalarGitObjects(httpObjects, fileSystem); + + dut.TryDownloadAndSaveObject(ValidTestObjectFileContents, ScalarGitObjects.RequestSource.FileStreamCallback) + .ShouldEqual(GitObjects.DownloadAndSaveObjectResult.Success); + } + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void FailsZeroByteLooseObjectsDownloads() + { + this.AssertRetryableExceptionOnDownload( + new MemoryStream(), + ScalarConstants.MediaTypes.LooseObjectMediaType, + gitObjects => gitObjects.TryDownloadAndSaveObject("aabbcc", ScalarGitObjects.RequestSource.FileStreamCallback)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void FailsNullByteLooseObjectsDownloads() + { + this.AssertRetryableExceptionOnDownload( + new MemoryStream(new byte[256]), + ScalarConstants.MediaTypes.LooseObjectMediaType, + gitObjects => gitObjects.TryDownloadAndSaveObject("aabbcc", ScalarGitObjects.RequestSource.FileStreamCallback)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void FailsZeroBytePackDownloads() + { + this.AssertRetryableExceptionOnDownload( + new MemoryStream(), + ScalarConstants.MediaTypes.PackFileMediaType, + gitObjects => gitObjects.TryDownloadCommit("object0")); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void FailsNullBytePackDownloads() + { + this.AssertRetryableExceptionOnDownload( + new MemoryStream(new byte[256]), + ScalarConstants.MediaTypes.PackFileMediaType, + gitObjects => gitObjects.TryDownloadCommit("object0")); + } + + private void AssertRetryableExceptionOnDownload( + MemoryStream inputStream, + string mediaType, + Action download) + { + MockHttpGitObjects httpObjects = new MockHttpGitObjects(); + httpObjects.InputStream = inputStream; + httpObjects.MediaType = mediaType; + MockFileSystemWithCallbacks fileSystem = new MockFileSystemWithCallbacks(); + + using (ReusableMemoryStream downloadDestination = new ReusableMemoryStream(string.Empty)) + { + fileSystem.OnFileExists = () => false; + fileSystem.OnOpenFileStream = (path, mode, access) => downloadDestination; + + ScalarGitObjects gitObjects = this.CreateTestableScalarGitObjects(httpObjects, fileSystem); + + Assert.Throws(() => download(gitObjects)); + inputStream.Dispose(); + } + } + + private ScalarGitObjects CreateTestableScalarGitObjects(MockHttpGitObjects httpObjects, MockFileSystemWithCallbacks fileSystem) + { + MockTracer tracer = new MockTracer(); + ScalarEnlistment enlistment = new ScalarEnlistment(TestEnlistmentRoot, "https://fakeRepoUrl", "fakeGitBinPath", authentication: null); + enlistment.InitializeCachePathsFromKey(TestLocalCacheRoot, TestObjectRoot); + GitRepo repo = new GitRepo(tracer, enlistment, fileSystem, () => new MockLibGit2Repo(tracer)); + + ScalarContext context = new ScalarContext(tracer, fileSystem, repo, enlistment); + ScalarGitObjects dut = new ScalarGitObjects(context, httpObjects); + return dut; + } + + private string GetDataPath(string fileName) + { + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(workingDirectory, "Data", fileName); + } + + private class MockHttpGitObjects : GitObjectsHttpRequestor + { + public MockHttpGitObjects() + : this(new MockScalarEnlistment()) + { + } + + private MockHttpGitObjects(MockScalarEnlistment enlistment) + : base(new MockTracer(), enlistment, new MockCacheServerInfo(), new RetryConfig(maxRetries: 1)) + { + } + + public Stream InputStream { get; set; } + public string MediaType { get; set; } + + public static MemoryStream GetRandomStream(int size) + { + Random randy = new Random(0); + MemoryStream stream = new MemoryStream(); + byte[] buffer = new byte[size]; + + randy.NextBytes(buffer); + stream.Write(buffer, 0, buffer.Length); + + stream.Position = 0; + return stream; + } + + public override RetryWrapper.InvocationResult TryDownloadLooseObject( + string objectId, + bool retryOnFailure, + CancellationToken cancellationToken, + string requestSource, + Func.CallbackResult> onSuccess) + { + return this.TryDownloadObjects(new[] { objectId }, onSuccess, null, false); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + using (GitEndPointResponseData response = new GitEndPointResponseData( + HttpStatusCode.OK, + this.MediaType, + this.InputStream, + message: null, + onResponseDisposed: null)) + { + onSuccess(0, response); + } + + GitObjectTaskResult result = new GitObjectTaskResult(true); + return new RetryWrapper.InvocationResult(0, true, result); + } + + public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + } + } } diff --git a/Scalar.UnitTests/Maintenance/GitMaintenanceQueueTests.cs b/Scalar.UnitTests/Maintenance/GitMaintenanceQueueTests.cs index aa376d9b1f..1d52666162 100644 --- a/Scalar.UnitTests/Maintenance/GitMaintenanceQueueTests.cs +++ b/Scalar.UnitTests/Maintenance/GitMaintenanceQueueTests.cs @@ -1,184 +1,184 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Maintenance; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using System.Collections.Generic; -using System.Threading; - -namespace Scalar.UnitTests.Maintenance -{ - [TestFixture] - public class GitMaintenanceQueueTests - { - private int maxWaitTime = 500; - private ReadyFileSystem fileSystem; - private ScalarEnlistment enlistment; - private ScalarContext context; - private GitObjects gitObjects; - - [TestCase] - public void GitMaintenanceQueueEnlistmentRootReady() - { - this.TestSetup(); - - GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); - queue.EnlistmentRootReady().ShouldBeTrue(); - - this.fileSystem.Paths.Remove(this.enlistment.EnlistmentRoot); - queue.EnlistmentRootReady().ShouldBeFalse(); - - this.fileSystem.Paths.Remove(this.enlistment.GitObjectsRoot); - queue.EnlistmentRootReady().ShouldBeFalse(); - - this.fileSystem.Paths.Add(this.enlistment.EnlistmentRoot); - queue.EnlistmentRootReady().ShouldBeFalse(); - - this.fileSystem.Paths.Add(this.enlistment.GitObjectsRoot); - queue.EnlistmentRootReady().ShouldBeTrue(); - - queue.Stop(); - } - - [TestCase] - public void GitMaintenanceQueueHandlesTwoJobs() - { - this.TestSetup(); - - TestGitMaintenanceStep step1 = new TestGitMaintenanceStep(this.context); - TestGitMaintenanceStep step2 = new TestGitMaintenanceStep(this.context); - - GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); - - queue.TryEnqueue(step1); - queue.TryEnqueue(step2); - - step1.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue(); - step2.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue(); - - queue.Stop(); - - step1.NumberOfExecutions.ShouldEqual(1); - step2.NumberOfExecutions.ShouldEqual(1); - } - - [TestCase] - public void GitMaintenanceQueueStopSuceedsWhenQueueIsEmpty() - { - this.TestSetup(); - - GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); - - queue.Stop(); - - TestGitMaintenanceStep step = new TestGitMaintenanceStep(this.context); - queue.TryEnqueue(step).ShouldEqual(false); - } - - [TestCase] - public void GitMaintenanceQueueStopsJob() - { - this.TestSetup(); - - GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); - - // This step stops the queue after the step is started, - // then checks if Stop() was called. - WatchForStopStep watchForStop = new WatchForStopStep(queue, this.context); - - queue.TryEnqueue(watchForStop); - Assert.IsTrue(watchForStop.EventTriggered.WaitOne(this.maxWaitTime)); - watchForStop.SawStopping.ShouldBeTrue(); - - // Ensure we don't start a job after the Stop() call - TestGitMaintenanceStep watchForStart = new TestGitMaintenanceStep(this.context); - queue.TryEnqueue(watchForStart).ShouldBeFalse(); - - // This only ensures the event didn't happen within maxWaitTime - Assert.IsFalse(watchForStart.EventTriggered.WaitOne(this.maxWaitTime)); - - queue.Stop(); - } - - private void TestSetup() - { - ITracer tracer = new MockTracer(); - this.enlistment = new MockScalarEnlistment(); - - // We need to have the EnlistmentRoot and GitObjectsRoot available for jobs to run - this.fileSystem = new ReadyFileSystem(new string[] - { - this.enlistment.EnlistmentRoot, - this.enlistment.GitObjectsRoot - }); - - this.context = new ScalarContext(tracer, this.fileSystem, null, this.enlistment); - this.gitObjects = new MockPhysicalGitObjects(tracer, this.fileSystem, this.enlistment, null); - } - - public class ReadyFileSystem : PhysicalFileSystem - { - public ReadyFileSystem(IEnumerable paths) - { - this.Paths = new HashSet(paths); - } - - public HashSet Paths { get; } - - public override bool DirectoryExists(string path) - { - return this.Paths.Contains(path); - } - } - - public class TestGitMaintenanceStep : GitMaintenanceStep - { - public TestGitMaintenanceStep(ScalarContext context) - : base(context, requireObjectCacheLock: true) - { - this.EventTriggered = new ManualResetEvent(initialState: false); - } - - public ManualResetEvent EventTriggered { get; set; } - public int NumberOfExecutions { get; set; } - - public override string Area => "TestGitMaintenanceStep"; - - protected override void PerformMaintenance() - { - this.NumberOfExecutions++; - this.EventTriggered.Set(); - } - } - - private class WatchForStopStep : GitMaintenanceStep - { - public WatchForStopStep(GitMaintenanceQueue queue, ScalarContext context) - : base(context, requireObjectCacheLock: true) - { - this.Queue = queue; - this.EventTriggered = new ManualResetEvent(false); - } - - public GitMaintenanceQueue Queue { get; set; } - - public bool SawStopping { get; private set; } - - public ManualResetEvent EventTriggered { get; private set; } - - public override string Area => "WatchForStopStep"; - - protected override void PerformMaintenance() - { - this.Queue.Stop(); - - this.SawStopping = this.Stopping; - - this.EventTriggered.Set(); - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Maintenance; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using System.Collections.Generic; +using System.Threading; + +namespace Scalar.UnitTests.Maintenance +{ + [TestFixture] + public class GitMaintenanceQueueTests + { + private int maxWaitTime = 500; + private ReadyFileSystem fileSystem; + private ScalarEnlistment enlistment; + private ScalarContext context; + private GitObjects gitObjects; + + [TestCase] + public void GitMaintenanceQueueEnlistmentRootReady() + { + this.TestSetup(); + + GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); + queue.EnlistmentRootReady().ShouldBeTrue(); + + this.fileSystem.Paths.Remove(this.enlistment.EnlistmentRoot); + queue.EnlistmentRootReady().ShouldBeFalse(); + + this.fileSystem.Paths.Remove(this.enlistment.GitObjectsRoot); + queue.EnlistmentRootReady().ShouldBeFalse(); + + this.fileSystem.Paths.Add(this.enlistment.EnlistmentRoot); + queue.EnlistmentRootReady().ShouldBeFalse(); + + this.fileSystem.Paths.Add(this.enlistment.GitObjectsRoot); + queue.EnlistmentRootReady().ShouldBeTrue(); + + queue.Stop(); + } + + [TestCase] + public void GitMaintenanceQueueHandlesTwoJobs() + { + this.TestSetup(); + + TestGitMaintenanceStep step1 = new TestGitMaintenanceStep(this.context); + TestGitMaintenanceStep step2 = new TestGitMaintenanceStep(this.context); + + GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); + + queue.TryEnqueue(step1); + queue.TryEnqueue(step2); + + step1.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue(); + step2.EventTriggered.WaitOne(this.maxWaitTime).ShouldBeTrue(); + + queue.Stop(); + + step1.NumberOfExecutions.ShouldEqual(1); + step2.NumberOfExecutions.ShouldEqual(1); + } + + [TestCase] + public void GitMaintenanceQueueStopSuceedsWhenQueueIsEmpty() + { + this.TestSetup(); + + GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); + + queue.Stop(); + + TestGitMaintenanceStep step = new TestGitMaintenanceStep(this.context); + queue.TryEnqueue(step).ShouldEqual(false); + } + + [TestCase] + public void GitMaintenanceQueueStopsJob() + { + this.TestSetup(); + + GitMaintenanceQueue queue = new GitMaintenanceQueue(this.context); + + // This step stops the queue after the step is started, + // then checks if Stop() was called. + WatchForStopStep watchForStop = new WatchForStopStep(queue, this.context); + + queue.TryEnqueue(watchForStop); + Assert.IsTrue(watchForStop.EventTriggered.WaitOne(this.maxWaitTime)); + watchForStop.SawStopping.ShouldBeTrue(); + + // Ensure we don't start a job after the Stop() call + TestGitMaintenanceStep watchForStart = new TestGitMaintenanceStep(this.context); + queue.TryEnqueue(watchForStart).ShouldBeFalse(); + + // This only ensures the event didn't happen within maxWaitTime + Assert.IsFalse(watchForStart.EventTriggered.WaitOne(this.maxWaitTime)); + + queue.Stop(); + } + + private void TestSetup() + { + ITracer tracer = new MockTracer(); + this.enlistment = new MockScalarEnlistment(); + + // We need to have the EnlistmentRoot and GitObjectsRoot available for jobs to run + this.fileSystem = new ReadyFileSystem(new string[] + { + this.enlistment.EnlistmentRoot, + this.enlistment.GitObjectsRoot + }); + + this.context = new ScalarContext(tracer, this.fileSystem, null, this.enlistment); + this.gitObjects = new MockPhysicalGitObjects(tracer, this.fileSystem, this.enlistment, null); + } + + public class ReadyFileSystem : PhysicalFileSystem + { + public ReadyFileSystem(IEnumerable paths) + { + this.Paths = new HashSet(paths); + } + + public HashSet Paths { get; } + + public override bool DirectoryExists(string path) + { + return this.Paths.Contains(path); + } + } + + public class TestGitMaintenanceStep : GitMaintenanceStep + { + public TestGitMaintenanceStep(ScalarContext context) + : base(context, requireObjectCacheLock: true) + { + this.EventTriggered = new ManualResetEvent(initialState: false); + } + + public ManualResetEvent EventTriggered { get; set; } + public int NumberOfExecutions { get; set; } + + public override string Area => "TestGitMaintenanceStep"; + + protected override void PerformMaintenance() + { + this.NumberOfExecutions++; + this.EventTriggered.Set(); + } + } + + private class WatchForStopStep : GitMaintenanceStep + { + public WatchForStopStep(GitMaintenanceQueue queue, ScalarContext context) + : base(context, requireObjectCacheLock: true) + { + this.Queue = queue; + this.EventTriggered = new ManualResetEvent(false); + } + + public GitMaintenanceQueue Queue { get; set; } + + public bool SawStopping { get; private set; } + + public ManualResetEvent EventTriggered { get; private set; } + + public override string Area => "WatchForStopStep"; + + protected override void PerformMaintenance() + { + this.Queue.Stop(); + + this.SawStopping = this.Stopping; + + this.EventTriggered.Set(); + } + } + } +} diff --git a/Scalar.UnitTests/Maintenance/GitMaintenanceStepTests.cs b/Scalar.UnitTests/Maintenance/GitMaintenanceStepTests.cs index ea88e7196d..96f0bac315 100644 --- a/Scalar.UnitTests/Maintenance/GitMaintenanceStepTests.cs +++ b/Scalar.UnitTests/Maintenance/GitMaintenanceStepTests.cs @@ -1,128 +1,128 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Maintenance; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; - -namespace Scalar.UnitTests.Maintenance -{ - [TestFixture] - public class GitMaintenanceStepTests - { - private ScalarContext context; - - public enum WhenToStop - { - Never, - BeforeGitCommand, - DuringGitCommand - } - - [TestCase] - public void GitMaintenanceStepRunsGitAction() - { - this.TestSetup(); - - CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never); - step.Execute(); - - step.SawWorkInvoked.ShouldBeTrue(); - step.SawEndOfMethod.ShouldBeTrue(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void GitMaintenanceStepSkipsGitActionAfterStop() - { - this.TestSetup(); - - CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never); - - step.Stop(); - step.Execute(); - - step.SawWorkInvoked.ShouldBeFalse(); - step.SawEndOfMethod.ShouldBeFalse(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void GitMaintenanceStepSkipsRunGitCommandAfterStop() - { - this.TestSetup(); - - CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.BeforeGitCommand); - - step.Execute(); - - step.SawWorkInvoked.ShouldBeFalse(); - step.SawEndOfMethod.ShouldBeFalse(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void GitMaintenanceStepThrowsIfStoppedDuringGitCommand() - { - this.TestSetup(); - CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.DuringGitCommand); - - step.Execute(); - - step.SawWorkInvoked.ShouldBeTrue(); - step.SawEndOfMethod.ShouldBeFalse(); - } - - private void TestSetup() - { - ITracer tracer = new MockTracer(); - ScalarEnlistment enlistment = new MockScalarEnlistment(); - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null)); - - this.context = new ScalarContext(tracer, fileSystem, null, enlistment); - } - - public class CheckMethodStep : GitMaintenanceStep - { - private WhenToStop when; - - public CheckMethodStep(ScalarContext context, WhenToStop when) - : base(context, requireObjectCacheLock: true) - { - this.when = when; - } - - public bool SawWorkInvoked { get; set; } - public bool SawEndOfMethod { get; set; } - - public override string Area => "CheckMethodStep"; - - protected override void PerformMaintenance() - { - if (this.when == WhenToStop.BeforeGitCommand) - { - this.Stop(); - } - - this.RunGitCommand( - process => - { - this.SawWorkInvoked = true; - - if (this.when == WhenToStop.DuringGitCommand) - { - this.Stop(); - } - - return null; - }, - nameof(this.SawWorkInvoked)); - - this.SawEndOfMethod = true; - } - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Maintenance; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; + +namespace Scalar.UnitTests.Maintenance +{ + [TestFixture] + public class GitMaintenanceStepTests + { + private ScalarContext context; + + public enum WhenToStop + { + Never, + BeforeGitCommand, + DuringGitCommand + } + + [TestCase] + public void GitMaintenanceStepRunsGitAction() + { + this.TestSetup(); + + CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never); + step.Execute(); + + step.SawWorkInvoked.ShouldBeTrue(); + step.SawEndOfMethod.ShouldBeTrue(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void GitMaintenanceStepSkipsGitActionAfterStop() + { + this.TestSetup(); + + CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.Never); + + step.Stop(); + step.Execute(); + + step.SawWorkInvoked.ShouldBeFalse(); + step.SawEndOfMethod.ShouldBeFalse(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void GitMaintenanceStepSkipsRunGitCommandAfterStop() + { + this.TestSetup(); + + CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.BeforeGitCommand); + + step.Execute(); + + step.SawWorkInvoked.ShouldBeFalse(); + step.SawEndOfMethod.ShouldBeFalse(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void GitMaintenanceStepThrowsIfStoppedDuringGitCommand() + { + this.TestSetup(); + CheckMethodStep step = new CheckMethodStep(this.context, WhenToStop.DuringGitCommand); + + step.Execute(); + + step.SawWorkInvoked.ShouldBeTrue(); + step.SawEndOfMethod.ShouldBeFalse(); + } + + private void TestSetup() + { + ITracer tracer = new MockTracer(); + ScalarEnlistment enlistment = new MockScalarEnlistment(); + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, null, null)); + + this.context = new ScalarContext(tracer, fileSystem, null, enlistment); + } + + public class CheckMethodStep : GitMaintenanceStep + { + private WhenToStop when; + + public CheckMethodStep(ScalarContext context, WhenToStop when) + : base(context, requireObjectCacheLock: true) + { + this.when = when; + } + + public bool SawWorkInvoked { get; set; } + public bool SawEndOfMethod { get; set; } + + public override string Area => "CheckMethodStep"; + + protected override void PerformMaintenance() + { + if (this.when == WhenToStop.BeforeGitCommand) + { + this.Stop(); + } + + this.RunGitCommand( + process => + { + this.SawWorkInvoked = true; + + if (this.when == WhenToStop.DuringGitCommand) + { + this.Stop(); + } + + return null; + }, + nameof(this.SawWorkInvoked)); + + this.SawEndOfMethod = true; + } + } + } +} diff --git a/Scalar.UnitTests/Maintenance/LooseObjectStepTests.cs b/Scalar.UnitTests/Maintenance/LooseObjectStepTests.cs index aca4a2621d..e5594ce774 100644 --- a/Scalar.UnitTests/Maintenance/LooseObjectStepTests.cs +++ b/Scalar.UnitTests/Maintenance/LooseObjectStepTests.cs @@ -1,258 +1,258 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Maintenance; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using Scalar.UnitTests.Mock.Git; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.UnitTests.Maintenance -{ - [TestFixture] - public class LooseObjectStepTests - { - private const string PrunePackedCommand = "prune-packed -q"; - private string packCommand; - private MockTracer tracer; - private MockGitProcess gitProcess; - private ScalarContext context; - - [TestCase] - public void LooseObjectsIgnoreTimeRestriction() - { - this.TestSetup(DateTime.UtcNow); - - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: true); - step.Execute(); - - this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0); - List commands = this.gitProcess.CommandsRun; - commands.Count.ShouldEqual(2); - commands[0].ShouldEqual(PrunePackedCommand); - commands[1].ShouldEqual(this.packCommand); - } - - [TestCase] - public void LooseObjectsFailTimeRestriction() - { - this.TestSetup(DateTime.UtcNow); - - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - step.Execute(); - - this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1); - List commands = this.gitProcess.CommandsRun; - commands.Count.ShouldEqual(0); - } - - [TestCase] - public void LooseObjectsPassTimeRestriction() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - Mock mockChecker = new Mock(); - mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) - .Returns(Array.Empty()); - - LooseObjectsStep step = new LooseObjectsStep( - this.context, - requireCacheLock: false, - forceRun: false, - gitProcessChecker: mockChecker.Object); - step.Execute(); - - mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); - - this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0); - List commands = this.gitProcess.CommandsRun; - commands.Count.ShouldEqual(2); - commands[0].ShouldEqual(PrunePackedCommand); - commands[1].ShouldEqual(this.packCommand); - } - - [TestCase] - public void LooseObjectsFailGitProcessIds() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - Mock mockChecker = new Mock(); - mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) - .Returns(new int[] { 1 }); - - LooseObjectsStep step = new LooseObjectsStep( - this.context, - requireCacheLock: false, - forceRun: false, - gitProcessChecker: mockChecker.Object); - step.Execute(); - - mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); - - this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1); - List commands = this.gitProcess.CommandsRun; - commands.Count.ShouldEqual(0); - } - - [TestCase] - public void LooseObjectsLimitPackCount() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - // Verify with default limit - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); - - // Verify with limit of 2 - step.MaxLooseObjectsInPack = 2; - step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(2); - } - - [TestCase] - public void SkipInvalidLooseObjects() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - // Verify with valid Objects - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); - this.tracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.RelatedWarningEvents.Count.ShouldEqual(0); - - // Write an ObjectId file with an invalid name - this.context.FileSystem.WriteAllText(Path.Combine(this.context.Enlistment.GitObjectsRoot, "AA", "NOT_A_SHA"), string.Empty); - - // Verify it wasn't added and a warning exists - step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); - this.tracer.RelatedErrorEvents.Count.ShouldEqual(0); - this.tracer.RelatedWarningEvents.Count.ShouldEqual(1); - } - - [TestCase] - public void LooseObjectsCount() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - step.CountLooseObjects(out int count, out long size); - - count.ShouldEqual(3); - size.ShouldEqual("one".Length + "two".Length + "three".Length); - } - - [TestCase] - public void LooseObjectId() - { - this.TestSetup(DateTime.UtcNow.AddDays(-7)); - - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - string directoryName = "AB"; - string fileName = "830bb79cd4fadb2e73e780e452dc71db909001"; - step.TryGetLooseObjectId( - directoryName, - Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName), - out string objectId).ShouldBeTrue(); - objectId.ShouldEqual(directoryName + fileName); - - directoryName = "AB"; - fileName = "BAD_FILE_NAME"; - step.TryGetLooseObjectId( - directoryName, - Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName), - out objectId).ShouldBeFalse(); - } - - [TestCase] - public void LooseObjectFileName() - { - this.TestSetup(DateTime.UtcNow); - LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); - - step.GetLooseObjectFileName("0123456789012345678901234567890123456789") - .ShouldEqual(Path.Combine(this.context.Enlistment.GitObjectsRoot, "01", "23456789012345678901234567890123456789")); - } - - private void TestSetup(DateTime lastRun) - { - string lastRunTime = EpochConverter.ToUnixEpochSeconds(lastRun).ToString(); - - // Create GitProcess - this.gitProcess = new MockGitProcess(); - this.gitProcess.SetExpectedCommandResult( - PrunePackedCommand, - () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); - - // Create enlistment using git process - ScalarEnlistment enlistment = new MockScalarEnlistment(this.gitProcess); - - string packPrefix = Path.Combine(enlistment.GitPackRoot, "from-loose"); - this.packCommand = $"pack-objects {packPrefix} --non-empty --window=0 --depth=0 -q"; - - this.gitProcess.SetExpectedCommandResult( - this.packCommand, - () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); - - // Create a last run time file - MockFile timeFile = new MockFile(Path.Combine(enlistment.GitObjectsRoot, "info", LooseObjectsStep.LooseObjectsLastRunFileName), lastRunTime); - - // Create info directory to hold last run time file - MockDirectory infoRoot = new MockDirectory(Path.Combine(enlistment.GitObjectsRoot, "info"), null, new List() { timeFile }); - - // Create Hex Folder 1 with 1 File - MockDirectory hex1 = new MockDirectory( - Path.Combine(enlistment.GitObjectsRoot, "AA"), - null, - new List() - { - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "AA", "1156f4f2b850673090c285289ea8475d629fe1"), "one") - }); - - // Create Hex Folder 2 with 2 Files - MockDirectory hex2 = new MockDirectory( - Path.Combine(enlistment.GitObjectsRoot, "F1"), - null, - new List() - { - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe2"), "two"), - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe3"), "three") - }); - - // Create NonHex Folder with 4 Files - MockDirectory nonhex = new MockDirectory( - Path.Combine(enlistment.GitObjectsRoot, "ZZ"), - null, - new List() - { - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe4"), "4"), - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe5"), "5"), - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe6"), "6"), - new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe7"), "7") - }); - - MockDirectory pack = new MockDirectory( - enlistment.GitPackRoot, - null, - new List()); - - // Create git objects directory - MockDirectory gitObjectsRoot = new MockDirectory(enlistment.GitObjectsRoot, new List() { infoRoot, hex1, hex2, nonhex, pack }, null); - - // Add object directory to file System - List directories = new List() { gitObjectsRoot }; - PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null)); - - // Create and return Context - this.tracer = new MockTracer(); - this.context = new ScalarContext(this.tracer, fileSystem, repository: null, enlistment: enlistment); - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Maintenance; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using Scalar.UnitTests.Mock.Git; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Maintenance +{ + [TestFixture] + public class LooseObjectStepTests + { + private const string PrunePackedCommand = "prune-packed -q"; + private string packCommand; + private MockTracer tracer; + private MockGitProcess gitProcess; + private ScalarContext context; + + [TestCase] + public void LooseObjectsIgnoreTimeRestriction() + { + this.TestSetup(DateTime.UtcNow); + + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: true); + step.Execute(); + + this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0); + List commands = this.gitProcess.CommandsRun; + commands.Count.ShouldEqual(2); + commands[0].ShouldEqual(PrunePackedCommand); + commands[1].ShouldEqual(this.packCommand); + } + + [TestCase] + public void LooseObjectsFailTimeRestriction() + { + this.TestSetup(DateTime.UtcNow); + + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + step.Execute(); + + this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1); + List commands = this.gitProcess.CommandsRun; + commands.Count.ShouldEqual(0); + } + + [TestCase] + public void LooseObjectsPassTimeRestriction() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + Mock mockChecker = new Mock(); + mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) + .Returns(Array.Empty()); + + LooseObjectsStep step = new LooseObjectsStep( + this.context, + requireCacheLock: false, + forceRun: false, + gitProcessChecker: mockChecker.Object); + step.Execute(); + + mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); + + this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(0); + List commands = this.gitProcess.CommandsRun; + commands.Count.ShouldEqual(2); + commands[0].ShouldEqual(PrunePackedCommand); + commands[1].ShouldEqual(this.packCommand); + } + + [TestCase] + public void LooseObjectsFailGitProcessIds() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + Mock mockChecker = new Mock(); + mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) + .Returns(new int[] { 1 }); + + LooseObjectsStep step = new LooseObjectsStep( + this.context, + requireCacheLock: false, + forceRun: false, + gitProcessChecker: mockChecker.Object); + step.Execute(); + + mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); + + this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.StartActivityTracer.RelatedWarningEvents.Count.ShouldEqual(1); + List commands = this.gitProcess.CommandsRun; + commands.Count.ShouldEqual(0); + } + + [TestCase] + public void LooseObjectsLimitPackCount() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + // Verify with default limit + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); + + // Verify with limit of 2 + step.MaxLooseObjectsInPack = 2; + step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(2); + } + + [TestCase] + public void SkipInvalidLooseObjects() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + // Verify with valid Objects + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); + this.tracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.RelatedWarningEvents.Count.ShouldEqual(0); + + // Write an ObjectId file with an invalid name + this.context.FileSystem.WriteAllText(Path.Combine(this.context.Enlistment.GitObjectsRoot, "AA", "NOT_A_SHA"), string.Empty); + + // Verify it wasn't added and a warning exists + step.WriteLooseObjectIds(new StreamWriter(new MemoryStream())).ShouldEqual(3); + this.tracer.RelatedErrorEvents.Count.ShouldEqual(0); + this.tracer.RelatedWarningEvents.Count.ShouldEqual(1); + } + + [TestCase] + public void LooseObjectsCount() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + step.CountLooseObjects(out int count, out long size); + + count.ShouldEqual(3); + size.ShouldEqual("one".Length + "two".Length + "three".Length); + } + + [TestCase] + public void LooseObjectId() + { + this.TestSetup(DateTime.UtcNow.AddDays(-7)); + + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + string directoryName = "AB"; + string fileName = "830bb79cd4fadb2e73e780e452dc71db909001"; + step.TryGetLooseObjectId( + directoryName, + Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName), + out string objectId).ShouldBeTrue(); + objectId.ShouldEqual(directoryName + fileName); + + directoryName = "AB"; + fileName = "BAD_FILE_NAME"; + step.TryGetLooseObjectId( + directoryName, + Path.Combine(this.context.Enlistment.GitObjectsRoot, directoryName, fileName), + out objectId).ShouldBeFalse(); + } + + [TestCase] + public void LooseObjectFileName() + { + this.TestSetup(DateTime.UtcNow); + LooseObjectsStep step = new LooseObjectsStep(this.context, requireCacheLock: false, forceRun: false); + + step.GetLooseObjectFileName("0123456789012345678901234567890123456789") + .ShouldEqual(Path.Combine(this.context.Enlistment.GitObjectsRoot, "01", "23456789012345678901234567890123456789")); + } + + private void TestSetup(DateTime lastRun) + { + string lastRunTime = EpochConverter.ToUnixEpochSeconds(lastRun).ToString(); + + // Create GitProcess + this.gitProcess = new MockGitProcess(); + this.gitProcess.SetExpectedCommandResult( + PrunePackedCommand, + () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); + + // Create enlistment using git process + ScalarEnlistment enlistment = new MockScalarEnlistment(this.gitProcess); + + string packPrefix = Path.Combine(enlistment.GitPackRoot, "from-loose"); + this.packCommand = $"pack-objects {packPrefix} --non-empty --window=0 --depth=0 -q"; + + this.gitProcess.SetExpectedCommandResult( + this.packCommand, + () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); + + // Create a last run time file + MockFile timeFile = new MockFile(Path.Combine(enlistment.GitObjectsRoot, "info", LooseObjectsStep.LooseObjectsLastRunFileName), lastRunTime); + + // Create info directory to hold last run time file + MockDirectory infoRoot = new MockDirectory(Path.Combine(enlistment.GitObjectsRoot, "info"), null, new List() { timeFile }); + + // Create Hex Folder 1 with 1 File + MockDirectory hex1 = new MockDirectory( + Path.Combine(enlistment.GitObjectsRoot, "AA"), + null, + new List() + { + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "AA", "1156f4f2b850673090c285289ea8475d629fe1"), "one") + }); + + // Create Hex Folder 2 with 2 Files + MockDirectory hex2 = new MockDirectory( + Path.Combine(enlistment.GitObjectsRoot, "F1"), + null, + new List() + { + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe2"), "two"), + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "F1", "1156f4f2b850673090c285289ea8475d629fe3"), "three") + }); + + // Create NonHex Folder with 4 Files + MockDirectory nonhex = new MockDirectory( + Path.Combine(enlistment.GitObjectsRoot, "ZZ"), + null, + new List() + { + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe4"), "4"), + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe5"), "5"), + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe6"), "6"), + new MockFile(Path.Combine(enlistment.GitObjectsRoot, "ZZ", "1156f4f2b850673090c285289ea8475d629fe7"), "7") + }); + + MockDirectory pack = new MockDirectory( + enlistment.GitPackRoot, + null, + new List()); + + // Create git objects directory + MockDirectory gitObjectsRoot = new MockDirectory(enlistment.GitObjectsRoot, new List() { infoRoot, hex1, hex2, nonhex, pack }, null); + + // Add object directory to file System + List directories = new List() { gitObjectsRoot }; + PhysicalFileSystem fileSystem = new MockFileSystem(new MockDirectory(enlistment.EnlistmentRoot, directories, null)); + + // Create and return Context + this.tracer = new MockTracer(); + this.context = new ScalarContext(this.tracer, fileSystem, repository: null, enlistment: enlistment); + } + } +} diff --git a/Scalar.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs b/Scalar.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs index a6887e7bdc..574c150a54 100644 --- a/Scalar.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs +++ b/Scalar.UnitTests/Maintenance/PackfileMaintenanceStepTests.cs @@ -1,4 +1,4 @@ -using Moq; +using Moq; using NUnit.Framework; using Scalar.Common; using Scalar.Common.FileSystem; @@ -21,8 +21,8 @@ public class PackfileMaintenanceStepTests private const string KeepName = "pack-3.keep"; private MockTracer tracer; private MockGitProcess gitProcess; - private ScalarContext context; - + private ScalarContext context; + private string ExpireCommand => $"multi-pack-index expire --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\""; private string VerifyCommand => $"-c core.multiPackIndex=true multi-pack-index verify --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\""; private string WriteCommand => $"-c core.multiPackIndex=true multi-pack-index write --object-dir=\"{this.context.Enlistment.GitObjectsRoot}\""; @@ -64,11 +64,11 @@ public void PackfileMaintenanceFailTimeRestriction() [TestCase] public void PackfileMaintenancePassTimeRestriction() { - this.TestSetup(DateTime.UtcNow.AddDays(-1)); - - Mock mockChecker = new Mock(); - mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) - .Returns(Array.Empty()); + this.TestSetup(DateTime.UtcNow.AddDays(-1)); + + Mock mockChecker = new Mock(); + mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) + .Returns(Array.Empty()); PackfileMaintenanceStep step = new PackfileMaintenanceStep( this.context, @@ -76,8 +76,8 @@ public void PackfileMaintenancePassTimeRestriction() forceRun: false, gitProcessChecker: mockChecker.Object); - step.Execute(); - + step.Execute(); + mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); @@ -94,11 +94,11 @@ public void PackfileMaintenancePassTimeRestriction() [TestCase] public void PackfileMaintenanceFailGitProcessIds() { - this.TestSetup(DateTime.UtcNow.AddDays(-1)); - - Mock mockChecker = new Mock(); - mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) - .Returns(new int[] { 1 }); + this.TestSetup(DateTime.UtcNow.AddDays(-1)); + + Mock mockChecker = new Mock(); + mockChecker.Setup(checker => checker.GetRunningGitProcessIds()) + .Returns(new int[] { 1 }); PackfileMaintenanceStep step = new PackfileMaintenanceStep( this.context, @@ -106,8 +106,8 @@ public void PackfileMaintenanceFailGitProcessIds() forceRun: false, gitProcessChecker: mockChecker.Object); - step.Execute(); - + step.Execute(); + mockChecker.Verify(checker => checker.GetRunningGitProcessIds(), Times.Once()); this.tracer.StartActivityTracer.RelatedErrorEvents.Count.ShouldEqual(0); @@ -154,8 +154,8 @@ public void CountPackFiles() size.ShouldEqual(11); hasKeep.ShouldEqual(true); - this.context.FileSystem.DeleteFile(Path.Combine(this.context.Enlistment.GitPackRoot, KeepName)); - + this.context.FileSystem.DeleteFile(Path.Combine(this.context.Enlistment.GitPackRoot, KeepName)); + step.GetPackFilesInfo(out count, out size, out hasKeep); count.ShouldEqual(3); size.ShouldEqual(11); @@ -225,8 +225,8 @@ private void TestSetup(DateTime lastRun, bool failOnVerify = false) // Create and return Context this.tracer = new MockTracer(); - this.context = new ScalarContext(this.tracer, fileSystem, repository, enlistment); - + this.context = new ScalarContext(this.tracer, fileSystem, repository, enlistment); + this.gitProcess.SetExpectedCommandResult( this.WriteCommand, () => new GitProcess.Result(string.Empty, string.Empty, GitProcess.Result.SuccessCode)); diff --git a/Scalar.UnitTests/Maintenance/PostFetchStepTests.cs b/Scalar.UnitTests/Maintenance/PostFetchStepTests.cs index b6aa6257dd..9e927cb1a4 100644 --- a/Scalar.UnitTests/Maintenance/PostFetchStepTests.cs +++ b/Scalar.UnitTests/Maintenance/PostFetchStepTests.cs @@ -1,4 +1,4 @@ -using NUnit.Framework; +using NUnit.Framework; using Scalar.Common; using Scalar.Common.FileSystem; using Scalar.Common.Git; @@ -52,8 +52,8 @@ public void WriteGraphWithPacks() this.tracer.RelatedInfoEvents.Count.ShouldEqual(0); - List commands = this.gitProcess.CommandsRun; - + List commands = this.gitProcess.CommandsRun; + commands.Count.ShouldEqual(2); commands[0].ShouldEqual(this.CommitGraphWriteCommand); commands[1].ShouldEqual(this.CommitGraphVerifyCommand); diff --git a/Scalar.UnitTests/Mock/Common/MockFileBasedLock.cs b/Scalar.UnitTests/Mock/Common/MockFileBasedLock.cs index a0ab935050..fba539ad22 100644 --- a/Scalar.UnitTests/Mock/Common/MockFileBasedLock.cs +++ b/Scalar.UnitTests/Mock/Common/MockFileBasedLock.cs @@ -1,26 +1,26 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockFileBasedLock : FileBasedLock - { - public MockFileBasedLock( - PhysicalFileSystem fileSystem, - ITracer tracer, - string lockPath) - : base(fileSystem, tracer, lockPath) - { - } - - public override bool TryAcquireLock() - { - return true; - } - - public override void Dispose() - { - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockFileBasedLock : FileBasedLock + { + public MockFileBasedLock( + PhysicalFileSystem fileSystem, + ITracer tracer, + string lockPath) + : base(fileSystem, tracer, lockPath) + { + } + + public override bool TryAcquireLock() + { + return true; + } + + public override void Dispose() + { + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockLocalScalarConfig.cs b/Scalar.UnitTests/Mock/Common/MockLocalScalarConfig.cs index 8e39007f69..3453a1c0cb 100644 --- a/Scalar.UnitTests/Mock/Common/MockLocalScalarConfig.cs +++ b/Scalar.UnitTests/Mock/Common/MockLocalScalarConfig.cs @@ -1,53 +1,53 @@ -using Scalar.Common; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockLocalScalarConfig : LocalScalarConfig - { - public MockLocalScalarConfig() - { - this.Settings = new Dictionary(); - } - - private Dictionary Settings { get; set; } - - public override bool TryGetAllConfig(out Dictionary allConfig, out string error) - { - allConfig = new Dictionary(this.Settings); - error = null; - - return true; - } - - public override bool TryGetConfig( - string name, - out string value, - out string error) - { - error = null; - - this.Settings.TryGetValue(name, out value); - return true; - } - - public override bool TrySetConfig( - string name, - string value, - out string error) - { - error = null; - this.Settings[name] = value; - - return true; - } - - public override bool TryRemoveConfig(string name, out string error) - { - error = null; - this.Settings.Remove(name); - - return true; - } - } -} +using Scalar.Common; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockLocalScalarConfig : LocalScalarConfig + { + public MockLocalScalarConfig() + { + this.Settings = new Dictionary(); + } + + private Dictionary Settings { get; set; } + + public override bool TryGetAllConfig(out Dictionary allConfig, out string error) + { + allConfig = new Dictionary(this.Settings); + error = null; + + return true; + } + + public override bool TryGetConfig( + string name, + out string value, + out string error) + { + error = null; + + this.Settings.TryGetValue(name, out value); + return true; + } + + public override bool TrySetConfig( + string name, + string value, + out string error) + { + error = null; + this.Settings[name] = value; + + return true; + } + + public override bool TryRemoveConfig(string name, out string error) + { + error = null; + this.Settings.Remove(name); + + return true; + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockLocalScalarConfigBuilder.cs b/Scalar.UnitTests/Mock/Common/MockLocalScalarConfigBuilder.cs index 6e1f1fe37a..5cb5aa762d 100644 --- a/Scalar.UnitTests/Mock/Common/MockLocalScalarConfigBuilder.cs +++ b/Scalar.UnitTests/Mock/Common/MockLocalScalarConfigBuilder.cs @@ -1,91 +1,91 @@ -using Scalar.Common; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockLocalScalarConfigBuilder - { - private string defaultRing; - private string defaultUpgradeFeedUrl; - private string defaultUpgradeFeedPackageName; - private string defaultOrgServerUrl; - - private Dictionary entries; - - public MockLocalScalarConfigBuilder( - string defaultRing, - string defaultUpgradeFeedUrl, - string defaultUpgradeFeedPackageName, - string defaultOrgServerUrl) - { - this.defaultRing = defaultRing; - this.defaultUpgradeFeedUrl = defaultUpgradeFeedUrl; - this.defaultUpgradeFeedPackageName = defaultUpgradeFeedPackageName; - this.defaultOrgServerUrl = defaultOrgServerUrl; - this.entries = new Dictionary(); - } - - public MockLocalScalarConfigBuilder WithUpgradeRing(string value = null) - { - return this.With(ScalarConstants.LocalScalarConfig.UpgradeRing, value ?? this.defaultRing); - } - - public MockLocalScalarConfigBuilder WithNoUpgradeRing() - { - return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeRing); - } - - public MockLocalScalarConfigBuilder WithUpgradeFeedPackageName(string value = null) - { - return this.With(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName, value ?? this.defaultUpgradeFeedPackageName); - } - - public MockLocalScalarConfigBuilder WithNoUpgradeFeedPackageName() - { - return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName); - } - - public MockLocalScalarConfigBuilder WithUpgradeFeedUrl(string value = null) - { - return this.With(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl, value ?? this.defaultUpgradeFeedUrl); - } - - public MockLocalScalarConfigBuilder WithNoUpgradeFeedUrl() - { - return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl); - } - - public MockLocalScalarConfigBuilder WithOrgInfoServerUrl(string value = null) - { - return this.With(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl, value ?? this.defaultUpgradeFeedUrl); - } - - public MockLocalScalarConfigBuilder WithNoOrgInfoServerUrl() - { - return this.WithNo(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl); - } - - public MockLocalScalarConfig Build() - { - MockLocalScalarConfig scalarConfig = new MockLocalScalarConfig(); - foreach (KeyValuePair kvp in this.entries) - { - scalarConfig.TrySetConfig(kvp.Key, kvp.Value, out _); - } - - return scalarConfig; - } - - private MockLocalScalarConfigBuilder With(string key, string value) - { - this.entries.Add(key, value); - return this; - } - - private MockLocalScalarConfigBuilder WithNo(string key) - { - this.entries.Remove(key); - return this; - } - } -} +using Scalar.Common; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockLocalScalarConfigBuilder + { + private string defaultRing; + private string defaultUpgradeFeedUrl; + private string defaultUpgradeFeedPackageName; + private string defaultOrgServerUrl; + + private Dictionary entries; + + public MockLocalScalarConfigBuilder( + string defaultRing, + string defaultUpgradeFeedUrl, + string defaultUpgradeFeedPackageName, + string defaultOrgServerUrl) + { + this.defaultRing = defaultRing; + this.defaultUpgradeFeedUrl = defaultUpgradeFeedUrl; + this.defaultUpgradeFeedPackageName = defaultUpgradeFeedPackageName; + this.defaultOrgServerUrl = defaultOrgServerUrl; + this.entries = new Dictionary(); + } + + public MockLocalScalarConfigBuilder WithUpgradeRing(string value = null) + { + return this.With(ScalarConstants.LocalScalarConfig.UpgradeRing, value ?? this.defaultRing); + } + + public MockLocalScalarConfigBuilder WithNoUpgradeRing() + { + return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeRing); + } + + public MockLocalScalarConfigBuilder WithUpgradeFeedPackageName(string value = null) + { + return this.With(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName, value ?? this.defaultUpgradeFeedPackageName); + } + + public MockLocalScalarConfigBuilder WithNoUpgradeFeedPackageName() + { + return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeFeedPackageName); + } + + public MockLocalScalarConfigBuilder WithUpgradeFeedUrl(string value = null) + { + return this.With(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl, value ?? this.defaultUpgradeFeedUrl); + } + + public MockLocalScalarConfigBuilder WithNoUpgradeFeedUrl() + { + return this.WithNo(ScalarConstants.LocalScalarConfig.UpgradeFeedUrl); + } + + public MockLocalScalarConfigBuilder WithOrgInfoServerUrl(string value = null) + { + return this.With(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl, value ?? this.defaultUpgradeFeedUrl); + } + + public MockLocalScalarConfigBuilder WithNoOrgInfoServerUrl() + { + return this.WithNo(ScalarConstants.LocalScalarConfig.OrgInfoServerUrl); + } + + public MockLocalScalarConfig Build() + { + MockLocalScalarConfig scalarConfig = new MockLocalScalarConfig(); + foreach (KeyValuePair kvp in this.entries) + { + scalarConfig.TrySetConfig(kvp.Key, kvp.Value, out _); + } + + return scalarConfig; + } + + private MockLocalScalarConfigBuilder With(string key, string value) + { + this.entries.Add(key, value); + return this; + } + + private MockLocalScalarConfigBuilder WithNo(string key) + { + this.entries.Remove(key); + return this; + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockPhysicalGitObjects.cs b/Scalar.UnitTests/Mock/Common/MockPhysicalGitObjects.cs index f574b45042..ffd08ca298 100644 --- a/Scalar.UnitTests/Mock/Common/MockPhysicalGitObjects.cs +++ b/Scalar.UnitTests/Mock/Common/MockPhysicalGitObjects.cs @@ -1,44 +1,44 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System.Diagnostics; -using System.IO; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockPhysicalGitObjects : GitObjects - { - public MockPhysicalGitObjects(ITracer tracer, PhysicalFileSystem fileSystem, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor) - : base(tracer, enlistment, objectRequestor, fileSystem) - { - } - - public override string WriteLooseObject(Stream responseStream, string sha, bool overwriteExisting, byte[] sharedBuf = null) - { - using (StreamReader reader = new StreamReader(responseStream)) - { - // Return "file contents" as "file name". Weird, but proves we got the right thing. - return reader.ReadToEnd(); - } - } - - public override string WriteTempPackFile(Stream stream) - { - Debug.Assert(stream != null, "WriteTempPackFile should not receive a null stream"); - - using (stream) - using (StreamReader reader = new StreamReader(stream)) - { - // Return "file contents" as "file name". Weird, but proves we got the right thing. - return reader.ReadToEnd(); - } - } - - public override GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null) - { - return new GitProcess.Result(string.Empty, "TestFailure", GitProcess.Result.GenericFailureCode); - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System.Diagnostics; +using System.IO; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockPhysicalGitObjects : GitObjects + { + public MockPhysicalGitObjects(ITracer tracer, PhysicalFileSystem fileSystem, Enlistment enlistment, GitObjectsHttpRequestor objectRequestor) + : base(tracer, enlistment, objectRequestor, fileSystem) + { + } + + public override string WriteLooseObject(Stream responseStream, string sha, bool overwriteExisting, byte[] sharedBuf = null) + { + using (StreamReader reader = new StreamReader(responseStream)) + { + // Return "file contents" as "file name". Weird, but proves we got the right thing. + return reader.ReadToEnd(); + } + } + + public override string WriteTempPackFile(Stream stream) + { + Debug.Assert(stream != null, "WriteTempPackFile should not receive a null stream"); + + using (stream) + using (StreamReader reader = new StreamReader(stream)) + { + // Return "file contents" as "file name". Weird, but proves we got the right thing. + return reader.ReadToEnd(); + } + } + + public override GitProcess.Result IndexTempPackFile(string tempPackPath, GitProcess gitProcess = null) + { + return new GitProcess.Result(string.Empty, "TestFailure", GitProcess.Result.GenericFailureCode); + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockPlatform.cs b/Scalar.UnitTests/Mock/Common/MockPlatform.cs index e2f959fa22..75bd22a04a 100644 --- a/Scalar.UnitTests/Mock/Common/MockPlatform.cs +++ b/Scalar.UnitTests/Mock/Common/MockPlatform.cs @@ -1,225 +1,225 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using Scalar.UnitTests.Mock.FileSystem; -using Scalar.UnitTests.Mock.Git; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Pipes; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockPlatform : ScalarPlatform - { - public MockPlatform() : base(underConstruction: new UnderConstructionFlags()) - { - } - - public string MockCurrentUser { get; set; } - - public override IGitInstallation GitInstallation { get; } = new MockGitInstallation(); - - public override IDiskLayoutUpgradeData DiskLayoutUpgrade => throw new NotSupportedException(); - - public override IPlatformFileSystem FileSystem { get; } = new MockPlatformFileSystem(); - - public override string Name { get => "Mock"; } - - public override string ScalarConfigPath { get => Path.Combine("mock:", LocalScalarConfig.FileName); } - - public override ScalarPlatformConstants Constants { get; } = new MockPlatformConstants(); - - public HashSet ActiveProcesses { get; } = new HashSet(); - - public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) - { - throw new NotSupportedException(); - } - - public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) - { - throw new NotImplementedException(); - } - - public override string GetNamedPipeName(string enlistmentRoot) - { - return "Scalar_Mock_PipeName"; - } - - public override string GetScalarServiceNamedPipeName(string serviceName) - { - return Path.Combine("Scalar_Mock_ServicePipeName", serviceName); - } - - public override NamedPipeServerStream CreatePipeByName(string pipeName) - { - throw new NotSupportedException(); - } - - public override string GetCurrentUser() - { - return this.MockCurrentUser; - } - - public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) - { - return sessionId.ToString(); - } - - public override string GetOSVersionInformation() - { - throw new NotSupportedException(); - } - - public override string GetDataRootForScalar() - { - // TODO: Update this method to return non existant file path. - return Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), - "Scalar"); - } - - public override string GetDataRootForScalarComponent(string componentName) - { - return Path.Combine(this.GetDataRootForScalar(), componentName); - } - - public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) - { - return new Dictionary(); - } - - public override string GetUpgradeProtectedDataDirectory() - { - return this.GetDataRootForScalarComponent(ProductUpgraderInfo.UpgradeDirectoryName); - } - - public override string GetUpgradeLogDirectoryParentDirectory() - { - return this.GetUpgradeProtectedDataDirectory(); - } - - public override string GetUpgradeHighestAvailableVersionDirectory() - { - return this.GetUpgradeProtectedDataDirectory(); - } - - public override void InitializeEnlistmentACLs(string enlistmentPath) - { - throw new NotSupportedException(); - } - - public override bool IsConsoleOutputRedirectedToFile() - { - throw new NotSupportedException(); - } - - public override bool IsElevated() - { - throw new NotSupportedException(); - } - - public override bool IsProcessActive(int processId) - { - return this.ActiveProcesses.Contains(processId); - } - - public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) - { - throw new NotSupportedException(); - } - - public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) - { - throw new NotSupportedException(); - } - - public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError) - { - throw new NotImplementedException(); - } - - public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) - { - throw new NotSupportedException(); - } - - public override void PrepareProcessToRunInBackground() - { - throw new NotSupportedException(); - } - - public override FileBasedLock CreateFileBasedLock(PhysicalFileSystem fileSystem, ITracer tracer, string lockPath) - { - return new MockFileBasedLock(fileSystem, tracer, lockPath); - } - - public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInteractions( - PhysicalFileSystem fileSystem, - ITracer tracer) - { - return new MockProductUpgraderPlatformStrategy(fileSystem, tracer); - } - - public override bool TryKillProcessTree(int processId, out int exitCode, out string error) - { - error = null; - exitCode = 0; - return true; - } - - public class MockPlatformConstants : ScalarPlatformConstants - { - public override string ExecutableExtension - { - get { return ".mockexe"; } - } - - public override string InstallerExtension - { - get { return ".mockexe"; } - } - - public override string WorkingDirectoryBackingRootPath - { - get { return ScalarConstants.WorkingDirectoryRootName; } - } - - public override string DotScalarRoot - { - get { return ".mockscalar"; } - } - - public override string ScalarBinDirectoryPath - { - get { return Path.Combine("MockProgramFiles", this.ScalarBinDirectoryName); } - } - - public override string ScalarBinDirectoryName - { - get { return "MockScalar"; } - } - - public override string ScalarExecutableName - { - get { return "MockScalar" + this.ExecutableExtension; } - } - - public override string ProgramLocaterCommand - { - get { return "MockWhere"; } - } - - public override HashSet UpgradeBlockingProcesses - { - get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar", "Scalar.Mount", "git", "wish", "bash" }; } - } - - public override bool SupportsUpgradeWhileRunning => false; - - public override int MaxPipePathLength => 250; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using Scalar.UnitTests.Mock.FileSystem; +using Scalar.UnitTests.Mock.Git; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Pipes; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockPlatform : ScalarPlatform + { + public MockPlatform() : base(underConstruction: new UnderConstructionFlags()) + { + } + + public string MockCurrentUser { get; set; } + + public override IGitInstallation GitInstallation { get; } = new MockGitInstallation(); + + public override IDiskLayoutUpgradeData DiskLayoutUpgrade => throw new NotSupportedException(); + + public override IPlatformFileSystem FileSystem { get; } = new MockPlatformFileSystem(); + + public override string Name { get => "Mock"; } + + public override string ScalarConfigPath { get => Path.Combine("mock:", LocalScalarConfig.FileName); } + + public override ScalarPlatformConstants Constants { get; } = new MockPlatformConstants(); + + public HashSet ActiveProcesses { get; } = new HashSet(); + + public override void ConfigureVisualStudio(string gitBinPath, ITracer tracer) + { + throw new NotSupportedException(); + } + + public override bool TryVerifyAuthenticodeSignature(string path, out string subject, out string issuer, out string error) + { + throw new NotImplementedException(); + } + + public override string GetNamedPipeName(string enlistmentRoot) + { + return "Scalar_Mock_PipeName"; + } + + public override string GetScalarServiceNamedPipeName(string serviceName) + { + return Path.Combine("Scalar_Mock_ServicePipeName", serviceName); + } + + public override NamedPipeServerStream CreatePipeByName(string pipeName) + { + throw new NotSupportedException(); + } + + public override string GetCurrentUser() + { + return this.MockCurrentUser; + } + + public override string GetUserIdFromLoginSessionId(int sessionId, ITracer tracer) + { + return sessionId.ToString(); + } + + public override string GetOSVersionInformation() + { + throw new NotSupportedException(); + } + + public override string GetDataRootForScalar() + { + // TODO: Update this method to return non existant file path. + return Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Scalar"); + } + + public override string GetDataRootForScalarComponent(string componentName) + { + return Path.Combine(this.GetDataRootForScalar(), componentName); + } + + public override Dictionary GetPhysicalDiskInfo(string path, bool sizeStatsOnly) + { + return new Dictionary(); + } + + public override string GetUpgradeProtectedDataDirectory() + { + return this.GetDataRootForScalarComponent(ProductUpgraderInfo.UpgradeDirectoryName); + } + + public override string GetUpgradeLogDirectoryParentDirectory() + { + return this.GetUpgradeProtectedDataDirectory(); + } + + public override string GetUpgradeHighestAvailableVersionDirectory() + { + return this.GetUpgradeProtectedDataDirectory(); + } + + public override void InitializeEnlistmentACLs(string enlistmentPath) + { + throw new NotSupportedException(); + } + + public override bool IsConsoleOutputRedirectedToFile() + { + throw new NotSupportedException(); + } + + public override bool IsElevated() + { + throw new NotSupportedException(); + } + + public override bool IsProcessActive(int processId) + { + return this.ActiveProcesses.Contains(processId); + } + + public override void IsServiceInstalledAndRunning(string name, out bool installed, out bool running) + { + throw new NotSupportedException(); + } + + public override bool TryGetScalarEnlistmentRoot(string directory, out string enlistmentRoot, out string errorMessage) + { + throw new NotSupportedException(); + } + + public override bool TryGetDefaultLocalCacheRoot(string enlistmentRoot, out string localCacheRoot, out string localCacheRootError) + { + throw new NotImplementedException(); + } + + public override void StartBackgroundScalarProcess(ITracer tracer, string programName, string[] args) + { + throw new NotSupportedException(); + } + + public override void PrepareProcessToRunInBackground() + { + throw new NotSupportedException(); + } + + public override FileBasedLock CreateFileBasedLock(PhysicalFileSystem fileSystem, ITracer tracer, string lockPath) + { + return new MockFileBasedLock(fileSystem, tracer, lockPath); + } + + public override ProductUpgraderPlatformStrategy CreateProductUpgraderPlatformInteractions( + PhysicalFileSystem fileSystem, + ITracer tracer) + { + return new MockProductUpgraderPlatformStrategy(fileSystem, tracer); + } + + public override bool TryKillProcessTree(int processId, out int exitCode, out string error) + { + error = null; + exitCode = 0; + return true; + } + + public class MockPlatformConstants : ScalarPlatformConstants + { + public override string ExecutableExtension + { + get { return ".mockexe"; } + } + + public override string InstallerExtension + { + get { return ".mockexe"; } + } + + public override string WorkingDirectoryBackingRootPath + { + get { return ScalarConstants.WorkingDirectoryRootName; } + } + + public override string DotScalarRoot + { + get { return ".mockscalar"; } + } + + public override string ScalarBinDirectoryPath + { + get { return Path.Combine("MockProgramFiles", this.ScalarBinDirectoryName); } + } + + public override string ScalarBinDirectoryName + { + get { return "MockScalar"; } + } + + public override string ScalarExecutableName + { + get { return "MockScalar" + this.ExecutableExtension; } + } + + public override string ProgramLocaterCommand + { + get { return "MockWhere"; } + } + + public override HashSet UpgradeBlockingProcesses + { + get { return new HashSet(StringComparer.OrdinalIgnoreCase) { "Scalar", "Scalar.Mount", "git", "wish", "bash" }; } + } + + public override bool SupportsUpgradeWhileRunning => false; + + public override int MaxPipePathLength => 250; + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockScalarEnlistment.cs b/Scalar.UnitTests/Mock/Common/MockScalarEnlistment.cs index 7178e5a13f..42e69b6865 100644 --- a/Scalar.UnitTests/Mock/Common/MockScalarEnlistment.cs +++ b/Scalar.UnitTests/Mock/Common/MockScalarEnlistment.cs @@ -1,43 +1,43 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.UnitTests.Mock.Git; -using System.IO; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockScalarEnlistment : ScalarEnlistment - { - private MockGitProcess gitProcess; - - public MockScalarEnlistment() - : base(Path.Combine("mock:", "path"), "mock://repoUrl", Path.Combine("mock:", "git"), authentication: null) - { - this.GitObjectsRoot = Path.Combine("mock:", "path", ".git", "objects"); - this.LocalObjectsRoot = this.GitObjectsRoot; - this.GitPackRoot = Path.Combine("mock:", "path", ".git", "objects", "pack"); - } - - public MockScalarEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, MockGitProcess gitProcess) - : base(enlistmentRoot, repoUrl, gitBinPath, authentication: null) - { - this.gitProcess = gitProcess; - } - - public MockScalarEnlistment(MockGitProcess gitProcess) - : this() - { - this.gitProcess = gitProcess; - } - - public override string GitObjectsRoot { get; protected set; } - - public override string LocalObjectsRoot { get; protected set; } - - public override string GitPackRoot { get; protected set; } - - public override GitProcess CreateGitProcess() - { - return this.gitProcess ?? new MockGitProcess(); - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.UnitTests.Mock.Git; +using System.IO; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockScalarEnlistment : ScalarEnlistment + { + private MockGitProcess gitProcess; + + public MockScalarEnlistment() + : base(Path.Combine("mock:", "path"), "mock://repoUrl", Path.Combine("mock:", "git"), authentication: null) + { + this.GitObjectsRoot = Path.Combine("mock:", "path", ".git", "objects"); + this.LocalObjectsRoot = this.GitObjectsRoot; + this.GitPackRoot = Path.Combine("mock:", "path", ".git", "objects", "pack"); + } + + public MockScalarEnlistment(string enlistmentRoot, string repoUrl, string gitBinPath, MockGitProcess gitProcess) + : base(enlistmentRoot, repoUrl, gitBinPath, authentication: null) + { + this.gitProcess = gitProcess; + } + + public MockScalarEnlistment(MockGitProcess gitProcess) + : this() + { + this.gitProcess = gitProcess; + } + + public override string GitObjectsRoot { get; protected set; } + + public override string LocalObjectsRoot { get; protected set; } + + public override string GitPackRoot { get; protected set; } + + public override GitProcess CreateGitProcess() + { + return this.gitProcess ?? new MockGitProcess(); + } + } +} diff --git a/Scalar.UnitTests/Mock/Common/MockTracer.cs b/Scalar.UnitTests/Mock/Common/MockTracer.cs index 8636962808..fbc525714e 100644 --- a/Scalar.UnitTests/Mock/Common/MockTracer.cs +++ b/Scalar.UnitTests/Mock/Common/MockTracer.cs @@ -1,157 +1,157 @@ -using Newtonsoft.Json; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Threading; - -namespace Scalar.UnitTests.Mock.Common -{ - public class MockTracer : ITracer - { - private AutoResetEvent waitEvent; - - public MockTracer() - { - this.waitEvent = new AutoResetEvent(false); - this.RelatedInfoEvents = new List(); - this.RelatedWarningEvents = new List(); - this.RelatedErrorEvents = new List(); - } - - public MockTracer StartActivityTracer { get; private set; } - public string WaitRelatedEventName { get; set; } - - public List RelatedInfoEvents { get; } - public List RelatedWarningEvents { get; } - public List RelatedErrorEvents { get; } - - public void WaitForRelatedEvent() - { - this.waitEvent.WaitOne(); - } - - public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata) - { - if (eventName == this.WaitRelatedEventName) - { - this.waitEvent.Set(); - } - } - - public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata, Keywords keyword) - { - if (eventName == this.WaitRelatedEventName) - { - this.waitEvent.Set(); - } - } - - public void RelatedInfo(string message) - { - this.RelatedInfoEvents.Add(message); - } - - public void RelatedInfo(EventMetadata metadata, string message) - { - metadata[TracingConstants.MessageKey.InfoMessage] = message; - this.RelatedInfoEvents.Add(JsonConvert.SerializeObject(metadata)); - } - - public void RelatedInfo(string format, params object[] args) - { - this.RelatedInfo(string.Format(format, args)); - } - - public void RelatedWarning(EventMetadata metadata, string message) - { - if (metadata != null) - { - metadata[TracingConstants.MessageKey.WarningMessage] = message; - this.RelatedWarningEvents.Add(JsonConvert.SerializeObject(metadata)); - } - else if (message != null) - { - this.RelatedWarning(message); - } - } - - public void RelatedWarning(EventMetadata metadata, string message, Keywords keyword) - { - this.RelatedWarning(metadata, message); - } - - public void RelatedWarning(string message) - { - this.RelatedWarningEvents.Add(message); - } - - public void RelatedWarning(string format, params object[] args) - { - this.RelatedWarningEvents.Add(string.Format(format, args)); - } - - public void RelatedError(EventMetadata metadata, string message) - { - metadata[TracingConstants.MessageKey.ErrorMessage] = message; - this.RelatedErrorEvents.Add(JsonConvert.SerializeObject(metadata)); - } - - public void RelatedError(EventMetadata metadata, string message, Keywords keyword) - { - this.RelatedError(metadata, message); - } - - public void RelatedError(string message) - { - this.RelatedErrorEvents.Add(message); - } - - public void RelatedError(string format, params object[] args) - { - this.RelatedErrorEvents.Add(string.Format(format, args)); - } - - public ITracer StartActivity(string activityName, EventLevel level) - { - return this.StartActivity(activityName, level, metadata: null); - } - - public ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata) - { - return this.StartActivity(activityName, level, Keywords.None, metadata); - } - - public ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata) - { - this.StartActivityTracer = this.StartActivityTracer ?? new MockTracer(); - return this.StartActivityTracer; - } - - public TimeSpan Stop(EventMetadata metadata) - { - return TimeSpan.Zero; - } - - public void SetGitCommandSessionId(string sessionId) - { - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - protected void Dispose(bool disposing) - { - if (disposing) - { - if (this.waitEvent != null) - { - this.waitEvent.Dispose(); - this.waitEvent = null; - } - } - } - } +using Newtonsoft.Json; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Threading; + +namespace Scalar.UnitTests.Mock.Common +{ + public class MockTracer : ITracer + { + private AutoResetEvent waitEvent; + + public MockTracer() + { + this.waitEvent = new AutoResetEvent(false); + this.RelatedInfoEvents = new List(); + this.RelatedWarningEvents = new List(); + this.RelatedErrorEvents = new List(); + } + + public MockTracer StartActivityTracer { get; private set; } + public string WaitRelatedEventName { get; set; } + + public List RelatedInfoEvents { get; } + public List RelatedWarningEvents { get; } + public List RelatedErrorEvents { get; } + + public void WaitForRelatedEvent() + { + this.waitEvent.WaitOne(); + } + + public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata) + { + if (eventName == this.WaitRelatedEventName) + { + this.waitEvent.Set(); + } + } + + public void RelatedEvent(EventLevel error, string eventName, EventMetadata metadata, Keywords keyword) + { + if (eventName == this.WaitRelatedEventName) + { + this.waitEvent.Set(); + } + } + + public void RelatedInfo(string message) + { + this.RelatedInfoEvents.Add(message); + } + + public void RelatedInfo(EventMetadata metadata, string message) + { + metadata[TracingConstants.MessageKey.InfoMessage] = message; + this.RelatedInfoEvents.Add(JsonConvert.SerializeObject(metadata)); + } + + public void RelatedInfo(string format, params object[] args) + { + this.RelatedInfo(string.Format(format, args)); + } + + public void RelatedWarning(EventMetadata metadata, string message) + { + if (metadata != null) + { + metadata[TracingConstants.MessageKey.WarningMessage] = message; + this.RelatedWarningEvents.Add(JsonConvert.SerializeObject(metadata)); + } + else if (message != null) + { + this.RelatedWarning(message); + } + } + + public void RelatedWarning(EventMetadata metadata, string message, Keywords keyword) + { + this.RelatedWarning(metadata, message); + } + + public void RelatedWarning(string message) + { + this.RelatedWarningEvents.Add(message); + } + + public void RelatedWarning(string format, params object[] args) + { + this.RelatedWarningEvents.Add(string.Format(format, args)); + } + + public void RelatedError(EventMetadata metadata, string message) + { + metadata[TracingConstants.MessageKey.ErrorMessage] = message; + this.RelatedErrorEvents.Add(JsonConvert.SerializeObject(metadata)); + } + + public void RelatedError(EventMetadata metadata, string message, Keywords keyword) + { + this.RelatedError(metadata, message); + } + + public void RelatedError(string message) + { + this.RelatedErrorEvents.Add(message); + } + + public void RelatedError(string format, params object[] args) + { + this.RelatedErrorEvents.Add(string.Format(format, args)); + } + + public ITracer StartActivity(string activityName, EventLevel level) + { + return this.StartActivity(activityName, level, metadata: null); + } + + public ITracer StartActivity(string activityName, EventLevel level, EventMetadata metadata) + { + return this.StartActivity(activityName, level, Keywords.None, metadata); + } + + public ITracer StartActivity(string activityName, EventLevel level, Keywords startStopKeywords, EventMetadata metadata) + { + this.StartActivityTracer = this.StartActivityTracer ?? new MockTracer(); + return this.StartActivityTracer; + } + + public TimeSpan Stop(EventMetadata metadata) + { + return TimeSpan.Zero; + } + + public void SetGitCommandSessionId(string sessionId) + { + } + + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing) + { + if (this.waitEvent != null) + { + this.waitEvent.Dispose(); + this.waitEvent = null; + } + } + } + } } diff --git a/Scalar.UnitTests/Mock/Common/Tracing/MockListener.cs b/Scalar.UnitTests/Mock/Common/Tracing/MockListener.cs index df9eb2795c..0a9d0f20d2 100644 --- a/Scalar.UnitTests/Mock/Common/Tracing/MockListener.cs +++ b/Scalar.UnitTests/Mock/Common/Tracing/MockListener.cs @@ -1,20 +1,20 @@ -using Scalar.Common.Tracing; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Mock.Common.Tracing -{ - public class MockListener : EventListener - { - public MockListener(EventLevel maxVerbosity, Keywords keywordFilter) - : base(maxVerbosity, keywordFilter, null) - { - } - - public List EventNamesRead { get; set; } = new List(); - - protected override void RecordMessageInternal(TraceEventMessage message) - { - this.EventNamesRead.Add(message.EventName); - } - } -} +using Scalar.Common.Tracing; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Mock.Common.Tracing +{ + public class MockListener : EventListener + { + public MockListener(EventLevel maxVerbosity, Keywords keywordFilter) + : base(maxVerbosity, keywordFilter, null) + { + } + + public List EventNamesRead { get; set; } = new List(); + + protected override void RecordMessageInternal(TraceEventMessage message) + { + this.EventNamesRead.Add(message.EventName); + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/ConfigurableFileSystem.cs b/Scalar.UnitTests/Mock/FileSystem/ConfigurableFileSystem.cs index 7e5e907d9d..1aa2da2974 100644 --- a/Scalar.UnitTests/Mock/FileSystem/ConfigurableFileSystem.cs +++ b/Scalar.UnitTests/Mock/FileSystem/ConfigurableFileSystem.cs @@ -1,50 +1,50 @@ -using Scalar.Common.FileSystem; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class ConfigurableFileSystem : PhysicalFileSystem - { - public ConfigurableFileSystem() - { - this.ExpectedFiles = new Dictionary(); - this.ExpectedDirectories = new HashSet(); - } - - public Dictionary ExpectedFiles { get; } - public HashSet ExpectedDirectories { get; } - - public override void CreateDirectory(string path) - { - } - - public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - ReusableMemoryStream source; - this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName); - this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename); - - this.ExpectedFiles.Remove(sourceFileName); - this.ExpectedFiles[destinationFilename] = source; - } - - public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) - { - ReusableMemoryStream stream; - this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); - return stream; - } - - public override bool FileExists(string path) - { - return this.ExpectedFiles.ContainsKey(path); - } - - public override bool DirectoryExists(string path) - { - return this.ExpectedDirectories.Contains(path); - } - } -} +using Scalar.Common.FileSystem; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class ConfigurableFileSystem : PhysicalFileSystem + { + public ConfigurableFileSystem() + { + this.ExpectedFiles = new Dictionary(); + this.ExpectedDirectories = new HashSet(); + } + + public Dictionary ExpectedFiles { get; } + public HashSet ExpectedDirectories { get; } + + public override void CreateDirectory(string path) + { + } + + public override void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + ReusableMemoryStream source; + this.ExpectedFiles.TryGetValue(sourceFileName, out source).ShouldEqual(true, "Source file does not exist: " + sourceFileName); + this.ExpectedFiles.ContainsKey(destinationFilename).ShouldEqual(true, "MoveAndOverwriteFile expects the destination file to exist: " + destinationFilename); + + this.ExpectedFiles.Remove(sourceFileName); + this.ExpectedFiles[destinationFilename] = source; + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) + { + ReusableMemoryStream stream; + this.ExpectedFiles.TryGetValue(path, out stream).ShouldEqual(true, "Unexpected access of file: " + path); + return stream; + } + + public override bool FileExists(string path) + { + return this.ExpectedFiles.ContainsKey(path); + } + + public override bool DirectoryExists(string path) + { + return this.ExpectedDirectories.Contains(path); + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/MockDirectory.cs b/Scalar.UnitTests/Mock/FileSystem/MockDirectory.cs index a3895c272d..ab8a62ab22 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockDirectory.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockDirectory.cs @@ -1,302 +1,302 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class MockDirectory - { - public MockDirectory(string fullName, IEnumerable folders, IEnumerable files) - { - this.FullName = fullName; - this.Name = Path.GetFileName(this.FullName); - - this.Directories = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - if (folders != null) - { - foreach (MockDirectory folder in folders) - { - this.Directories[folder.FullName] = folder; - } - } - - if (files != null) - { - foreach (MockFile file in files) - { - this.Files[file.FullName] = file; - } - } - - this.FileProperties = FileProperties.DefaultDirectory; - } - - public string FullName { get; private set; } - public string Name { get; private set; } - public Dictionary Directories { get; private set; } - public Dictionary Files { get; private set; } - public FileProperties FileProperties { get; set; } - - public MockFile FindFile(string path) - { - MockFile file; - if (this.Files.TryGetValue(path, out file)) - { - return file; - } - - foreach (MockDirectory directory in this.Directories.Values) - { - file = directory.FindFile(path); - if (file != null) - { - return file; - } - } - - return null; - } - - public void AddOrOverwriteFile(MockFile file, string path) - { - string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); - MockDirectory parentDirectory = this.FindDirectory(parentPath); - - if (parentDirectory == null) - { - throw new IOException(); - } - - MockFile existingFileAtPath = parentDirectory.FindFile(path); - - if (existingFileAtPath != null) - { - parentDirectory.Files.Remove(path); - } - - parentDirectory.Files.Add(file.FullName, file); - } - - public void AddFile(MockFile file, string path) - { - string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); - MockDirectory parentDirectory = this.FindDirectory(parentPath); - - if (parentDirectory == null) - { - throw new IOException(); - } - - MockFile existingFileAtPath = parentDirectory.FindFile(path); - existingFileAtPath.ShouldBeNull(); - - parentDirectory.Files.Add(file.FullName, file); - } - - public void RemoveFile(string path) - { - MockFile file; - if (this.Files.TryGetValue(path, out file)) - { - this.Files.Remove(path); - return; - } - - foreach (MockDirectory directory in this.Directories.Values) - { - file = directory.FindFile(path); - if (file != null) - { - directory.RemoveFile(path); - return; - } - } - } - - public MockDirectory FindDirectory(string path) - { - if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) - { - return this; - } - - MockDirectory foundDirectory; - if (this.Directories.TryGetValue(path, out foundDirectory)) - { - return foundDirectory; - } - - foreach (MockDirectory subDirectory in this.Directories.Values) - { - foundDirectory = subDirectory.FindDirectory(path); - if (foundDirectory != null) - { - return foundDirectory; - } - } - - return null; - } - - public MockFile CreateFile(string path) - { - return this.CreateFile(path, string.Empty); - } - - public MockFile CreateFile(string path, string contents, bool createDirectories = false) - { - string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); - MockDirectory parentDirectory = this.FindDirectory(parentPath); - if (createDirectories) - { - if (parentDirectory == null) - { - parentDirectory = this.CreateDirectory(parentPath); - } - } - else - { - parentDirectory.ShouldNotBeNull(); - } - - MockFile newFile = new MockFile(path, contents); - parentDirectory.Files.Add(newFile.FullName, newFile); - - return newFile; - } - - public MockDirectory CreateDirectory(string path) - { - int lastSlashIdx = path.LastIndexOf(Path.DirectorySeparatorChar); - - if (lastSlashIdx <= 0) - { - return this; - } - - string parentPath = path.Substring(0, lastSlashIdx); - MockDirectory parentDirectory = this.FindDirectory(parentPath); - if (parentDirectory == null) - { - parentDirectory = this.CreateDirectory(parentPath); - } - - MockDirectory newDirectory; - if (!parentDirectory.Directories.TryGetValue(path, out newDirectory)) - { - newDirectory = new MockDirectory(path, null, null); - parentDirectory.Directories.Add(newDirectory.FullName, newDirectory); - } - - return newDirectory; - } - - public void DeleteDirectory(string path) - { - if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) - { - throw new NotSupportedException(); - } - - MockDirectory foundDirectory; - if (this.Directories.TryGetValue(path, out foundDirectory)) - { - this.Directories.Remove(path); - } - else - { - foreach (MockDirectory subDirectory in this.Directories.Values) - { - foundDirectory = subDirectory.FindDirectory(path); - if (foundDirectory != null) - { - subDirectory.DeleteDirectory(path); - return; - } - } - } - } - - public void MoveDirectory(string sourcePath, string targetPath) - { - MockDirectory sourceDirectory; - MockDirectory sourceDirectoryParent; - this.TryGetDirectoryAndParent(sourcePath, out sourceDirectory, out sourceDirectoryParent).ShouldEqual(true); - - int endPathIndex = targetPath.LastIndexOf(Path.DirectorySeparatorChar); - string targetDirectoryPath = targetPath.Substring(0, endPathIndex); - - MockDirectory targetDirectory = this.FindDirectory(targetDirectoryPath); - targetDirectory.ShouldNotBeNull(); - - sourceDirectoryParent.RemoveDirectory(sourceDirectory); - - sourceDirectory.FullName = targetPath; - - targetDirectory.AddDirectory(sourceDirectory); - } - - public void RemoveDirectory(MockDirectory directory) - { - this.Directories.ContainsKey(directory.FullName).ShouldEqual(true); - this.Directories.Remove(directory.FullName); - } - - private void AddDirectory(MockDirectory directory) - { - if (this.Directories.ContainsKey(directory.FullName)) - { - MockDirectory oldDirectory = this.Directories[directory.FullName]; - foreach (MockFile newFile in directory.Files.Values) - { - newFile.FullName = Path.Combine(oldDirectory.FullName, newFile.Name); - oldDirectory.AddOrOverwriteFile(newFile, newFile.FullName); - } - - foreach (MockDirectory newDirectory in directory.Directories.Values) - { - newDirectory.FullName = Path.Combine(oldDirectory.FullName, newDirectory.Name); - this.AddDirectory(newDirectory); - } - } - else - { - this.Directories.Add(directory.FullName, directory); - } - } - - private bool TryGetDirectoryAndParent(string path, out MockDirectory directory, out MockDirectory parentDirectory) - { - if (this.Directories.TryGetValue(path, out directory)) - { - parentDirectory = this; - return true; - } - else - { - string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); - parentDirectory = this.FindDirectory(parentPath); - if (parentDirectory != null) - { - foreach (MockDirectory subDirectory in this.Directories.Values) - { - directory = subDirectory.FindDirectory(path); - if (directory != null) - { - return true; - } - } - } - } - - directory = null; - parentDirectory = null; - return false; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class MockDirectory + { + public MockDirectory(string fullName, IEnumerable folders, IEnumerable files) + { + this.FullName = fullName; + this.Name = Path.GetFileName(this.FullName); + + this.Directories = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + this.Files = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + if (folders != null) + { + foreach (MockDirectory folder in folders) + { + this.Directories[folder.FullName] = folder; + } + } + + if (files != null) + { + foreach (MockFile file in files) + { + this.Files[file.FullName] = file; + } + } + + this.FileProperties = FileProperties.DefaultDirectory; + } + + public string FullName { get; private set; } + public string Name { get; private set; } + public Dictionary Directories { get; private set; } + public Dictionary Files { get; private set; } + public FileProperties FileProperties { get; set; } + + public MockFile FindFile(string path) + { + MockFile file; + if (this.Files.TryGetValue(path, out file)) + { + return file; + } + + foreach (MockDirectory directory in this.Directories.Values) + { + file = directory.FindFile(path); + if (file != null) + { + return file; + } + } + + return null; + } + + public void AddOrOverwriteFile(MockFile file, string path) + { + string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + + if (parentDirectory == null) + { + throw new IOException(); + } + + MockFile existingFileAtPath = parentDirectory.FindFile(path); + + if (existingFileAtPath != null) + { + parentDirectory.Files.Remove(path); + } + + parentDirectory.Files.Add(file.FullName, file); + } + + public void AddFile(MockFile file, string path) + { + string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + + if (parentDirectory == null) + { + throw new IOException(); + } + + MockFile existingFileAtPath = parentDirectory.FindFile(path); + existingFileAtPath.ShouldBeNull(); + + parentDirectory.Files.Add(file.FullName, file); + } + + public void RemoveFile(string path) + { + MockFile file; + if (this.Files.TryGetValue(path, out file)) + { + this.Files.Remove(path); + return; + } + + foreach (MockDirectory directory in this.Directories.Values) + { + file = directory.FindFile(path); + if (file != null) + { + directory.RemoveFile(path); + return; + } + } + } + + public MockDirectory FindDirectory(string path) + { + if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) + { + return this; + } + + MockDirectory foundDirectory; + if (this.Directories.TryGetValue(path, out foundDirectory)) + { + return foundDirectory; + } + + foreach (MockDirectory subDirectory in this.Directories.Values) + { + foundDirectory = subDirectory.FindDirectory(path); + if (foundDirectory != null) + { + return foundDirectory; + } + } + + return null; + } + + public MockFile CreateFile(string path) + { + return this.CreateFile(path, string.Empty); + } + + public MockFile CreateFile(string path, string contents, bool createDirectories = false) + { + string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + if (createDirectories) + { + if (parentDirectory == null) + { + parentDirectory = this.CreateDirectory(parentPath); + } + } + else + { + parentDirectory.ShouldNotBeNull(); + } + + MockFile newFile = new MockFile(path, contents); + parentDirectory.Files.Add(newFile.FullName, newFile); + + return newFile; + } + + public MockDirectory CreateDirectory(string path) + { + int lastSlashIdx = path.LastIndexOf(Path.DirectorySeparatorChar); + + if (lastSlashIdx <= 0) + { + return this; + } + + string parentPath = path.Substring(0, lastSlashIdx); + MockDirectory parentDirectory = this.FindDirectory(parentPath); + if (parentDirectory == null) + { + parentDirectory = this.CreateDirectory(parentPath); + } + + MockDirectory newDirectory; + if (!parentDirectory.Directories.TryGetValue(path, out newDirectory)) + { + newDirectory = new MockDirectory(path, null, null); + parentDirectory.Directories.Add(newDirectory.FullName, newDirectory); + } + + return newDirectory; + } + + public void DeleteDirectory(string path) + { + if (path.Equals(this.FullName, StringComparison.InvariantCultureIgnoreCase)) + { + throw new NotSupportedException(); + } + + MockDirectory foundDirectory; + if (this.Directories.TryGetValue(path, out foundDirectory)) + { + this.Directories.Remove(path); + } + else + { + foreach (MockDirectory subDirectory in this.Directories.Values) + { + foundDirectory = subDirectory.FindDirectory(path); + if (foundDirectory != null) + { + subDirectory.DeleteDirectory(path); + return; + } + } + } + } + + public void MoveDirectory(string sourcePath, string targetPath) + { + MockDirectory sourceDirectory; + MockDirectory sourceDirectoryParent; + this.TryGetDirectoryAndParent(sourcePath, out sourceDirectory, out sourceDirectoryParent).ShouldEqual(true); + + int endPathIndex = targetPath.LastIndexOf(Path.DirectorySeparatorChar); + string targetDirectoryPath = targetPath.Substring(0, endPathIndex); + + MockDirectory targetDirectory = this.FindDirectory(targetDirectoryPath); + targetDirectory.ShouldNotBeNull(); + + sourceDirectoryParent.RemoveDirectory(sourceDirectory); + + sourceDirectory.FullName = targetPath; + + targetDirectory.AddDirectory(sourceDirectory); + } + + public void RemoveDirectory(MockDirectory directory) + { + this.Directories.ContainsKey(directory.FullName).ShouldEqual(true); + this.Directories.Remove(directory.FullName); + } + + private void AddDirectory(MockDirectory directory) + { + if (this.Directories.ContainsKey(directory.FullName)) + { + MockDirectory oldDirectory = this.Directories[directory.FullName]; + foreach (MockFile newFile in directory.Files.Values) + { + newFile.FullName = Path.Combine(oldDirectory.FullName, newFile.Name); + oldDirectory.AddOrOverwriteFile(newFile, newFile.FullName); + } + + foreach (MockDirectory newDirectory in directory.Directories.Values) + { + newDirectory.FullName = Path.Combine(oldDirectory.FullName, newDirectory.Name); + this.AddDirectory(newDirectory); + } + } + else + { + this.Directories.Add(directory.FullName, directory); + } + } + + private bool TryGetDirectoryAndParent(string path, out MockDirectory directory, out MockDirectory parentDirectory) + { + if (this.Directories.TryGetValue(path, out directory)) + { + parentDirectory = this; + return true; + } + else + { + string parentPath = path.Substring(0, path.LastIndexOf(Path.DirectorySeparatorChar)); + parentDirectory = this.FindDirectory(parentPath); + if (parentDirectory != null) + { + foreach (MockDirectory subDirectory in this.Directories.Values) + { + directory = subDirectory.FindDirectory(path); + if (directory != null) + { + return true; + } + } + } + } + + directory = null; + parentDirectory = null; + return false; + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/MockFile.cs b/Scalar.UnitTests/Mock/FileSystem/MockFile.cs index 7b29ee272a..8541135f0d 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockFile.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockFile.cs @@ -1,69 +1,69 @@ -using Scalar.Common.FileSystem; -using System; -using System.IO; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class MockFile - { - private ReusableMemoryStream contentStream; - private FileProperties fileProperties; - - public MockFile(string fullName, string contents) - { - this.FullName = fullName; - this.Name = Path.GetFileName(this.FullName); - - this.FileProperties = FileProperties.DefaultFile; - - this.contentStream = new ReusableMemoryStream(contents); - } - - public MockFile(string fullName, byte[] contents) - { - this.FullName = fullName; - this.Name = Path.GetFileName(this.FullName); - - this.FileProperties = FileProperties.DefaultFile; - - this.contentStream = new ReusableMemoryStream(contents); - } - - public event Action Changed; - - public string FullName { get; set; } - public string Name { get; set; } - public FileProperties FileProperties - { - get - { - // The underlying content stream is the correct/true source of the file length - // Create a new copy of the properties to make sure the length is set correctly. - FileProperties newProperties = new FileProperties( - this.fileProperties.FileAttributes, - this.fileProperties.CreationTimeUTC, - this.fileProperties.LastAccessTimeUTC, - this.fileProperties.LastWriteTimeUTC, - this.contentStream.Length); - - this.fileProperties = newProperties; - return this.fileProperties; - } - - set - { - this.fileProperties = value; - if (this.Changed != null) - { - this.Changed(); - } - } - } - - public Stream GetContentStream() - { - this.contentStream.Position = 0; - return this.contentStream; - } - } -} +using Scalar.Common.FileSystem; +using System; +using System.IO; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class MockFile + { + private ReusableMemoryStream contentStream; + private FileProperties fileProperties; + + public MockFile(string fullName, string contents) + { + this.FullName = fullName; + this.Name = Path.GetFileName(this.FullName); + + this.FileProperties = FileProperties.DefaultFile; + + this.contentStream = new ReusableMemoryStream(contents); + } + + public MockFile(string fullName, byte[] contents) + { + this.FullName = fullName; + this.Name = Path.GetFileName(this.FullName); + + this.FileProperties = FileProperties.DefaultFile; + + this.contentStream = new ReusableMemoryStream(contents); + } + + public event Action Changed; + + public string FullName { get; set; } + public string Name { get; set; } + public FileProperties FileProperties + { + get + { + // The underlying content stream is the correct/true source of the file length + // Create a new copy of the properties to make sure the length is set correctly. + FileProperties newProperties = new FileProperties( + this.fileProperties.FileAttributes, + this.fileProperties.CreationTimeUTC, + this.fileProperties.LastAccessTimeUTC, + this.fileProperties.LastWriteTimeUTC, + this.contentStream.Length); + + this.fileProperties = newProperties; + return this.fileProperties; + } + + set + { + this.fileProperties = value; + if (this.Changed != null) + { + this.Changed(); + } + } + } + + public Stream GetContentStream() + { + this.contentStream.Position = 0; + return this.contentStream; + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs b/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs index 68f3cf504c..62a7afabb5 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockFileSystem.cs @@ -1,363 +1,363 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class MockFileSystem : PhysicalFileSystem - { - public MockFileSystem(MockDirectory rootDirectory) - { - this.RootDirectory = rootDirectory; - this.DeleteNonExistentFileThrowsException = true; - this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; - } - - public MockDirectory RootDirectory { get; private set; } - - public bool DeleteFileThrowsException { get; set; } - public Exception ExceptionThrownByCreateDirectory { get; set; } - - public bool TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed { get; set; } - - /// - /// Allow FileMoves without checking the input arguments. - /// This is to support tests that just want to allow arbitrary - /// MoveFile calls to succeed. - /// - public bool AllowMoveFile { get; set; } - - /// - /// Normal behavior C# File.Delete(..) is to not throw if the file to - /// be deleted does not exist. However, existing behavior of this mock - /// is to throw. This flag allows consumers to control this behavior. - /// - public bool DeleteNonExistentFileThrowsException { get; set; } - - public override void DeleteDirectory(string path, bool recursive = true) - { - if (!recursive) - { - throw new NotImplementedException(); - } - - this.RootDirectory.DeleteDirectory(path); - } - - public override bool FileExists(string path) - { - return this.RootDirectory.FindFile(path) != null; - } - - public override bool DirectoryExists(string path) - { - return this.RootDirectory.FindDirectory(path) != null; - } - - public override void CopyFile(string sourcePath, string destinationPath, bool overwrite) - { - throw new NotImplementedException(); - } - - public override void DeleteFile(string path) - { - if (this.DeleteFileThrowsException) - { - throw new IOException("Exception when deleting file"); - } - - MockFile file = this.RootDirectory.FindFile(path); - - if (file == null && !this.DeleteNonExistentFileThrowsException) - { - return; - } - - file.ShouldNotBeNull(); - - this.RootDirectory.RemoveFile(path); - } - - public override void MoveAndOverwriteFile(string sourcePath, string destinationPath) - { - if (sourcePath == null || destinationPath == null) - { - throw new ArgumentNullException(); - } - - if (this.AllowMoveFile) - { - return; - } - - MockFile sourceFile = this.RootDirectory.FindFile(sourcePath); - MockFile destinationFile = this.RootDirectory.FindFile(destinationPath); - if (sourceFile == null) - { - throw new FileNotFoundException(); - } - - if (destinationFile != null) - { - this.RootDirectory.RemoveFile(destinationPath); - } - - this.WriteAllText(destinationPath, this.ReadAllText(sourcePath)); - this.RootDirectory.RemoveFile(sourcePath); - } - - public override bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) - { - normalizedPath = path; - errorMessage = null; - return true; - } - - public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) - { - MockFile file = this.RootDirectory.FindFile(path); - if (fileMode == FileMode.Create) - { - if (file != null) - { - this.RootDirectory.RemoveFile(path); - } - - return this.CreateAndOpenFileStream(path); - } - - if (fileMode == FileMode.OpenOrCreate) - { - if (file == null) - { - return this.CreateAndOpenFileStream(path); - } - } - else - { - file.ShouldNotBeNull(); - } - - return file.GetContentStream(); - } - - public override void FlushFileBuffers(string path) - { - throw new NotImplementedException(); - } - - public override void WriteAllText(string path, string contents) - { - MockFile file = new MockFile(path, contents); - this.RootDirectory.AddOrOverwriteFile(file, path); - } - - public override string ReadAllText(string path) - { - MockFile file = this.RootDirectory.FindFile(path); - - using (StreamReader reader = new StreamReader(file.GetContentStream())) - { - return reader.ReadToEnd(); - } - } - - public override byte[] ReadAllBytes(string path) - { - MockFile file = this.RootDirectory.FindFile(path); - - using (Stream s = file.GetContentStream()) - { - int count = (int)s.Length; - - int pos = 0; - byte[] result = new byte[count]; - while (count > 0) - { - int n = s.Read(result, pos, count); - if (n == 0) - { - throw new IOException("Unexpected end of stream"); - } - - pos += n; - count -= n; - } - - return result; - } - } - - public override IEnumerable ReadLines(string path) - { - MockFile file = this.RootDirectory.FindFile(path); - using (StreamReader reader = new StreamReader(file.GetContentStream())) - { - while (!reader.EndOfStream) - { - yield return reader.ReadLine(); - } - } - } - - public override void CreateDirectory(string path) - { - if (this.ExceptionThrownByCreateDirectory != null) - { - throw this.ExceptionThrownByCreateDirectory; - } - - this.RootDirectory.CreateDirectory(path); - } - - public override bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) - { - throw new NotImplementedException(); - } - - public override bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) - { - error = null; - - if (this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed) - { - // TryCreateOrUpdateDirectoryToAdminModifyPermissions is typically called for paths in C:\ProgramData\Scalar, - // if it's called for one of those paths remap the paths to be inside the mock: root - string mockDirectoryPath = directoryPath; - string scalarProgramData = @"C:\ProgramData\Scalar"; - if (directoryPath.StartsWith(scalarProgramData, StringComparison.OrdinalIgnoreCase)) - { - mockDirectoryPath = mockDirectoryPath.Substring(scalarProgramData.Length); - mockDirectoryPath = "mock:" + mockDirectoryPath; - } - - this.RootDirectory.CreateDirectory(mockDirectoryPath); - return true; - } - - return false; - } - - public override IEnumerable ItemsInDirectory(string path) - { - MockDirectory directory = this.RootDirectory.FindDirectory(path); - directory.ShouldNotBeNull(); - - foreach (MockDirectory subDirectory in directory.Directories.Values) - { - yield return new DirectoryItemInfo() - { - Name = subDirectory.Name, - FullName = subDirectory.FullName, - IsDirectory = true - }; - } - - foreach (MockFile file in directory.Files.Values) - { - yield return new DirectoryItemInfo() - { - FullName = file.FullName, - Name = file.Name, - IsDirectory = false, - Length = file.FileProperties.Length - }; - } - } - - public override IEnumerable EnumerateDirectories(string path) - { - MockDirectory directory = this.RootDirectory.FindDirectory(path); - directory.ShouldNotBeNull(); - - if (directory != null) - { - foreach (MockDirectory subDirectory in directory.Directories.Values) - { - yield return subDirectory.Name; - } - } - } - - public override FileProperties GetFileProperties(string path) - { - MockFile file = this.RootDirectory.FindFile(path); - if (file != null) - { - return file.FileProperties; - } - else - { - return FileProperties.DefaultFile; - } - } - - public override FileAttributes GetAttributes(string path) - { - return FileAttributes.Normal; - } - - public override void SetAttributes(string path, FileAttributes fileAttributes) - { - } - - public override void MoveFile(string sourcePath, string targetPath) - { - if (this.AllowMoveFile) - { - return; - } - else - { - throw new NotImplementedException(); - } - } - - public override string[] GetFiles(string directoryPath, string mask) - { - if (!mask.Equals("*")) - { - throw new NotImplementedException(); - } - - MockDirectory directory = this.RootDirectory.FindDirectory(directoryPath); - directory.ShouldNotBeNull(); - - List files = new List(); - foreach (MockFile file in directory.Files.Values) - { - files.Add(file.FullName); - } - - return files.ToArray(); - } - - public override FileVersionInfo GetVersionInfo(string path) - { - throw new NotImplementedException(); - } - - public override bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) - { - throw new NotImplementedException(); - } - - public override bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) - { - throw new NotImplementedException(); - } - - private Stream CreateAndOpenFileStream(string path) - { - MockFile file = this.RootDirectory.CreateFile(path); - file.ShouldNotBeNull(); - - return this.OpenFileStream(path, FileMode.Open, (FileAccess)NativeMethods.FileAccess.FILE_GENERIC_READ, FileShare.Read, callFlushFileBuffers: false); - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class MockFileSystem : PhysicalFileSystem + { + public MockFileSystem(MockDirectory rootDirectory) + { + this.RootDirectory = rootDirectory; + this.DeleteNonExistentFileThrowsException = true; + this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed = true; + } + + public MockDirectory RootDirectory { get; private set; } + + public bool DeleteFileThrowsException { get; set; } + public Exception ExceptionThrownByCreateDirectory { get; set; } + + public bool TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed { get; set; } + + /// + /// Allow FileMoves without checking the input arguments. + /// This is to support tests that just want to allow arbitrary + /// MoveFile calls to succeed. + /// + public bool AllowMoveFile { get; set; } + + /// + /// Normal behavior C# File.Delete(..) is to not throw if the file to + /// be deleted does not exist. However, existing behavior of this mock + /// is to throw. This flag allows consumers to control this behavior. + /// + public bool DeleteNonExistentFileThrowsException { get; set; } + + public override void DeleteDirectory(string path, bool recursive = true) + { + if (!recursive) + { + throw new NotImplementedException(); + } + + this.RootDirectory.DeleteDirectory(path); + } + + public override bool FileExists(string path) + { + return this.RootDirectory.FindFile(path) != null; + } + + public override bool DirectoryExists(string path) + { + return this.RootDirectory.FindDirectory(path) != null; + } + + public override void CopyFile(string sourcePath, string destinationPath, bool overwrite) + { + throw new NotImplementedException(); + } + + public override void DeleteFile(string path) + { + if (this.DeleteFileThrowsException) + { + throw new IOException("Exception when deleting file"); + } + + MockFile file = this.RootDirectory.FindFile(path); + + if (file == null && !this.DeleteNonExistentFileThrowsException) + { + return; + } + + file.ShouldNotBeNull(); + + this.RootDirectory.RemoveFile(path); + } + + public override void MoveAndOverwriteFile(string sourcePath, string destinationPath) + { + if (sourcePath == null || destinationPath == null) + { + throw new ArgumentNullException(); + } + + if (this.AllowMoveFile) + { + return; + } + + MockFile sourceFile = this.RootDirectory.FindFile(sourcePath); + MockFile destinationFile = this.RootDirectory.FindFile(destinationPath); + if (sourceFile == null) + { + throw new FileNotFoundException(); + } + + if (destinationFile != null) + { + this.RootDirectory.RemoveFile(destinationPath); + } + + this.WriteAllText(destinationPath, this.ReadAllText(sourcePath)); + this.RootDirectory.RemoveFile(sourcePath); + } + + public override bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) + { + normalizedPath = path; + errorMessage = null; + return true; + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) + { + MockFile file = this.RootDirectory.FindFile(path); + if (fileMode == FileMode.Create) + { + if (file != null) + { + this.RootDirectory.RemoveFile(path); + } + + return this.CreateAndOpenFileStream(path); + } + + if (fileMode == FileMode.OpenOrCreate) + { + if (file == null) + { + return this.CreateAndOpenFileStream(path); + } + } + else + { + file.ShouldNotBeNull(); + } + + return file.GetContentStream(); + } + + public override void FlushFileBuffers(string path) + { + throw new NotImplementedException(); + } + + public override void WriteAllText(string path, string contents) + { + MockFile file = new MockFile(path, contents); + this.RootDirectory.AddOrOverwriteFile(file, path); + } + + public override string ReadAllText(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + + using (StreamReader reader = new StreamReader(file.GetContentStream())) + { + return reader.ReadToEnd(); + } + } + + public override byte[] ReadAllBytes(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + + using (Stream s = file.GetContentStream()) + { + int count = (int)s.Length; + + int pos = 0; + byte[] result = new byte[count]; + while (count > 0) + { + int n = s.Read(result, pos, count); + if (n == 0) + { + throw new IOException("Unexpected end of stream"); + } + + pos += n; + count -= n; + } + + return result; + } + } + + public override IEnumerable ReadLines(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + using (StreamReader reader = new StreamReader(file.GetContentStream())) + { + while (!reader.EndOfStream) + { + yield return reader.ReadLine(); + } + } + } + + public override void CreateDirectory(string path) + { + if (this.ExceptionThrownByCreateDirectory != null) + { + throw this.ExceptionThrownByCreateDirectory; + } + + this.RootDirectory.CreateDirectory(path); + } + + public override bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) + { + throw new NotImplementedException(); + } + + public override bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) + { + error = null; + + if (this.TryCreateOrUpdateDirectoryToAdminModifyPermissionsShouldSucceed) + { + // TryCreateOrUpdateDirectoryToAdminModifyPermissions is typically called for paths in C:\ProgramData\Scalar, + // if it's called for one of those paths remap the paths to be inside the mock: root + string mockDirectoryPath = directoryPath; + string scalarProgramData = @"C:\ProgramData\Scalar"; + if (directoryPath.StartsWith(scalarProgramData, StringComparison.OrdinalIgnoreCase)) + { + mockDirectoryPath = mockDirectoryPath.Substring(scalarProgramData.Length); + mockDirectoryPath = "mock:" + mockDirectoryPath; + } + + this.RootDirectory.CreateDirectory(mockDirectoryPath); + return true; + } + + return false; + } + + public override IEnumerable ItemsInDirectory(string path) + { + MockDirectory directory = this.RootDirectory.FindDirectory(path); + directory.ShouldNotBeNull(); + + foreach (MockDirectory subDirectory in directory.Directories.Values) + { + yield return new DirectoryItemInfo() + { + Name = subDirectory.Name, + FullName = subDirectory.FullName, + IsDirectory = true + }; + } + + foreach (MockFile file in directory.Files.Values) + { + yield return new DirectoryItemInfo() + { + FullName = file.FullName, + Name = file.Name, + IsDirectory = false, + Length = file.FileProperties.Length + }; + } + } + + public override IEnumerable EnumerateDirectories(string path) + { + MockDirectory directory = this.RootDirectory.FindDirectory(path); + directory.ShouldNotBeNull(); + + if (directory != null) + { + foreach (MockDirectory subDirectory in directory.Directories.Values) + { + yield return subDirectory.Name; + } + } + } + + public override FileProperties GetFileProperties(string path) + { + MockFile file = this.RootDirectory.FindFile(path); + if (file != null) + { + return file.FileProperties; + } + else + { + return FileProperties.DefaultFile; + } + } + + public override FileAttributes GetAttributes(string path) + { + return FileAttributes.Normal; + } + + public override void SetAttributes(string path, FileAttributes fileAttributes) + { + } + + public override void MoveFile(string sourcePath, string targetPath) + { + if (this.AllowMoveFile) + { + return; + } + else + { + throw new NotImplementedException(); + } + } + + public override string[] GetFiles(string directoryPath, string mask) + { + if (!mask.Equals("*")) + { + throw new NotImplementedException(); + } + + MockDirectory directory = this.RootDirectory.FindDirectory(directoryPath); + directory.ShouldNotBeNull(); + + List files = new List(); + foreach (MockFile file in directory.Files.Values) + { + files.Add(file.FullName); + } + + return files.ToArray(); + } + + public override FileVersionInfo GetVersionInfo(string path) + { + throw new NotImplementedException(); + } + + public override bool FileVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) + { + throw new NotImplementedException(); + } + + public override bool ProductVersionsMatch(FileVersionInfo versionInfo1, FileVersionInfo versionInfo2) + { + throw new NotImplementedException(); + } + + private Stream CreateAndOpenFileStream(string path) + { + MockFile file = this.RootDirectory.CreateFile(path); + file.ShouldNotBeNull(); + + return this.OpenFileStream(path, FileMode.Open, (FileAccess)NativeMethods.FileAccess.FILE_GENERIC_READ, FileShare.Read, callFlushFileBuffers: false); + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/MockFileSystemWithCallbacks.cs b/Scalar.UnitTests/Mock/FileSystem/MockFileSystemWithCallbacks.cs index 989b98e3b8..8f2fd75087 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockFileSystemWithCallbacks.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockFileSystemWithCallbacks.cs @@ -1,79 +1,79 @@ -using Scalar.Common.FileSystem; -using System; -using System.IO; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class MockFileSystemWithCallbacks : PhysicalFileSystem - { - public Func OnFileExists { get; set; } - - public Func OnOpenFileStream { get; set; } - - public override FileProperties GetFileProperties(string path) - { - throw new InvalidOperationException("GetFileProperties has not been implemented."); - } - - public override bool FileExists(string path) - { - if (this.OnFileExists == null) - { - throw new InvalidOperationException("OnFileExists should be set if it is expected to be called."); - } - - return this.OnFileExists(); - } - - public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) - { - if (this.OnOpenFileStream == null) - { - throw new InvalidOperationException("OnOpenFileStream should be set if it is expected to be called."); - } - - return this.OnOpenFileStream(path, fileMode, fileAccess); - } - - public override void WriteAllText(string path, string contents) - { - } - - public override string ReadAllText(string path) - { - throw new InvalidOperationException("ReadAllText has not been implemented."); - } - - public override void DeleteFile(string path) - { - } - - public override void DeleteDirectory(string path, bool recursive = true) - { - throw new InvalidOperationException("DeleteDirectory has not been implemented."); - } - - public override void CreateDirectory(string path) - { - } - - public override FileAttributes GetAttributes(string path) - { - return FileAttributes.Normal; - } - - public override void SetAttributes(string path, FileAttributes fileAttributes) - { - } - - public override void MoveFile(string sourcePath, string targetPath) - { - throw new NotImplementedException(); - } - - public override string[] GetFiles(string directoryPath, string mask) - { - throw new NotImplementedException(); - } - } -} +using Scalar.Common.FileSystem; +using System; +using System.IO; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class MockFileSystemWithCallbacks : PhysicalFileSystem + { + public Func OnFileExists { get; set; } + + public Func OnOpenFileStream { get; set; } + + public override FileProperties GetFileProperties(string path) + { + throw new InvalidOperationException("GetFileProperties has not been implemented."); + } + + public override bool FileExists(string path) + { + if (this.OnFileExists == null) + { + throw new InvalidOperationException("OnFileExists should be set if it is expected to be called."); + } + + return this.OnFileExists(); + } + + public override Stream OpenFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare shareMode, FileOptions options, bool flushesToDisk) + { + if (this.OnOpenFileStream == null) + { + throw new InvalidOperationException("OnOpenFileStream should be set if it is expected to be called."); + } + + return this.OnOpenFileStream(path, fileMode, fileAccess); + } + + public override void WriteAllText(string path, string contents) + { + } + + public override string ReadAllText(string path) + { + throw new InvalidOperationException("ReadAllText has not been implemented."); + } + + public override void DeleteFile(string path) + { + } + + public override void DeleteDirectory(string path, bool recursive = true) + { + throw new InvalidOperationException("DeleteDirectory has not been implemented."); + } + + public override void CreateDirectory(string path) + { + } + + public override FileAttributes GetAttributes(string path) + { + return FileAttributes.Normal; + } + + public override void SetAttributes(string path, FileAttributes fileAttributes) + { + } + + public override void MoveFile(string sourcePath, string targetPath) + { + throw new NotImplementedException(); + } + + public override string[] GetFiles(string directoryPath, string mask) + { + throw new NotImplementedException(); + } + } +} diff --git a/Scalar.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs b/Scalar.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs index 2bec2104e2..dfb920839f 100644 --- a/Scalar.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs +++ b/Scalar.UnitTests/Mock/FileSystem/MockPlatformFileSystem.cs @@ -1,64 +1,64 @@ -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; - -namespace Scalar.UnitTests.Mock.FileSystem -{ - public class MockPlatformFileSystem : IPlatformFileSystem - { - public bool SupportsFileMode { get; } = true; - - public void FlushFileBuffers(string path) - { - throw new NotSupportedException(); - } - - public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) - { - throw new NotSupportedException(); - } - - public void ChangeMode(string path, ushort mode) - { - throw new NotSupportedException(); - } - - public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) - { - errorMessage = null; - normalizedPath = path; - return true; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; + +namespace Scalar.UnitTests.Mock.FileSystem +{ + public class MockPlatformFileSystem : IPlatformFileSystem + { + public bool SupportsFileMode { get; } = true; + + public void FlushFileBuffers(string path) + { + throw new NotSupportedException(); + } + + public void MoveAndOverwriteFile(string sourceFileName, string destinationFilename) + { + throw new NotSupportedException(); + } + + public void ChangeMode(string path, ushort mode) + { + throw new NotSupportedException(); + } + + public bool TryGetNormalizedPath(string path, out string normalizedPath, out string errorMessage) + { + errorMessage = null; + normalizedPath = path; + return true; + } + + public bool HydrateFile(string fileName, byte[] buffer) + { + throw new NotSupportedException(); } - public bool HydrateFile(string fileName, byte[] buffer) - { - throw new NotSupportedException(); - } - - public bool IsExecutable(string fileName) - { - throw new NotSupportedException(); - } - - public bool IsSocket(string fileName) - { - throw new NotSupportedException(); - } - - public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) - { - throw new NotSupportedException(); - } - - public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) - { - throw new NotSupportedException(); - } - - public bool IsFileSystemSupported(string path, out string error) - { - error = null; - return true; - } - } -} + public bool IsExecutable(string fileName) + { + throw new NotSupportedException(); + } + + public bool IsSocket(string fileName) + { + throw new NotSupportedException(); + } + + public bool TryCreateDirectoryWithAdminAndUserModifyPermissions(string directoryPath, out string error) + { + throw new NotSupportedException(); + } + + public bool TryCreateOrUpdateDirectoryToAdminModifyPermissions(ITracer tracer, string directoryPath, out string error) + { + throw new NotSupportedException(); + } + + public bool IsFileSystemSupported(string path, out string error) + { + error = null; + return true; + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockBatchHttpGitObjects.cs b/Scalar.UnitTests/Mock/Git/MockBatchHttpGitObjects.cs index 2f508c2120..4287162440 100644 --- a/Scalar.UnitTests/Mock/Git/MockBatchHttpGitObjects.cs +++ b/Scalar.UnitTests/Mock/Git/MockBatchHttpGitObjects.cs @@ -1,122 +1,122 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Text; -using System.Threading; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockBatchHttpGitObjects : GitObjectsHttpRequestor - { - private Func objectResolver; - - public MockBatchHttpGitObjects(ITracer tracer, Enlistment enlistment, Func objectResolver) - : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig()) - { - this.objectResolver = objectResolver; - } - - public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - public override GitRefs QueryInfoRefs(string branch) - { - throw new NotImplementedException(); - } - - public override RetryWrapper.InvocationResult TryDownloadObjects( - Func> objectIdGenerator, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects); - } - - public override RetryWrapper.InvocationResult TryDownloadObjects( - IEnumerable objectIds, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - return this.StreamObjects(objectIds, onSuccess, onFailure); - } - - private RetryWrapper.InvocationResult StreamObjects( - IEnumerable objectIds, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure) - { - for (int i = 0; i < this.RetryConfig.MaxAttempts; ++i) - { - try - { - using (ReusableMemoryStream mem = new ReusableMemoryStream(string.Empty)) - using (BinaryWriter writer = new BinaryWriter(mem)) - { - writer.Write(new byte[] { (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', 1 }); - - foreach (string objectId in objectIds) - { - string contents = this.objectResolver(objectId); - if (!string.IsNullOrEmpty(contents)) - { - writer.Write(this.SHA1BytesFromString(objectId)); - byte[] bytes = Encoding.UTF8.GetBytes(contents); - writer.Write((long)bytes.Length); - writer.Write(bytes); - } - else - { - writer.Write(new byte[20]); - writer.Write(0L); - } - } - - writer.Write(new byte[20]); - writer.Flush(); - mem.Seek(0, SeekOrigin.Begin); - - using (GitEndPointResponseData response = new GitEndPointResponseData( - HttpStatusCode.OK, - ScalarConstants.MediaTypes.CustomLooseObjectsMediaType, - mem, - message: null, - onResponseDisposed: null)) - { - RetryWrapper.CallbackResult result = onSuccess(1, response); - return new RetryWrapper.InvocationResult(1, true, result.Result); - } - } - } - catch - { - continue; - } - } - - return new RetryWrapper.InvocationResult(this.RetryConfig.MaxAttempts, null); - } - - private byte[] SHA1BytesFromString(string s) - { - s.Length.ShouldEqual(40); - - byte[] output = new byte[20]; - for (int x = 0; x < s.Length; x += 2) - { - output[x / 2] = Convert.ToByte(s.Substring(x, 2), 16); - } - - return output; - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockBatchHttpGitObjects : GitObjectsHttpRequestor + { + private Func objectResolver; + + public MockBatchHttpGitObjects(ITracer tracer, Enlistment enlistment, Func objectResolver) + : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig()) + { + this.objectResolver = objectResolver; + } + + public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public override GitRefs QueryInfoRefs(string branch) + { + throw new NotImplementedException(); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.StreamObjects(objectIds, onSuccess, onFailure); + } + + private RetryWrapper.InvocationResult StreamObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + for (int i = 0; i < this.RetryConfig.MaxAttempts; ++i) + { + try + { + using (ReusableMemoryStream mem = new ReusableMemoryStream(string.Empty)) + using (BinaryWriter writer = new BinaryWriter(mem)) + { + writer.Write(new byte[] { (byte)'G', (byte)'V', (byte)'F', (byte)'S', (byte)' ', 1 }); + + foreach (string objectId in objectIds) + { + string contents = this.objectResolver(objectId); + if (!string.IsNullOrEmpty(contents)) + { + writer.Write(this.SHA1BytesFromString(objectId)); + byte[] bytes = Encoding.UTF8.GetBytes(contents); + writer.Write((long)bytes.Length); + writer.Write(bytes); + } + else + { + writer.Write(new byte[20]); + writer.Write(0L); + } + } + + writer.Write(new byte[20]); + writer.Flush(); + mem.Seek(0, SeekOrigin.Begin); + + using (GitEndPointResponseData response = new GitEndPointResponseData( + HttpStatusCode.OK, + ScalarConstants.MediaTypes.CustomLooseObjectsMediaType, + mem, + message: null, + onResponseDisposed: null)) + { + RetryWrapper.CallbackResult result = onSuccess(1, response); + return new RetryWrapper.InvocationResult(1, true, result.Result); + } + } + } + catch + { + continue; + } + } + + return new RetryWrapper.InvocationResult(this.RetryConfig.MaxAttempts, null); + } + + private byte[] SHA1BytesFromString(string s) + { + s.Length.ShouldEqual(40); + + byte[] output = new byte[20]; + for (int x = 0; x < s.Length; x += 2) + { + output[x / 2] = Convert.ToByte(s.Substring(x, 2), 16); + } + + return output; + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockGitInstallation.cs b/Scalar.UnitTests/Mock/Git/MockGitInstallation.cs index 0c7d107491..d4f06c3768 100644 --- a/Scalar.UnitTests/Mock/Git/MockGitInstallation.cs +++ b/Scalar.UnitTests/Mock/Git/MockGitInstallation.cs @@ -1,22 +1,22 @@ -using Scalar.Common.Git; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockGitInstallation : IGitInstallation - { - public bool GitExists(string gitBinPath) - { - return false; - } - - public string GetInstalledGitBinPath() - { - return null; - } - } -} +using Scalar.Common.Git; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockGitInstallation : IGitInstallation + { + public bool GitExists(string gitBinPath) + { + return false; + } + + public string GetInstalledGitBinPath() + { + return null; + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockGitProcess.cs b/Scalar.UnitTests/Mock/Git/MockGitProcess.cs index 2dee7d95fa..a83581c568 100644 --- a/Scalar.UnitTests/Mock/Git/MockGitProcess.cs +++ b/Scalar.UnitTests/Mock/Git/MockGitProcess.cs @@ -1,146 +1,146 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockGitProcess : GitProcess - { - private List expectedCommandInfos = new List(); - - public MockGitProcess() - : base(new MockScalarEnlistment()) - { - this.CommandsRun = new List(); - this.StoredCredentials = new Dictionary(StringComparer.OrdinalIgnoreCase); - this.CredentialApprovals = new Dictionary>(); - this.CredentialRejections = new Dictionary>(); - } - - public List CommandsRun { get; } - public bool ShouldFail { get; set; } - public Dictionary StoredCredentials { get; } - public Dictionary> CredentialApprovals { get; } - public Dictionary> CredentialRejections { get; } - - public void SetExpectedCommandResult(string command, Func result, bool matchPrefix = false) - { - CommandInfo commandInfo = new CommandInfo(command, result, matchPrefix); - this.expectedCommandInfos.Add(commandInfo); - } - - public override bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string error) - { - Credential credential = new Credential(username, password); - - // Record the approval request for this credential - List acceptedCredentials; - if (!this.CredentialApprovals.TryGetValue(repoUrl, out acceptedCredentials)) - { - acceptedCredentials = new List(); - this.CredentialApprovals[repoUrl] = acceptedCredentials; - } - - acceptedCredentials.Add(credential); - - // Store the credential - this.StoredCredentials[repoUrl] = credential; - - return base.TryStoreCredential(tracer, repoUrl, username, password, out error); - } - - public override bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string error) - { - Credential credential = new Credential(username, password); - - // Record the rejection request for this credential - List rejectedCredentials; - if (!this.CredentialRejections.TryGetValue(repoUrl, out rejectedCredentials)) - { - rejectedCredentials = new List(); - this.CredentialRejections[repoUrl] = rejectedCredentials; - } - - rejectedCredentials.Add(credential); - - // Erase the credential - this.StoredCredentials.Remove(repoUrl); - - return base.TryDeleteCredential(tracer, repoUrl, username, password, out error); - } - - protected override Result InvokeGitImpl( - string command, - string workingDirectory, - string dotGitDirectory, - bool useReadObjectHook, - Action writeStdIn, - Action parseStdOutLine, - int timeoutMs, - string gitObjectsDirectory = null) - { - this.CommandsRun.Add(command); - - if (this.ShouldFail) - { - return new Result(string.Empty, string.Empty, Result.GenericFailureCode); - } - - Predicate commandMatchFunction = - (CommandInfo commandInfo) => - { - if (commandInfo.MatchPrefix) - { - return command.StartsWith(commandInfo.Command); - } - else - { - return string.Equals(command, commandInfo.Command, StringComparison.Ordinal); - } - }; - - CommandInfo matchedCommand = this.expectedCommandInfos.Find(commandMatchFunction); - matchedCommand.ShouldNotBeNull("Unexpected command: " + command); - - return matchedCommand.Result(); - } - - public class Credential - { - public Credential(string username, string password) - { - this.Username = username; - this.Password = password; - } - - public string Username { get; } - public string Password { get; } - - public string BasicAuthString - { - get => Convert.ToBase64String(Encoding.ASCII.GetBytes(this.Username + ":" + this.Password)); - } - } - - private class CommandInfo - { - public CommandInfo(string command, Func result, bool matchPrefix) - { - this.Command = command; - this.Result = result; - this.MatchPrefix = matchPrefix; - } - - public string Command { get; private set; } - - public Func Result { get; private set; } - - public bool MatchPrefix { get; private set; } - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockGitProcess : GitProcess + { + private List expectedCommandInfos = new List(); + + public MockGitProcess() + : base(new MockScalarEnlistment()) + { + this.CommandsRun = new List(); + this.StoredCredentials = new Dictionary(StringComparer.OrdinalIgnoreCase); + this.CredentialApprovals = new Dictionary>(); + this.CredentialRejections = new Dictionary>(); + } + + public List CommandsRun { get; } + public bool ShouldFail { get; set; } + public Dictionary StoredCredentials { get; } + public Dictionary> CredentialApprovals { get; } + public Dictionary> CredentialRejections { get; } + + public void SetExpectedCommandResult(string command, Func result, bool matchPrefix = false) + { + CommandInfo commandInfo = new CommandInfo(command, result, matchPrefix); + this.expectedCommandInfos.Add(commandInfo); + } + + public override bool TryStoreCredential(ITracer tracer, string repoUrl, string username, string password, out string error) + { + Credential credential = new Credential(username, password); + + // Record the approval request for this credential + List acceptedCredentials; + if (!this.CredentialApprovals.TryGetValue(repoUrl, out acceptedCredentials)) + { + acceptedCredentials = new List(); + this.CredentialApprovals[repoUrl] = acceptedCredentials; + } + + acceptedCredentials.Add(credential); + + // Store the credential + this.StoredCredentials[repoUrl] = credential; + + return base.TryStoreCredential(tracer, repoUrl, username, password, out error); + } + + public override bool TryDeleteCredential(ITracer tracer, string repoUrl, string username, string password, out string error) + { + Credential credential = new Credential(username, password); + + // Record the rejection request for this credential + List rejectedCredentials; + if (!this.CredentialRejections.TryGetValue(repoUrl, out rejectedCredentials)) + { + rejectedCredentials = new List(); + this.CredentialRejections[repoUrl] = rejectedCredentials; + } + + rejectedCredentials.Add(credential); + + // Erase the credential + this.StoredCredentials.Remove(repoUrl); + + return base.TryDeleteCredential(tracer, repoUrl, username, password, out error); + } + + protected override Result InvokeGitImpl( + string command, + string workingDirectory, + string dotGitDirectory, + bool useReadObjectHook, + Action writeStdIn, + Action parseStdOutLine, + int timeoutMs, + string gitObjectsDirectory = null) + { + this.CommandsRun.Add(command); + + if (this.ShouldFail) + { + return new Result(string.Empty, string.Empty, Result.GenericFailureCode); + } + + Predicate commandMatchFunction = + (CommandInfo commandInfo) => + { + if (commandInfo.MatchPrefix) + { + return command.StartsWith(commandInfo.Command); + } + else + { + return string.Equals(command, commandInfo.Command, StringComparison.Ordinal); + } + }; + + CommandInfo matchedCommand = this.expectedCommandInfos.Find(commandMatchFunction); + matchedCommand.ShouldNotBeNull("Unexpected command: " + command); + + return matchedCommand.Result(); + } + + public class Credential + { + public Credential(string username, string password) + { + this.Username = username; + this.Password = password; + } + + public string Username { get; } + public string Password { get; } + + public string BasicAuthString + { + get => Convert.ToBase64String(Encoding.ASCII.GetBytes(this.Username + ":" + this.Password)); + } + } + + private class CommandInfo + { + public CommandInfo(string command, Func result, bool matchPrefix) + { + this.Command = command; + this.Result = result; + this.MatchPrefix = matchPrefix; + } + + public string Command { get; private set; } + + public Func Result { get; private set; } + + public bool MatchPrefix { get; private set; } + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockGitRepo.cs b/Scalar.UnitTests/Mock/Git/MockGitRepo.cs index 46aaf0f1c1..72ebf8149a 100644 --- a/Scalar.UnitTests/Mock/Git/MockGitRepo.cs +++ b/Scalar.UnitTests/Mock/Git/MockGitRepo.cs @@ -1,118 +1,118 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockGitRepo : GitRepo - { - private Dictionary objects = new Dictionary(); - private string rootSha; - - public MockGitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem) - : base(tracer) - { - this.rootSha = Guid.NewGuid().ToString(); - this.AddTree(this.rootSha, "."); - } - - /// - /// Adds an unparented tree to the "repo" - /// - public void AddTree(string sha, string name, params string[] childShas) - { - MockGitObject newObj = new MockGitObject(sha, name, false); - newObj.ChildShas.AddRange(childShas); - this.objects.Add(sha, newObj); - } - - /// - /// Adds an unparented blob to the "repo" - /// - public void AddBlob(string sha, string name, string contents) - { - MockGitObject newObj = new MockGitObject(sha, name, true); - newObj.Content = contents; - this.objects.Add(sha, newObj); - } - - /// - /// Adds a child sha to an existing tree - /// - public void AddChildBySha(string treeSha, string childSha) - { - MockGitObject treeObj = this.GetTree(treeSha); - treeObj.ChildShas.Add(childSha); - } - - /// - /// Adds an parented blob to the "repo" - /// - public string AddChildBlob(string parentSha, string childName, string childContent) - { - string newSha = Guid.NewGuid().ToString(); - this.AddBlob(newSha, childName, childContent); - this.AddChildBySha(parentSha, newSha); - return newSha; - } - - /// - /// Adds an parented tree to the "repo" - /// - public string AddChildTree(string parentSha, string name, params string[] childShas) - { - string newSha = Guid.NewGuid().ToString(); - this.AddTree(newSha, name, childShas); - this.AddChildBySha(parentSha, newSha); - return newSha; - } - - public string GetHeadTreeSha() - { - return this.rootSha; - } - - public override bool TryGetBlobLength(string blobSha, out long size) - { - MockGitObject obj; - if (this.objects.TryGetValue(blobSha, out obj)) - { - obj.IsBlob.ShouldEqual(true); - size = obj.Content.Length; - return true; - } - - size = 0; - return false; - } - - private MockGitObject GetTree(string treeSha) - { - this.objects.ContainsKey(treeSha).ShouldEqual(true); - MockGitObject obj = this.objects[treeSha]; - obj.IsBlob.ShouldEqual(false); - return obj; - } - - private class MockGitObject - { - public MockGitObject(string sha, string name, bool isBlob) - { - this.Sha = sha; - this.Name = name; - this.IsBlob = isBlob; - this.ChildShas = new List(); - } - - public string Sha { get; private set; } - public string Name { get; set; } - public bool IsBlob { get; set; } - public List ChildShas { get; set; } - public string Content { get; set; } - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockGitRepo : GitRepo + { + private Dictionary objects = new Dictionary(); + private string rootSha; + + public MockGitRepo(ITracer tracer, Enlistment enlistment, PhysicalFileSystem fileSystem) + : base(tracer) + { + this.rootSha = Guid.NewGuid().ToString(); + this.AddTree(this.rootSha, "."); + } + + /// + /// Adds an unparented tree to the "repo" + /// + public void AddTree(string sha, string name, params string[] childShas) + { + MockGitObject newObj = new MockGitObject(sha, name, false); + newObj.ChildShas.AddRange(childShas); + this.objects.Add(sha, newObj); + } + + /// + /// Adds an unparented blob to the "repo" + /// + public void AddBlob(string sha, string name, string contents) + { + MockGitObject newObj = new MockGitObject(sha, name, true); + newObj.Content = contents; + this.objects.Add(sha, newObj); + } + + /// + /// Adds a child sha to an existing tree + /// + public void AddChildBySha(string treeSha, string childSha) + { + MockGitObject treeObj = this.GetTree(treeSha); + treeObj.ChildShas.Add(childSha); + } + + /// + /// Adds an parented blob to the "repo" + /// + public string AddChildBlob(string parentSha, string childName, string childContent) + { + string newSha = Guid.NewGuid().ToString(); + this.AddBlob(newSha, childName, childContent); + this.AddChildBySha(parentSha, newSha); + return newSha; + } + + /// + /// Adds an parented tree to the "repo" + /// + public string AddChildTree(string parentSha, string name, params string[] childShas) + { + string newSha = Guid.NewGuid().ToString(); + this.AddTree(newSha, name, childShas); + this.AddChildBySha(parentSha, newSha); + return newSha; + } + + public string GetHeadTreeSha() + { + return this.rootSha; + } + + public override bool TryGetBlobLength(string blobSha, out long size) + { + MockGitObject obj; + if (this.objects.TryGetValue(blobSha, out obj)) + { + obj.IsBlob.ShouldEqual(true); + size = obj.Content.Length; + return true; + } + + size = 0; + return false; + } + + private MockGitObject GetTree(string treeSha) + { + this.objects.ContainsKey(treeSha).ShouldEqual(true); + MockGitObject obj = this.objects[treeSha]; + obj.IsBlob.ShouldEqual(false); + return obj; + } + + private class MockGitObject + { + public MockGitObject(string sha, string name, bool isBlob) + { + this.Sha = sha; + this.Name = name; + this.IsBlob = isBlob; + this.ChildShas = new List(); + } + + public string Sha { get; private set; } + public string Name { get; set; } + public bool IsBlob { get; set; } + public List ChildShas { get; set; } + public string Content { get; set; } + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockHttpGitObjects.cs b/Scalar.UnitTests/Mock/Git/MockHttpGitObjects.cs index 9bb671672e..dc6c87eccd 100644 --- a/Scalar.UnitTests/Mock/Git/MockHttpGitObjects.cs +++ b/Scalar.UnitTests/Mock/Git/MockHttpGitObjects.cs @@ -1,107 +1,107 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockHttpGitObjects : GitObjectsHttpRequestor - { - private Dictionary shaLengths = new Dictionary(StringComparer.OrdinalIgnoreCase); - private Dictionary shaContents = new Dictionary(StringComparer.OrdinalIgnoreCase); - - public MockHttpGitObjects(ITracer tracer, Enlistment enlistment) - : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig()) - { - } - - public void AddShaLength(string sha, long length) - { - this.shaLengths.Add(sha, length); - } - - public void AddBlobContent(string sha, string content) - { - this.shaContents.Add(sha, content); - } - - public void AddShaLengths(IEnumerable> shaLengthPairs) - { - foreach (KeyValuePair kvp in shaLengthPairs) - { - this.AddShaLength(kvp.Key, kvp.Value); - } - } - - public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) - { - return objectIds.Select(oid => new GitObjectSize(oid, this.QueryForFileSize(oid))).ToList(); - } - - public override GitRefs QueryInfoRefs(string branch) - { - throw new NotImplementedException(); - } - - public override RetryWrapper.InvocationResult TryDownloadObjects( - Func> objectIdGenerator, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects); - } - - public override RetryWrapper.InvocationResult TryDownloadObjects( - IEnumerable objectIds, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure, - bool preferBatchedLooseObjects) - { - // When working within the mocks, we do not support multiple objects. - // PhysicalGitObjects should be overridden to serialize the calls. - objectIds.Count().ShouldEqual(1); - string objectId = objectIds.First(); - return this.GetSingleObject(objectId, onSuccess, onFailure); - } - - private RetryWrapper.InvocationResult GetSingleObject( - string objectId, - Func.CallbackResult> onSuccess, - Action.ErrorEventArgs> onFailure) - { - if (this.shaContents.ContainsKey(objectId)) - { - using (GitEndPointResponseData response = new GitEndPointResponseData( - HttpStatusCode.OK, - ScalarConstants.MediaTypes.LooseObjectMediaType, - new ReusableMemoryStream(this.shaContents[objectId]), - message: null, - onResponseDisposed: null)) - { - RetryWrapper.CallbackResult result = onSuccess(1, response); - return new RetryWrapper.InvocationResult(1, true, result.Result); - } - } - - if (onFailure != null) - { - onFailure(new RetryWrapper.ErrorEventArgs(new Exception("Could not find mock object: " + objectId), 1, false)); - } - - return new RetryWrapper.InvocationResult(1, new Exception("Mock failure in TryDownloadObjectsAsync")); - } - - private long QueryForFileSize(string objectId) - { - this.shaLengths.ContainsKey(objectId).ShouldEqual(true); - return this.shaLengths[objectId]; - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Threading; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockHttpGitObjects : GitObjectsHttpRequestor + { + private Dictionary shaLengths = new Dictionary(StringComparer.OrdinalIgnoreCase); + private Dictionary shaContents = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public MockHttpGitObjects(ITracer tracer, Enlistment enlistment) + : base(tracer, enlistment, new MockCacheServerInfo(), new RetryConfig()) + { + } + + public void AddShaLength(string sha, long length) + { + this.shaLengths.Add(sha, length); + } + + public void AddBlobContent(string sha, string content) + { + this.shaContents.Add(sha, content); + } + + public void AddShaLengths(IEnumerable> shaLengthPairs) + { + foreach (KeyValuePair kvp in shaLengthPairs) + { + this.AddShaLength(kvp.Key, kvp.Value); + } + } + + public override List QueryForFileSizes(IEnumerable objectIds, CancellationToken cancellationToken) + { + return objectIds.Select(oid => new GitObjectSize(oid, this.QueryForFileSize(oid))).ToList(); + } + + public override GitRefs QueryInfoRefs(string branch) + { + throw new NotImplementedException(); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + Func> objectIdGenerator, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + return this.TryDownloadObjects(objectIdGenerator(), onSuccess, onFailure, preferBatchedLooseObjects); + } + + public override RetryWrapper.InvocationResult TryDownloadObjects( + IEnumerable objectIds, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure, + bool preferBatchedLooseObjects) + { + // When working within the mocks, we do not support multiple objects. + // PhysicalGitObjects should be overridden to serialize the calls. + objectIds.Count().ShouldEqual(1); + string objectId = objectIds.First(); + return this.GetSingleObject(objectId, onSuccess, onFailure); + } + + private RetryWrapper.InvocationResult GetSingleObject( + string objectId, + Func.CallbackResult> onSuccess, + Action.ErrorEventArgs> onFailure) + { + if (this.shaContents.ContainsKey(objectId)) + { + using (GitEndPointResponseData response = new GitEndPointResponseData( + HttpStatusCode.OK, + ScalarConstants.MediaTypes.LooseObjectMediaType, + new ReusableMemoryStream(this.shaContents[objectId]), + message: null, + onResponseDisposed: null)) + { + RetryWrapper.CallbackResult result = onSuccess(1, response); + return new RetryWrapper.InvocationResult(1, true, result.Result); + } + } + + if (onFailure != null) + { + onFailure(new RetryWrapper.ErrorEventArgs(new Exception("Could not find mock object: " + objectId), 1, false)); + } + + return new RetryWrapper.InvocationResult(1, new Exception("Mock failure in TryDownloadObjectsAsync")); + } + + private long QueryForFileSize(string objectId) + { + this.shaLengths.ContainsKey(objectId).ShouldEqual(true); + return this.shaLengths[objectId]; + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockLibGit2Repo.cs b/Scalar.UnitTests/Mock/Git/MockLibGit2Repo.cs index 8a434d20fe..515ae1ffad 100644 --- a/Scalar.UnitTests/Mock/Git/MockLibGit2Repo.cs +++ b/Scalar.UnitTests/Mock/Git/MockLibGit2Repo.cs @@ -1,25 +1,25 @@ -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.IO; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockLibGit2Repo : LibGit2Repo - { - public MockLibGit2Repo(ITracer tracer) - : base() - { - } - - public override bool CommitAndRootTreeExists(string commitish) - { - return false; - } - - public override bool ObjectExists(string sha) - { - return false; - } - } -} +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.IO; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockLibGit2Repo : LibGit2Repo + { + public MockLibGit2Repo(ITracer tracer) + : base() + { + } + + public override bool CommitAndRootTreeExists(string commitish) + { + return false; + } + + public override bool ObjectExists(string sha) + { + return false; + } + } +} diff --git a/Scalar.UnitTests/Mock/Git/MockScalarGitObjects.cs b/Scalar.UnitTests/Mock/Git/MockScalarGitObjects.cs index ff255acc01..8e1bb0ad8f 100644 --- a/Scalar.UnitTests/Mock/Git/MockScalarGitObjects.cs +++ b/Scalar.UnitTests/Mock/Git/MockScalarGitObjects.cs @@ -1,60 +1,60 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Http; -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; - -namespace Scalar.UnitTests.Mock.Git -{ - public class MockScalarGitObjects : ScalarGitObjects - { - private ScalarContext context; - - public MockScalarGitObjects(ScalarContext context, GitObjectsHttpRequestor httpGitObjects) - : base(context, httpGitObjects) - { - this.context = context; - } - - public uint FileLength { get; set; } - - public override bool TryDownloadCommit(string objectSha) - { - RetryWrapper.InvocationResult result = this.GitObjectRequestor.TryDownloadObjects( - new[] { objectSha }, - onSuccess: (tryCount, response) => - { - // Add the contents to the mock repo - ((MockGitRepo)this.Context.Repository).AddBlob(objectSha, "DownloadedFile", response.RetryableReadToEnd()); - - return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); - }, - onFailure: null, - preferBatchedLooseObjects: false); - - return result.Succeeded && result.Result.Success; - } - - public override string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "") - { - return Array.Empty(); - } - - public override GitProcess.Result IndexPackFile(string packfilePath, GitProcess process) - { - return new GitProcess.Result("mocked", null, 0); - } - - public override void DeleteStaleTempPrefetchPackAndIdxs() - { - } - - public override bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, out List packIndexes) - { - packIndexes = new List(); - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Http; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace Scalar.UnitTests.Mock.Git +{ + public class MockScalarGitObjects : ScalarGitObjects + { + private ScalarContext context; + + public MockScalarGitObjects(ScalarContext context, GitObjectsHttpRequestor httpGitObjects) + : base(context, httpGitObjects) + { + this.context = context; + } + + public uint FileLength { get; set; } + + public override bool TryDownloadCommit(string objectSha) + { + RetryWrapper.InvocationResult result = this.GitObjectRequestor.TryDownloadObjects( + new[] { objectSha }, + onSuccess: (tryCount, response) => + { + // Add the contents to the mock repo + ((MockGitRepo)this.Context.Repository).AddBlob(objectSha, "DownloadedFile", response.RetryableReadToEnd()); + + return new RetryWrapper.CallbackResult(new GitObjectsHttpRequestor.GitObjectTaskResult(true)); + }, + onFailure: null, + preferBatchedLooseObjects: false); + + return result.Succeeded && result.Result.Success; + } + + public override string[] ReadPackFileNames(string packFolderPath, string prefixFilter = "") + { + return Array.Empty(); + } + + public override GitProcess.Result IndexPackFile(string packfilePath, GitProcess process) + { + return new GitProcess.Result("mocked", null, 0); + } + + public override void DeleteStaleTempPrefetchPackAndIdxs() + { + } + + public override bool TryDownloadPrefetchPacks(GitProcess gitProcess, long latestTimestamp, out List packIndexes) + { + packIndexes = new List(); + return true; + } + } +} diff --git a/Scalar.UnitTests/Mock/MockCacheServerInfo.cs b/Scalar.UnitTests/Mock/MockCacheServerInfo.cs index 6852498de5..83ae103df1 100644 --- a/Scalar.UnitTests/Mock/MockCacheServerInfo.cs +++ b/Scalar.UnitTests/Mock/MockCacheServerInfo.cs @@ -1,11 +1,11 @@ -using Scalar.Common.Http; - -namespace Scalar.UnitTests.Mock -{ - public class MockCacheServerInfo : CacheServerInfo - { - public MockCacheServerInfo() : base("https://mock", "mock") - { - } - } -} +using Scalar.Common.Http; + +namespace Scalar.UnitTests.Mock +{ + public class MockCacheServerInfo : CacheServerInfo + { + public MockCacheServerInfo() : base("https://mock", "mock") + { + } + } +} diff --git a/Scalar.UnitTests/Mock/MockGitHubUpgrader.cs b/Scalar.UnitTests/Mock/MockGitHubUpgrader.cs index ffeb2442a7..d662507b04 100644 --- a/Scalar.UnitTests/Mock/MockGitHubUpgrader.cs +++ b/Scalar.UnitTests/Mock/MockGitHubUpgrader.cs @@ -1,261 +1,261 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.UnitTests.Mock.Upgrader -{ - public class MockGitHubUpgrader : GitHubUpgrader - { - private string expectedScalarAssetName; - private string expectedGitAssetName; - private ActionType failActionTypes; - - public MockGitHubUpgrader( - string currentVersion, - ITracer tracer, - PhysicalFileSystem fileSystem, - GitHubUpgraderConfig config) : base(currentVersion, tracer, fileSystem, config) - { - this.DownloadedFiles = new List(); - this.InstallerArgs = new Dictionary>(); - } - - [Flags] - public enum ActionType - { - Invalid = 0, - FetchReleaseInfo = 0x1, - CopyTools = 0x2, - GitDownload = 0x4, - ScalarDownload = 0x8, - GitInstall = 0x10, - ScalarInstall = 0x20, - ScalarCleanup = 0x40, - GitCleanup = 0x80, - GitAuthenticodeCheck = 0x100, - ScalarAuthenticodeCheck = 0x200, - CreateDownloadDirectory = 0x400, - } - - public List DownloadedFiles { get; private set; } - public Dictionary> InstallerArgs { get; private set; } - public bool InstallerExeLaunched { get; set; } - private Release FakeUpgradeRelease { get; set; } - - public void SetDryRun(bool dryRun) - { - this.dryRun = dryRun; - } - - public void SetFailOnAction(ActionType failureType) - { - this.failActionTypes |= failureType; - } - - public void SetSucceedOnAction(ActionType failureType) - { - this.failActionTypes &= ~failureType; - } - - public void ResetFailedAction() - { - this.failActionTypes = ActionType.Invalid; - } - - public void PretendNewReleaseAvailableAtRemote(string upgradeVersion, GitHubUpgraderConfig.RingType remoteRing) - { - string assetDownloadURLPrefix = "https://github.com/Microsoft/Scalar/releases/download/v" + upgradeVersion; - Release release = new Release(); - - release.Name = "Scalar " + upgradeVersion; - release.Tag = "v" + upgradeVersion; - release.PreRelease = remoteRing == GitHubUpgraderConfig.RingType.Fast; - release.Assets = new List(); - - Random random = new Random(); - Asset scalarAsset = new Asset(); - scalarAsset.Name = "Scalar." + upgradeVersion + ScalarPlatform.Instance.Constants.InstallerExtension; - - // This is not cross-checked anywhere, random value is good. - scalarAsset.Size = random.Next(int.MaxValue / 10, int.MaxValue / 2); - scalarAsset.DownloadURL = new Uri(assetDownloadURLPrefix + "/Scalar." + upgradeVersion + ScalarPlatform.Instance.Constants.InstallerExtension); - release.Assets.Add(scalarAsset); - - Asset gitAsset = new Asset(); - gitAsset.Name = "Git-2.17.1.scalar.2.1.4.g4385455-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension; - gitAsset.Size = random.Next(int.MaxValue / 10, int.MaxValue / 2); - gitAsset.DownloadURL = new Uri(assetDownloadURLPrefix + "/Git-2.17.1.scalar.2.1.4.g4385455-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); - release.Assets.Add(gitAsset); - - this.expectedScalarAssetName = scalarAsset.Name; - this.expectedGitAssetName = gitAsset.Name; - this.FakeUpgradeRelease = release; - } - - public override bool TrySetupUpgradeApplicationDirectory(out string upgradeApplicationPath, out string error) - { - if (this.failActionTypes.HasFlag(ActionType.CopyTools)) - { - upgradeApplicationPath = null; - error = "Unable to copy upgrader tools"; - return false; - } - - upgradeApplicationPath = @"mock:\ProgramData\Scalar\Scalar.Upgrade\Tools\Scalar.Upgrader.exe"; - error = null; - return true; - } - - protected override bool TryCreateAndConfigureDownloadDirectory(ITracer tracer, out string error) - { - if (this.failActionTypes.HasFlag(ActionType.CreateDownloadDirectory)) - { - error = "Error creating download directory"; - return false; - } - - error = null; - return true; - } - - protected override bool TryDownloadAsset(Asset asset, out string errorMessage) - { - bool validAsset = true; - if (this.expectedScalarAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) - { - if (this.failActionTypes.HasFlag(ActionType.ScalarDownload)) - { - errorMessage = "Error downloading Scalar from GitHub"; - return false; - } - } - else if (this.expectedGitAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) - { - if (this.failActionTypes.HasFlag(ActionType.GitDownload)) - { - errorMessage = "Error downloading Git from GitHub"; - return false; - } - } - else - { - validAsset = false; - } - - if (validAsset) - { - string fakeDownloadDirectory = @"mock:\ProgramData\Scalar\Scalar.Upgrade\Downloads"; - asset.LocalPath = Path.Combine(fakeDownloadDirectory, asset.Name); - this.DownloadedFiles.Add(asset.LocalPath); - - errorMessage = null; - return true; - } - - errorMessage = "Cannot download unknown asset."; - return false; - } - - protected override bool TryDeleteDownloadedAsset(Asset asset, out Exception exception) - { - if (this.expectedScalarAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) - { - if (this.failActionTypes.HasFlag(ActionType.ScalarCleanup)) - { - exception = new Exception("Error deleting downloaded Scalar installer."); - return false; - } - - exception = null; - return true; - } - else if (this.expectedGitAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) - { - if (this.failActionTypes.HasFlag(ActionType.GitCleanup)) - { - exception = new Exception("Error deleting downloaded Git installer."); - return false; - } - - exception = null; - return true; - } - else - { - exception = new Exception("Unknown asset."); - return false; - } - } - - protected override bool TryFetchReleases(out List releases, out string errorMessage) - { - if (this.failActionTypes.HasFlag(ActionType.FetchReleaseInfo)) - { - releases = null; - errorMessage = "Error fetching upgrade release info."; - return false; - } - - releases = new List { this.FakeUpgradeRelease }; - errorMessage = null; - - return true; - } - - protected override void RunInstaller(string path, string args, string certCN, string issuerCN, out int exitCode, out string error) - { - string fileName = Path.GetFileName(path); - Dictionary installationInfo = new Dictionary(); - installationInfo.Add("Installer", fileName); - installationInfo.Add("Args", args); - - exitCode = 0; - error = null; - - if (fileName.Equals(this.expectedGitAssetName, StringComparison.OrdinalIgnoreCase)) - { - this.InstallerArgs.Add("Git", installationInfo); - this.InstallerExeLaunched = true; - if (this.failActionTypes.HasFlag(ActionType.GitInstall)) - { - exitCode = -1; - error = "Git installation failed"; - } - - if (this.failActionTypes.HasFlag(ActionType.GitAuthenticodeCheck)) - { - exitCode = -1; - error = "The contents of file C:\\ProgramData\\Scalar\\Scalar.Upgrade\\Tools\\Git-2.17.1.scalar.2.1.4.g4385455-64-bit might have been changed by an unauthorized user or process, because the hash of the file does not match the hash stored in the digital signature. The script cannot run on the specified system. For more information, run Get-Help about_Signing."; - } - - return; - } - - if (fileName.Equals(this.expectedScalarAssetName, StringComparison.OrdinalIgnoreCase)) - { - this.InstallerArgs.Add("Scalar", installationInfo); - this.InstallerExeLaunched = true; - if (this.failActionTypes.HasFlag(ActionType.ScalarInstall)) - { - exitCode = -1; - error = "Scalar installation failed"; - } - - if (this.failActionTypes.HasFlag(ActionType.ScalarAuthenticodeCheck)) - { - exitCode = -1; - error = "The contents of file C:\\ProgramData\\Scalar\\Scalar.Upgrade\\Tools\\SetupScalar.1.0.18297.1.exe might have been changed by an unauthorized user or process, because the hash of the file does not match the hash stored in the digital signature. The script cannot run on the specified system. For more information, run Get-Help about_Signing."; - } - - return; - } - - exitCode = -1; - error = "Cannot launch unknown installer"; - return; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.UnitTests.Mock.Upgrader +{ + public class MockGitHubUpgrader : GitHubUpgrader + { + private string expectedScalarAssetName; + private string expectedGitAssetName; + private ActionType failActionTypes; + + public MockGitHubUpgrader( + string currentVersion, + ITracer tracer, + PhysicalFileSystem fileSystem, + GitHubUpgraderConfig config) : base(currentVersion, tracer, fileSystem, config) + { + this.DownloadedFiles = new List(); + this.InstallerArgs = new Dictionary>(); + } + + [Flags] + public enum ActionType + { + Invalid = 0, + FetchReleaseInfo = 0x1, + CopyTools = 0x2, + GitDownload = 0x4, + ScalarDownload = 0x8, + GitInstall = 0x10, + ScalarInstall = 0x20, + ScalarCleanup = 0x40, + GitCleanup = 0x80, + GitAuthenticodeCheck = 0x100, + ScalarAuthenticodeCheck = 0x200, + CreateDownloadDirectory = 0x400, + } + + public List DownloadedFiles { get; private set; } + public Dictionary> InstallerArgs { get; private set; } + public bool InstallerExeLaunched { get; set; } + private Release FakeUpgradeRelease { get; set; } + + public void SetDryRun(bool dryRun) + { + this.dryRun = dryRun; + } + + public void SetFailOnAction(ActionType failureType) + { + this.failActionTypes |= failureType; + } + + public void SetSucceedOnAction(ActionType failureType) + { + this.failActionTypes &= ~failureType; + } + + public void ResetFailedAction() + { + this.failActionTypes = ActionType.Invalid; + } + + public void PretendNewReleaseAvailableAtRemote(string upgradeVersion, GitHubUpgraderConfig.RingType remoteRing) + { + string assetDownloadURLPrefix = "https://github.com/Microsoft/Scalar/releases/download/v" + upgradeVersion; + Release release = new Release(); + + release.Name = "Scalar " + upgradeVersion; + release.Tag = "v" + upgradeVersion; + release.PreRelease = remoteRing == GitHubUpgraderConfig.RingType.Fast; + release.Assets = new List(); + + Random random = new Random(); + Asset scalarAsset = new Asset(); + scalarAsset.Name = "Scalar." + upgradeVersion + ScalarPlatform.Instance.Constants.InstallerExtension; + + // This is not cross-checked anywhere, random value is good. + scalarAsset.Size = random.Next(int.MaxValue / 10, int.MaxValue / 2); + scalarAsset.DownloadURL = new Uri(assetDownloadURLPrefix + "/Scalar." + upgradeVersion + ScalarPlatform.Instance.Constants.InstallerExtension); + release.Assets.Add(scalarAsset); + + Asset gitAsset = new Asset(); + gitAsset.Name = "Git-2.17.1.scalar.2.1.4.g4385455-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension; + gitAsset.Size = random.Next(int.MaxValue / 10, int.MaxValue / 2); + gitAsset.DownloadURL = new Uri(assetDownloadURLPrefix + "/Git-2.17.1.scalar.2.1.4.g4385455-64-bit" + ScalarPlatform.Instance.Constants.InstallerExtension); + release.Assets.Add(gitAsset); + + this.expectedScalarAssetName = scalarAsset.Name; + this.expectedGitAssetName = gitAsset.Name; + this.FakeUpgradeRelease = release; + } + + public override bool TrySetupUpgradeApplicationDirectory(out string upgradeApplicationPath, out string error) + { + if (this.failActionTypes.HasFlag(ActionType.CopyTools)) + { + upgradeApplicationPath = null; + error = "Unable to copy upgrader tools"; + return false; + } + + upgradeApplicationPath = @"mock:\ProgramData\Scalar\Scalar.Upgrade\Tools\Scalar.Upgrader.exe"; + error = null; + return true; + } + + protected override bool TryCreateAndConfigureDownloadDirectory(ITracer tracer, out string error) + { + if (this.failActionTypes.HasFlag(ActionType.CreateDownloadDirectory)) + { + error = "Error creating download directory"; + return false; + } + + error = null; + return true; + } + + protected override bool TryDownloadAsset(Asset asset, out string errorMessage) + { + bool validAsset = true; + if (this.expectedScalarAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) + { + if (this.failActionTypes.HasFlag(ActionType.ScalarDownload)) + { + errorMessage = "Error downloading Scalar from GitHub"; + return false; + } + } + else if (this.expectedGitAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) + { + if (this.failActionTypes.HasFlag(ActionType.GitDownload)) + { + errorMessage = "Error downloading Git from GitHub"; + return false; + } + } + else + { + validAsset = false; + } + + if (validAsset) + { + string fakeDownloadDirectory = @"mock:\ProgramData\Scalar\Scalar.Upgrade\Downloads"; + asset.LocalPath = Path.Combine(fakeDownloadDirectory, asset.Name); + this.DownloadedFiles.Add(asset.LocalPath); + + errorMessage = null; + return true; + } + + errorMessage = "Cannot download unknown asset."; + return false; + } + + protected override bool TryDeleteDownloadedAsset(Asset asset, out Exception exception) + { + if (this.expectedScalarAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) + { + if (this.failActionTypes.HasFlag(ActionType.ScalarCleanup)) + { + exception = new Exception("Error deleting downloaded Scalar installer."); + return false; + } + + exception = null; + return true; + } + else if (this.expectedGitAssetName.Equals(asset.Name, StringComparison.OrdinalIgnoreCase)) + { + if (this.failActionTypes.HasFlag(ActionType.GitCleanup)) + { + exception = new Exception("Error deleting downloaded Git installer."); + return false; + } + + exception = null; + return true; + } + else + { + exception = new Exception("Unknown asset."); + return false; + } + } + + protected override bool TryFetchReleases(out List releases, out string errorMessage) + { + if (this.failActionTypes.HasFlag(ActionType.FetchReleaseInfo)) + { + releases = null; + errorMessage = "Error fetching upgrade release info."; + return false; + } + + releases = new List { this.FakeUpgradeRelease }; + errorMessage = null; + + return true; + } + + protected override void RunInstaller(string path, string args, string certCN, string issuerCN, out int exitCode, out string error) + { + string fileName = Path.GetFileName(path); + Dictionary installationInfo = new Dictionary(); + installationInfo.Add("Installer", fileName); + installationInfo.Add("Args", args); + + exitCode = 0; + error = null; + + if (fileName.Equals(this.expectedGitAssetName, StringComparison.OrdinalIgnoreCase)) + { + this.InstallerArgs.Add("Git", installationInfo); + this.InstallerExeLaunched = true; + if (this.failActionTypes.HasFlag(ActionType.GitInstall)) + { + exitCode = -1; + error = "Git installation failed"; + } + + if (this.failActionTypes.HasFlag(ActionType.GitAuthenticodeCheck)) + { + exitCode = -1; + error = "The contents of file C:\\ProgramData\\Scalar\\Scalar.Upgrade\\Tools\\Git-2.17.1.scalar.2.1.4.g4385455-64-bit might have been changed by an unauthorized user or process, because the hash of the file does not match the hash stored in the digital signature. The script cannot run on the specified system. For more information, run Get-Help about_Signing."; + } + + return; + } + + if (fileName.Equals(this.expectedScalarAssetName, StringComparison.OrdinalIgnoreCase)) + { + this.InstallerArgs.Add("Scalar", installationInfo); + this.InstallerExeLaunched = true; + if (this.failActionTypes.HasFlag(ActionType.ScalarInstall)) + { + exitCode = -1; + error = "Scalar installation failed"; + } + + if (this.failActionTypes.HasFlag(ActionType.ScalarAuthenticodeCheck)) + { + exitCode = -1; + error = "The contents of file C:\\ProgramData\\Scalar\\Scalar.Upgrade\\Tools\\SetupScalar.1.0.18297.1.exe might have been changed by an unauthorized user or process, because the hash of the file does not match the hash stored in the digital signature. The script cannot run on the specified system. For more information, run Get-Help about_Signing."; + } + + return; + } + + exitCode = -1; + error = "Cannot launch unknown installer"; + return; + } + } +} diff --git a/Scalar.UnitTests/Mock/MockInstallerPreRunChecker.cs b/Scalar.UnitTests/Mock/MockInstallerPreRunChecker.cs index 4405d496b4..6b6daacd4f 100644 --- a/Scalar.UnitTests/Mock/MockInstallerPreRunChecker.cs +++ b/Scalar.UnitTests/Mock/MockInstallerPreRunChecker.cs @@ -1,113 +1,113 @@ -using Scalar.Common.Tracing; -using Scalar.Upgrader; -using System; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Mock.Upgrader -{ - public class MockInstallerPrerunChecker : InstallerPreRunChecker - { - public const string GitUpgradeCheckError = "Unable to upgrade Git"; - - private FailOnCheckType failOnCheck; - - public MockInstallerPrerunChecker(ITracer tracer) : base(tracer, string.Empty) - { - } - - [Flags] - public enum FailOnCheckType - { - Invalid = 0, - IsElevated = 0x2, - BlockingProcessesRunning = 0x4, - UnattendedMode = 0x8, - UnMountRepos = 0x10, - RemountRepos = 0x20, - IsServiceInstalledAndNotRunning = 0x40, - } - - public void SetReturnFalseOnCheck(FailOnCheckType prerunCheck) - { - this.failOnCheck |= prerunCheck; - } - - public void SetReturnTrueOnCheck(FailOnCheckType prerunCheck) - { - this.failOnCheck &= ~prerunCheck; - } - - public void Reset() - { - this.failOnCheck = FailOnCheckType.Invalid; - - this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnattendedMode); - this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.BlockingProcessesRunning); - this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsServiceInstalledAndNotRunning); - } - - public void SetCommandToRerun(string command) - { - this.CommandToRerun = command; - } - - protected override bool IsServiceInstalledAndNotRunning() - { - return this.FakedResultOfCheck(FailOnCheckType.IsServiceInstalledAndNotRunning); - } - - protected override bool IsElevated() - { - return this.FakedResultOfCheck(FailOnCheckType.IsElevated); - } - - protected override bool IsScalarUpgradeSupported() - { - return true; - } - - protected override bool IsUnattended() - { - return this.FakedResultOfCheck(FailOnCheckType.UnattendedMode); - } - - protected override bool IsBlockingProcessRunning(out HashSet processes) - { - processes = new HashSet(); - - bool isRunning = this.FakedResultOfCheck(FailOnCheckType.BlockingProcessesRunning); - if (isRunning) - { - processes.Add("Scalar.Mount"); - processes.Add("git"); - } - - return isRunning; - } - - protected override bool TryRunScalarWithArgs(string args, out string error) - { - if (string.CompareOrdinal(args, "service --unmount-all") == 0) - { - bool result = this.FakedResultOfCheck(FailOnCheckType.UnMountRepos); - error = result == false ? "Unmount of some of the repositories failed." : null; - return result; - } - - if (string.CompareOrdinal(args, "service --mount-all") == 0) - { - bool result = this.FakedResultOfCheck(FailOnCheckType.RemountRepos); - error = result == false ? "Auto remount failed." : null; - return result; - } - - error = "Unknown Scalar command"; - return false; - } - - private bool FakedResultOfCheck(FailOnCheckType checkType) - { - return !this.failOnCheck.HasFlag(checkType); - } - } -} +using Scalar.Common.Tracing; +using Scalar.Upgrader; +using System; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Mock.Upgrader +{ + public class MockInstallerPrerunChecker : InstallerPreRunChecker + { + public const string GitUpgradeCheckError = "Unable to upgrade Git"; + + private FailOnCheckType failOnCheck; + + public MockInstallerPrerunChecker(ITracer tracer) : base(tracer, string.Empty) + { + } + + [Flags] + public enum FailOnCheckType + { + Invalid = 0, + IsElevated = 0x2, + BlockingProcessesRunning = 0x4, + UnattendedMode = 0x8, + UnMountRepos = 0x10, + RemountRepos = 0x20, + IsServiceInstalledAndNotRunning = 0x40, + } + + public void SetReturnFalseOnCheck(FailOnCheckType prerunCheck) + { + this.failOnCheck |= prerunCheck; + } + + public void SetReturnTrueOnCheck(FailOnCheckType prerunCheck) + { + this.failOnCheck &= ~prerunCheck; + } + + public void Reset() + { + this.failOnCheck = FailOnCheckType.Invalid; + + this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnattendedMode); + this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.BlockingProcessesRunning); + this.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.IsServiceInstalledAndNotRunning); + } + + public void SetCommandToRerun(string command) + { + this.CommandToRerun = command; + } + + protected override bool IsServiceInstalledAndNotRunning() + { + return this.FakedResultOfCheck(FailOnCheckType.IsServiceInstalledAndNotRunning); + } + + protected override bool IsElevated() + { + return this.FakedResultOfCheck(FailOnCheckType.IsElevated); + } + + protected override bool IsScalarUpgradeSupported() + { + return true; + } + + protected override bool IsUnattended() + { + return this.FakedResultOfCheck(FailOnCheckType.UnattendedMode); + } + + protected override bool IsBlockingProcessRunning(out HashSet processes) + { + processes = new HashSet(); + + bool isRunning = this.FakedResultOfCheck(FailOnCheckType.BlockingProcessesRunning); + if (isRunning) + { + processes.Add("Scalar.Mount"); + processes.Add("git"); + } + + return isRunning; + } + + protected override bool TryRunScalarWithArgs(string args, out string error) + { + if (string.CompareOrdinal(args, "service --unmount-all") == 0) + { + bool result = this.FakedResultOfCheck(FailOnCheckType.UnMountRepos); + error = result == false ? "Unmount of some of the repositories failed." : null; + return result; + } + + if (string.CompareOrdinal(args, "service --mount-all") == 0) + { + bool result = this.FakedResultOfCheck(FailOnCheckType.RemountRepos); + error = result == false ? "Auto remount failed." : null; + return result; + } + + error = "Unknown Scalar command"; + return false; + } + + private bool FakedResultOfCheck(FailOnCheckType checkType) + { + return !this.failOnCheck.HasFlag(checkType); + } + } +} diff --git a/Scalar.UnitTests/Mock/MockTextWriter.cs b/Scalar.UnitTests/Mock/MockTextWriter.cs index 926c004b08..5b923e90f2 100644 --- a/Scalar.UnitTests/Mock/MockTextWriter.cs +++ b/Scalar.UnitTests/Mock/MockTextWriter.cs @@ -1,47 +1,47 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text; - -namespace Scalar.UnitTests.Mock.Upgrader -{ - public class MockTextWriter : TextWriter - { - private StringBuilder stringBuilder; - - public MockTextWriter() : base() - { - this.AllLines = new List(); - this.stringBuilder = new StringBuilder(); - } - - public List AllLines { get; private set; } - - public override Encoding Encoding - { - get { return Encoding.Default; } - } - - public override void Write(char value) - { - if (value.Equals('\r')) - { - return; - } - - if (value.Equals('\n')) - { - this.AllLines.Add(this.stringBuilder.ToString()); - this.stringBuilder.Clear(); - return; - } - - this.stringBuilder.Append(value); - } - - public bool ContainsLine(string line) - { - return this.AllLines.Exists(x => x.Equals(line, StringComparison.Ordinal)); - } - } -} +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; + +namespace Scalar.UnitTests.Mock.Upgrader +{ + public class MockTextWriter : TextWriter + { + private StringBuilder stringBuilder; + + public MockTextWriter() : base() + { + this.AllLines = new List(); + this.stringBuilder = new StringBuilder(); + } + + public List AllLines { get; private set; } + + public override Encoding Encoding + { + get { return Encoding.Default; } + } + + public override void Write(char value) + { + if (value.Equals('\r')) + { + return; + } + + if (value.Equals('\n')) + { + this.AllLines.Add(this.stringBuilder.ToString()); + this.stringBuilder.Clear(); + return; + } + + this.stringBuilder.Append(value); + } + + public bool ContainsLine(string line) + { + return this.AllLines.Exists(x => x.Equals(line, StringComparison.Ordinal)); + } + } +} diff --git a/Scalar.UnitTests/Mock/ReusableMemoryStream.cs b/Scalar.UnitTests/Mock/ReusableMemoryStream.cs index 09046a719d..c63f5ddf5d 100644 --- a/Scalar.UnitTests/Mock/ReusableMemoryStream.cs +++ b/Scalar.UnitTests/Mock/ReusableMemoryStream.cs @@ -1,159 +1,159 @@ -using System; -using System.IO; -using System.Text; - -namespace Scalar.UnitTests.Mock -{ - public class ReusableMemoryStream : Stream - { - private byte[] contents; - private long length; - private long position; - - public ReusableMemoryStream(string initialContents) - { - this.contents = Encoding.UTF8.GetBytes(initialContents); - this.length = this.contents.Length; - } - - public ReusableMemoryStream(byte[] initialContents) - { - this.contents = initialContents; - this.length = initialContents.Length; - } - - public bool TruncateWrites { get; set; } - - public override bool CanRead - { - get { return true; } - } - - public override bool CanSeek - { - get { return true; } - } - - public override bool CanWrite - { - get { return true; } - } - - public override long Length - { - get { return this.length; } - } - - public override long Position - { - get { return this.position; } - set { this.position = value; } - } - - public override void Flush() - { - // noop - } - - public string ReadAsString() - { - return Encoding.UTF8.GetString(this.contents, 0, (int)this.length); - } - - public string ReadAt(long position, long length) - { - long lastPosition = this.Position; - - this.Position = position; - - byte[] bytes = new byte[length]; - this.Read(bytes, 0, (int)length); - - this.Position = lastPosition; - - return Encoding.UTF8.GetString(bytes); - } - - public override int Read(byte[] buffer, int offset, int count) - { - int actualCount = Math.Min((int)(this.length - this.position), count); - Array.Copy(this.contents, this.Position, buffer, offset, actualCount); - this.Position += actualCount; - - return actualCount; - } - - public override long Seek(long offset, SeekOrigin origin) - { - if (origin == SeekOrigin.Begin) - { - this.position = offset; - } - else if (origin == SeekOrigin.End) - { - this.position = this.length - offset; - } - else - { - this.position += offset; - } - - if (this.position > this.length) - { - this.position = this.length - 1; - } - - return this.position; - } - - public override void SetLength(long value) - { - while (value > this.contents.Length) - { - if (this.contents.Length == 0) - { - this.contents = new byte[1024]; - } - else - { - Array.Resize(ref this.contents, this.contents.Length * 2); - } - } - - this.length = value; - } - - public override void Write(byte[] buffer, int offset, int count) - { - if (this.position + count > this.contents.Length) - { - this.SetLength(this.position + count); - } - - if (this.TruncateWrites) - { - count /= 2; - } - - Array.Copy(buffer, offset, this.contents, this.position, count); - this.position += count; - if (this.position > this.length) - { - this.length = this.position; - } - - if (this.TruncateWrites) - { - throw new IOException("Could not complete write"); - } - } - - protected override void Dispose(bool disposing) - { - // This method is a noop besides resetting the position. - // The byte[] in this class is the source of truth for the contents that this - // stream is providing, so we can't dispose it here. - this.position = 0; - } - } -} +using System; +using System.IO; +using System.Text; + +namespace Scalar.UnitTests.Mock +{ + public class ReusableMemoryStream : Stream + { + private byte[] contents; + private long length; + private long position; + + public ReusableMemoryStream(string initialContents) + { + this.contents = Encoding.UTF8.GetBytes(initialContents); + this.length = this.contents.Length; + } + + public ReusableMemoryStream(byte[] initialContents) + { + this.contents = initialContents; + this.length = initialContents.Length; + } + + public bool TruncateWrites { get; set; } + + public override bool CanRead + { + get { return true; } + } + + public override bool CanSeek + { + get { return true; } + } + + public override bool CanWrite + { + get { return true; } + } + + public override long Length + { + get { return this.length; } + } + + public override long Position + { + get { return this.position; } + set { this.position = value; } + } + + public override void Flush() + { + // noop + } + + public string ReadAsString() + { + return Encoding.UTF8.GetString(this.contents, 0, (int)this.length); + } + + public string ReadAt(long position, long length) + { + long lastPosition = this.Position; + + this.Position = position; + + byte[] bytes = new byte[length]; + this.Read(bytes, 0, (int)length); + + this.Position = lastPosition; + + return Encoding.UTF8.GetString(bytes); + } + + public override int Read(byte[] buffer, int offset, int count) + { + int actualCount = Math.Min((int)(this.length - this.position), count); + Array.Copy(this.contents, this.Position, buffer, offset, actualCount); + this.Position += actualCount; + + return actualCount; + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + { + this.position = offset; + } + else if (origin == SeekOrigin.End) + { + this.position = this.length - offset; + } + else + { + this.position += offset; + } + + if (this.position > this.length) + { + this.position = this.length - 1; + } + + return this.position; + } + + public override void SetLength(long value) + { + while (value > this.contents.Length) + { + if (this.contents.Length == 0) + { + this.contents = new byte[1024]; + } + else + { + Array.Resize(ref this.contents, this.contents.Length * 2); + } + } + + this.length = value; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (this.position + count > this.contents.Length) + { + this.SetLength(this.position + count); + } + + if (this.TruncateWrites) + { + count /= 2; + } + + Array.Copy(buffer, offset, this.contents, this.position, count); + this.position += count; + if (this.position > this.length) + { + this.length = this.position; + } + + if (this.TruncateWrites) + { + throw new IOException("Could not complete write"); + } + } + + protected override void Dispose(bool disposing) + { + // This method is a noop besides resetting the position. + // The byte[] in this class is the source of truth for the contents that this + // stream is providing, so we can't dispose it here. + this.position = 0; + } + } +} diff --git a/Scalar.UnitTests/Platform.Mac/MacDaemonControllerTests.cs b/Scalar.UnitTests/Platform.Mac/MacDaemonControllerTests.cs index 78157d53d0..65a49a5ddd 100644 --- a/Scalar.UnitTests/Platform.Mac/MacDaemonControllerTests.cs +++ b/Scalar.UnitTests/Platform.Mac/MacDaemonControllerTests.cs @@ -1,39 +1,39 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Platform.Mac; -using Scalar.Tests.Should; -using System.Collections.Generic; -using System.Text; - -namespace Scalar.UnitTests.Platform.Mac -{ - [TestFixture] - public class MacServiceProcessTests - { - [TestCase] - public void CanGetServices() - { - Mock processHelperMock = new Mock(MockBehavior.Strict); - - StringBuilder sb = new StringBuilder(); - sb.AppendLine("PID\tStatus\tLabel"); - sb.AppendLine("1\t0\tcom.apple.process1"); - sb.AppendLine("2\t0\tcom.apple.process2"); - sb.AppendLine("3\t0\tcom.apple.process3"); - sb.AppendLine("-\t0\tcom.apple.process4"); - - ProcessResult processResult = new ProcessResult(sb.ToString(), string.Empty, 0); - - processHelperMock.Setup(m => m.Run("/bin/launchctl", "asuser 521 /bin/launchctl list", true)).Returns(processResult); - - MacDaemonController daemonController = new MacDaemonController(processHelperMock.Object); - bool success = daemonController.TryGetDaemons("521", out List daemons, out string error); - - success.ShouldBeTrue(); - daemons.ShouldNotBeNull(); - daemons.Count.ShouldEqual(4); - processHelperMock.VerifyAll(); - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Platform.Mac; +using Scalar.Tests.Should; +using System.Collections.Generic; +using System.Text; + +namespace Scalar.UnitTests.Platform.Mac +{ + [TestFixture] + public class MacServiceProcessTests + { + [TestCase] + public void CanGetServices() + { + Mock processHelperMock = new Mock(MockBehavior.Strict); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("PID\tStatus\tLabel"); + sb.AppendLine("1\t0\tcom.apple.process1"); + sb.AppendLine("2\t0\tcom.apple.process2"); + sb.AppendLine("3\t0\tcom.apple.process3"); + sb.AppendLine("-\t0\tcom.apple.process4"); + + ProcessResult processResult = new ProcessResult(sb.ToString(), string.Empty, 0); + + processHelperMock.Setup(m => m.Run("/bin/launchctl", "asuser 521 /bin/launchctl list", true)).Returns(processResult); + + MacDaemonController daemonController = new MacDaemonController(processHelperMock.Object); + bool success = daemonController.TryGetDaemons("521", out List daemons, out string error); + + success.ShouldBeTrue(); + daemons.ShouldNotBeNull(); + daemons.Count.ShouldEqual(4); + processHelperMock.VerifyAll(); + } + } +} diff --git a/Scalar.UnitTests/Prefetch/BatchObjectDownloadStageTests.cs b/Scalar.UnitTests/Prefetch/BatchObjectDownloadStageTests.cs index 83bc88e73d..0c4e191bef 100644 --- a/Scalar.UnitTests/Prefetch/BatchObjectDownloadStageTests.cs +++ b/Scalar.UnitTests/Prefetch/BatchObjectDownloadStageTests.cs @@ -1,77 +1,77 @@ -using NUnit.Framework; -using Scalar.Common.Prefetch.Pipeline; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; -using System; -using System.Collections.Concurrent; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixture] - public class BatchObjectDownloadStageTests - { - private const int MaxParallel = 1; - private const int ChunkSize = 2; - - // This test confirms that if two objects are downloaded at the same time and the second - // object's download fails, the first object should not be downloaded again - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void OnlyRequestsObjectsNotDownloaded() - { - string obj1Sha = new string('1', 40); - string obj2Sha = new string('2', 40); - - BlockingCollection input = new BlockingCollection(); - input.Add(obj1Sha); - input.Add(obj2Sha); - input.CompleteAdding(); - - int obj1Count = 0; - int obj2Count = 0; - - Func objectResolver = (oid) => - { - if (oid.Equals(obj1Sha)) - { - obj1Count++; - return "Object1Contents"; - } - - if (oid.Equals(obj2Sha) && obj2Count++ == 1) - { - return "Object2Contents"; - } - - return null; - }; - - BlockingCollection output = new BlockingCollection(); - MockTracer tracer = new MockTracer(); - MockScalarEnlistment enlistment = new MockScalarEnlistment(); - MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver); - - BatchObjectDownloadStage dut = new BatchObjectDownloadStage( - MaxParallel, - ChunkSize, - input, - output, - tracer, - enlistment, - httpObjects, - new MockPhysicalGitObjects(tracer, null, enlistment, httpObjects)); - - dut.Start(); - dut.WaitForCompletion(); - - input.Count.ShouldEqual(0); - output.Count.ShouldEqual(2); - output.Take().ShouldEqual(obj1Sha); - output.Take().ShouldEqual(obj2Sha); - obj1Count.ShouldEqual(1); - obj2Count.ShouldEqual(2); - } - } +using NUnit.Framework; +using Scalar.Common.Prefetch.Pipeline; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; +using System; +using System.Collections.Concurrent; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixture] + public class BatchObjectDownloadStageTests + { + private const int MaxParallel = 1; + private const int ChunkSize = 2; + + // This test confirms that if two objects are downloaded at the same time and the second + // object's download fails, the first object should not be downloaded again + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void OnlyRequestsObjectsNotDownloaded() + { + string obj1Sha = new string('1', 40); + string obj2Sha = new string('2', 40); + + BlockingCollection input = new BlockingCollection(); + input.Add(obj1Sha); + input.Add(obj2Sha); + input.CompleteAdding(); + + int obj1Count = 0; + int obj2Count = 0; + + Func objectResolver = (oid) => + { + if (oid.Equals(obj1Sha)) + { + obj1Count++; + return "Object1Contents"; + } + + if (oid.Equals(obj2Sha) && obj2Count++ == 1) + { + return "Object2Contents"; + } + + return null; + }; + + BlockingCollection output = new BlockingCollection(); + MockTracer tracer = new MockTracer(); + MockScalarEnlistment enlistment = new MockScalarEnlistment(); + MockBatchHttpGitObjects httpObjects = new MockBatchHttpGitObjects(tracer, enlistment, objectResolver); + + BatchObjectDownloadStage dut = new BatchObjectDownloadStage( + MaxParallel, + ChunkSize, + input, + output, + tracer, + enlistment, + httpObjects, + new MockPhysicalGitObjects(tracer, null, enlistment, httpObjects)); + + dut.Start(); + dut.WaitForCompletion(); + + input.Count.ShouldEqual(0); + output.Count.ShouldEqual(2); + output.Take().ShouldEqual(obj1Sha); + output.Take().ShouldEqual(obj2Sha); + obj1Count.ShouldEqual(1); + obj2Count.ShouldEqual(2); + } + } } diff --git a/Scalar.UnitTests/Prefetch/BlobPrefetcherTests.cs b/Scalar.UnitTests/Prefetch/BlobPrefetcherTests.cs index 2170dc4828..99d16ab33a 100644 --- a/Scalar.UnitTests/Prefetch/BlobPrefetcherTests.cs +++ b/Scalar.UnitTests/Prefetch/BlobPrefetcherTests.cs @@ -1,33 +1,33 @@ -using NUnit.Framework; -using Scalar.Common.Prefetch; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.FileSystem; -using System.IO; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixture] - public class BlobPrefetcherTests - { - [TestCase] - public void AppendToNewlineSeparatedFileTests() - { - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.Combine("mock:", "Scalar", "UnitTests", "Repo"), null, null)); - - // Validate can write to a file that doesn't exist. - string testFileName = Path.Combine("mock:", "Scalar", "UnitTests", "Repo", "appendTests"); - BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected content line 1"); - fileSystem.ReadAllText(testFileName).ShouldEqual("expected content line 1\n"); - - // Validate that if the file doesn't end in a newline it gets a newline added. - fileSystem.WriteAllText(testFileName, "existing content"); - BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2"); - fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n"); - - // Validate that if the file ends in a newline, we don't end up with two newlines - fileSystem.WriteAllText(testFileName, "existing content\n"); - BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2"); - fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n"); - } - } +using NUnit.Framework; +using Scalar.Common.Prefetch; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.FileSystem; +using System.IO; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixture] + public class BlobPrefetcherTests + { + [TestCase] + public void AppendToNewlineSeparatedFileTests() + { + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(Path.Combine("mock:", "Scalar", "UnitTests", "Repo"), null, null)); + + // Validate can write to a file that doesn't exist. + string testFileName = Path.Combine("mock:", "Scalar", "UnitTests", "Repo", "appendTests"); + BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected content line 1"); + fileSystem.ReadAllText(testFileName).ShouldEqual("expected content line 1\n"); + + // Validate that if the file doesn't end in a newline it gets a newline added. + fileSystem.WriteAllText(testFileName, "existing content"); + BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2"); + fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n"); + + // Validate that if the file ends in a newline, we don't end up with two newlines + fileSystem.WriteAllText(testFileName, "existing content\n"); + BlobPrefetcher.AppendToNewlineSeparatedFile(fileSystem, testFileName, "expected line 2"); + fileSystem.ReadAllText(testFileName).ShouldEqual("existing content\nexpected line 2\n"); + } + } } diff --git a/Scalar.UnitTests/Prefetch/DiffHelperTests.cs b/Scalar.UnitTests/Prefetch/DiffHelperTests.cs index ca04dce454..e8b24c5b1a 100644 --- a/Scalar.UnitTests/Prefetch/DiffHelperTests.cs +++ b/Scalar.UnitTests/Prefetch/DiffHelperTests.cs @@ -1,26 +1,26 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Common.Prefetch.Git; -using Scalar.Tests; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))] - public class DiffHelperTests +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Common.Prefetch.Git; +using Scalar.Tests; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixtureSource(typeof(DataSources), nameof(DataSources.AllBools))] + public class DiffHelperTests { - public DiffHelperTests(bool symLinkSupport) - { - this.IncludeSymLinks = symLinkSupport; - } + public DiffHelperTests(bool symLinkSupport) + { + this.IncludeSymLinks = symLinkSupport; + } - public bool IncludeSymLinks { get; set; } + public bool IncludeSymLinks { get; set; } // Make two commits. The first should look like this: // recursiveDelete @@ -49,103 +49,103 @@ public DiffHelperTests(bool symLinkSupport) // Then to generate the diffs, run: // git diff-tree -r -t Head~1 Head > forward.txt // git diff-tree -r -t Head Head ~1 > backward.txt - [TestCase] - public void CanParseDiffForwards() - { - MockTracer tracer = new MockTracer(); - DiffHelper diffForwards = new DiffHelper(tracer, new MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); - diffForwards.ParseDiffFile(GetDataPath("forward.txt")); - - // File added, file edited, file renamed, folder => file, edit-rename file, SymLink added (if applicable) - // Children of: Add folder, Renamed folder, edited folder, file => folder - diffForwards.RequiredBlobs.Count.ShouldEqual(diffForwards.ShouldIncludeSymLinks ? 10 : 9); - - diffForwards.FileAddOperations.ContainsKey("3bd509d373734a9f9685d6a73ba73324f72931e3").ShouldEqual(diffForwards.ShouldIncludeSymLinks); - - // File deleted, folder deleted, file > folder, edit-rename - diffForwards.FileDeleteOperations.Count.ShouldEqual(4); - - // Includes children of: Recursive delete folder, deleted folder, renamed folder, and folder => file - diffForwards.TotalFileDeletes.ShouldEqual(8); - - // Folder created, folder edited, folder deleted, folder renamed (add + delete), - // folder => file, file => folder, recursive delete (top-level only) - diffForwards.DirectoryOperations.Count.ShouldEqual(8); - - // Should also include the deleted folder of recursive delete - diffForwards.TotalDirectoryOperations.ShouldEqual(9); - } - - // Parses Diff B => A - [TestCase] - public void CanParseBackwardsDiff() - { - MockTracer tracer = new MockTracer(); - DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); - diffBackwards.ParseDiffFile(GetDataPath("backward.txt")); - - // File > folder, deleted file, edited file, renamed file, rename-edit file - // Children of file > folder, renamed folder, deleted folder, recursive delete file, edited folder - diffBackwards.RequiredBlobs.Count.ShouldEqual(10); - - // File added, folder > file, moved folder, added folder - diffBackwards.FileDeleteOperations.Count.ShouldEqual(4); - - // Also includes, the children of: Folder added, folder renamed, file => folder - diffBackwards.TotalFileDeletes.ShouldEqual(7); - - // Folder created, folder edited, folder deleted, folder renamed (add + delete), - // folder => file, file => folder, recursive delete (include subfolder) - diffBackwards.TotalDirectoryOperations.ShouldEqual(9); - } - - // Delete a folder with two sub folders each with a single file - // Readd it with a different casing and same contents - [TestCase] - public void ParsesCaseChangesAsAdds() - { - MockTracer tracer = new MockTracer(); - DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); - diffBackwards.ParseDiffFile(GetDataPath("caseChange.txt")); - - diffBackwards.RequiredBlobs.Count.ShouldEqual(2); - diffBackwards.FileAddOperations.Sum(list => list.Value.Count).ShouldEqual(2); - - diffBackwards.FileDeleteOperations.Count.ShouldEqual(0); - diffBackwards.TotalFileDeletes.ShouldEqual(0); - - diffBackwards.DirectoryOperations.ShouldNotContain(entry => entry.Operation == DiffTreeResult.Operations.Delete); - diffBackwards.TotalDirectoryOperations.ShouldEqual(3); - } - - [TestCase] - public void DetectsFailuresInDiffTree() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("diff-tree -r -t sha1 sha2", () => new GitProcess.Result(string.Empty, string.Empty, 1)); - - DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks); - diffBackwards.PerformDiff("sha1", "sha2"); - diffBackwards.HasFailures.ShouldEqual(true); - } - - [TestCase] - public void DetectsFailuresInLsTree() - { - MockTracer tracer = new MockTracer(); - MockGitProcess gitProcess = new MockGitProcess(); - gitProcess.SetExpectedCommandResult("ls-tree -r -t sha1", () => new GitProcess.Result(string.Empty, string.Empty, 1)); - - DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks); - diffBackwards.PerformDiff(null, "sha1"); - diffBackwards.HasFailures.ShouldEqual(true); - } - - private static string GetDataPath(string fileName) - { - string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); - return Path.Combine(workingDirectory, "Data", fileName); - } - } + [TestCase] + public void CanParseDiffForwards() + { + MockTracer tracer = new MockTracer(); + DiffHelper diffForwards = new DiffHelper(tracer, new MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); + diffForwards.ParseDiffFile(GetDataPath("forward.txt")); + + // File added, file edited, file renamed, folder => file, edit-rename file, SymLink added (if applicable) + // Children of: Add folder, Renamed folder, edited folder, file => folder + diffForwards.RequiredBlobs.Count.ShouldEqual(diffForwards.ShouldIncludeSymLinks ? 10 : 9); + + diffForwards.FileAddOperations.ContainsKey("3bd509d373734a9f9685d6a73ba73324f72931e3").ShouldEqual(diffForwards.ShouldIncludeSymLinks); + + // File deleted, folder deleted, file > folder, edit-rename + diffForwards.FileDeleteOperations.Count.ShouldEqual(4); + + // Includes children of: Recursive delete folder, deleted folder, renamed folder, and folder => file + diffForwards.TotalFileDeletes.ShouldEqual(8); + + // Folder created, folder edited, folder deleted, folder renamed (add + delete), + // folder => file, file => folder, recursive delete (top-level only) + diffForwards.DirectoryOperations.Count.ShouldEqual(8); + + // Should also include the deleted folder of recursive delete + diffForwards.TotalDirectoryOperations.ShouldEqual(9); + } + + // Parses Diff B => A + [TestCase] + public void CanParseBackwardsDiff() + { + MockTracer tracer = new MockTracer(); + DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); + diffBackwards.ParseDiffFile(GetDataPath("backward.txt")); + + // File > folder, deleted file, edited file, renamed file, rename-edit file + // Children of file > folder, renamed folder, deleted folder, recursive delete file, edited folder + diffBackwards.RequiredBlobs.Count.ShouldEqual(10); + + // File added, folder > file, moved folder, added folder + diffBackwards.FileDeleteOperations.Count.ShouldEqual(4); + + // Also includes, the children of: Folder added, folder renamed, file => folder + diffBackwards.TotalFileDeletes.ShouldEqual(7); + + // Folder created, folder edited, folder deleted, folder renamed (add + delete), + // folder => file, file => folder, recursive delete (include subfolder) + diffBackwards.TotalDirectoryOperations.ShouldEqual(9); + } + + // Delete a folder with two sub folders each with a single file + // Readd it with a different casing and same contents + [TestCase] + public void ParsesCaseChangesAsAdds() + { + MockTracer tracer = new MockTracer(); + DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), new List(), new List(), includeSymLinks: this.IncludeSymLinks); + diffBackwards.ParseDiffFile(GetDataPath("caseChange.txt")); + + diffBackwards.RequiredBlobs.Count.ShouldEqual(2); + diffBackwards.FileAddOperations.Sum(list => list.Value.Count).ShouldEqual(2); + + diffBackwards.FileDeleteOperations.Count.ShouldEqual(0); + diffBackwards.TotalFileDeletes.ShouldEqual(0); + + diffBackwards.DirectoryOperations.ShouldNotContain(entry => entry.Operation == DiffTreeResult.Operations.Delete); + diffBackwards.TotalDirectoryOperations.ShouldEqual(3); + } + + [TestCase] + public void DetectsFailuresInDiffTree() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("diff-tree -r -t sha1 sha2", () => new GitProcess.Result(string.Empty, string.Empty, 1)); + + DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks); + diffBackwards.PerformDiff("sha1", "sha2"); + diffBackwards.HasFailures.ShouldEqual(true); + } + + [TestCase] + public void DetectsFailuresInLsTree() + { + MockTracer tracer = new MockTracer(); + MockGitProcess gitProcess = new MockGitProcess(); + gitProcess.SetExpectedCommandResult("ls-tree -r -t sha1", () => new GitProcess.Result(string.Empty, string.Empty, 1)); + + DiffHelper diffBackwards = new DiffHelper(tracer, new Mock.Common.MockScalarEnlistment(), gitProcess, new List(), new List(), includeSymLinks: this.IncludeSymLinks); + diffBackwards.PerformDiff(null, "sha1"); + diffBackwards.HasFailures.ShouldEqual(true); + } + + private static string GetDataPath(string fileName) + { + string workingDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + return Path.Combine(workingDirectory, "Data", fileName); + } + } } diff --git a/Scalar.UnitTests/Prefetch/DiffTreeResultTests.cs b/Scalar.UnitTests/Prefetch/DiffTreeResultTests.cs index 96100a38ec..d1e4760843 100644 --- a/Scalar.UnitTests/Prefetch/DiffTreeResultTests.cs +++ b/Scalar.UnitTests/Prefetch/DiffTreeResultTests.cs @@ -1,371 +1,371 @@ -using NUnit.Framework; -using Scalar.Common.Git; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using System; -using System.IO; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixture] - public class DiffTreeResultTests - { - private const string TestSha1 = "0ee459db639f34c3064f56845acbc7df0d528e81"; - private const string Test2Sha1 = "2052fbe2ce5b081db3e3b9ffdebe9b0258d14cce"; - private const string EmptySha1 = "0000000000000000000000000000000000000000"; - - private const string TestTreePath1 = "Test/Scalar"; - private const string TestTreePath2 = "Test/directory with blob and spaces"; - private const string TestBlobPath1 = "Test/file with spaces.txt"; - private const string TestBlobPath2 = "Test/file with tree and spaces.txt"; - - private static readonly string MissingColonLineFromDiffTree = $"040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}"; - private static readonly string TooManyFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M BadData\t{TestTreePath1}"; - private static readonly string NotEnoughFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1}\t{TestTreePath1}"; - private static readonly string TwoPathLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}\t{TestBlobPath1}"; - private static readonly string ModifyTreeLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}"; - private static readonly string DeleteTreeLineFromDiffTree = $":040000 000000 {TestSha1} {EmptySha1} D\t{TestTreePath1}"; - private static readonly string AddTreeLineFromDiffTree = $":000000 040000 {EmptySha1} {Test2Sha1} A\t{TestTreePath1}"; - private static readonly string ModifyBlobLineFromDiffTree = $":100644 100644 {TestSha1} {Test2Sha1} M\t{TestBlobPath1}"; - private static readonly string DeleteBlobLineFromDiffTree = $":100755 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}"; - private static readonly string DeleteBlobLineFromDiffTree2 = $":100644 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}"; - private static readonly string AddBlobLineFromDiffTree = $":000000 100644 {EmptySha1} {Test2Sha1} A\t{TestBlobPath1}"; - - private static readonly string BlobLineFromLsTree = $"100644 blob {TestSha1}\t{TestTreePath1}"; - private static readonly string BlobLineWithTreePathFromLsTree = $"100644 blob {TestSha1}\t{TestBlobPath2}"; - private static readonly string TreeLineFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath1}"; - private static readonly string TreeLineWithBlobPathFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath2}"; - private static readonly string InvalidLineFromLsTree = $"040000 bad {TestSha1}\t{TestTreePath1}"; - private static readonly string SymLinkLineFromLsTree = $"120000 blob {TestSha1}\t{TestTreePath1}"; - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_NullLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(null)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_EmptyLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(string.Empty)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_EmptyRepo() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Modify, - SourceIsDirectory = true, - TargetIsDirectory = true, - TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, - SourceSha = TestSha1, - TargetSha = Test2Sha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromLsTreeLine_NullLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(null)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromLsTreeLine_EmptyLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(string.Empty)); - } - - [TestCase] - public void ParseFromLsTreeLine_EmptyRepoRoot() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = null, - TargetSha = TestSha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromLsTreeLine_BlobLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = null, - TargetSha = TestSha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromLsTreeLine_TreeLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = true, - TargetPath = CreateTreePath(TestTreePath1), - SourceSha = null, - TargetSha = null - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromLsTreeLine_InvalidLine() - { - DiffTreeResult.ParseFromLsTreeLine(InvalidLineFromLsTree).ShouldBeNull(); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_NoColonLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(MissingColonLineFromDiffTree)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_TooManyFieldsLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TooManyFieldsLineFromDiffTree)); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_NotEnoughFieldsLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(NotEnoughFieldsLineFromDiffTree)); - } - +using NUnit.Framework; +using Scalar.Common.Git; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using System; +using System.IO; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixture] + public class DiffTreeResultTests + { + private const string TestSha1 = "0ee459db639f34c3064f56845acbc7df0d528e81"; + private const string Test2Sha1 = "2052fbe2ce5b081db3e3b9ffdebe9b0258d14cce"; + private const string EmptySha1 = "0000000000000000000000000000000000000000"; + + private const string TestTreePath1 = "Test/Scalar"; + private const string TestTreePath2 = "Test/directory with blob and spaces"; + private const string TestBlobPath1 = "Test/file with spaces.txt"; + private const string TestBlobPath2 = "Test/file with tree and spaces.txt"; + + private static readonly string MissingColonLineFromDiffTree = $"040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}"; + private static readonly string TooManyFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M BadData\t{TestTreePath1}"; + private static readonly string NotEnoughFieldsLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1}\t{TestTreePath1}"; + private static readonly string TwoPathLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}\t{TestBlobPath1}"; + private static readonly string ModifyTreeLineFromDiffTree = $":040000 040000 {TestSha1} {Test2Sha1} M\t{TestTreePath1}"; + private static readonly string DeleteTreeLineFromDiffTree = $":040000 000000 {TestSha1} {EmptySha1} D\t{TestTreePath1}"; + private static readonly string AddTreeLineFromDiffTree = $":000000 040000 {EmptySha1} {Test2Sha1} A\t{TestTreePath1}"; + private static readonly string ModifyBlobLineFromDiffTree = $":100644 100644 {TestSha1} {Test2Sha1} M\t{TestBlobPath1}"; + private static readonly string DeleteBlobLineFromDiffTree = $":100755 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}"; + private static readonly string DeleteBlobLineFromDiffTree2 = $":100644 000000 {TestSha1} {EmptySha1} D\t{TestBlobPath1}"; + private static readonly string AddBlobLineFromDiffTree = $":000000 100644 {EmptySha1} {Test2Sha1} A\t{TestBlobPath1}"; + + private static readonly string BlobLineFromLsTree = $"100644 blob {TestSha1}\t{TestTreePath1}"; + private static readonly string BlobLineWithTreePathFromLsTree = $"100644 blob {TestSha1}\t{TestBlobPath2}"; + private static readonly string TreeLineFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath1}"; + private static readonly string TreeLineWithBlobPathFromLsTree = $"040000 tree {TestSha1}\t{TestTreePath2}"; + private static readonly string InvalidLineFromLsTree = $"040000 bad {TestSha1}\t{TestTreePath1}"; + private static readonly string SymLinkLineFromLsTree = $"120000 blob {TestSha1}\t{TestTreePath1}"; + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_NullLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(null)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_EmptyLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(string.Empty)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_EmptyRepo() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Modify, + SourceIsDirectory = true, + TargetIsDirectory = true, + TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, + SourceSha = TestSha1, + TargetSha = Test2Sha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromLsTreeLine_NullLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(null)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromLsTreeLine_EmptyLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromLsTreeLine(string.Empty)); + } + + [TestCase] + public void ParseFromLsTreeLine_EmptyRepoRoot() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = null, + TargetSha = TestSha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromLsTreeLine_BlobLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = null, + TargetSha = TestSha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromLsTreeLine_TreeLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = true, + TargetPath = CreateTreePath(TestTreePath1), + SourceSha = null, + TargetSha = null + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromLsTreeLine_InvalidLine() + { + DiffTreeResult.ParseFromLsTreeLine(InvalidLineFromLsTree).ShouldBeNull(); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_NoColonLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(MissingColonLineFromDiffTree)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_TooManyFieldsLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TooManyFieldsLineFromDiffTree)); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_NotEnoughFieldsLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(NotEnoughFieldsLineFromDiffTree)); + } + [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public void ParseFromDiffTreeLine_TwoPathLine() - { - Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TwoPathLineFromDiffTree)); - } - - [TestCase] - public void ParseFromDiffTreeLine_ModifyTreeLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Modify, - SourceIsDirectory = true, - TargetIsDirectory = true, - TargetPath = CreateTreePath(TestTreePath1), - SourceSha = TestSha1, - TargetSha = Test2Sha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_DeleteTreeLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Delete, - SourceIsDirectory = true, - TargetIsDirectory = false, - TargetPath = CreateTreePath(TestTreePath1), - SourceSha = TestSha1, - TargetSha = EmptySha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteTreeLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_AddTreeLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = true, - TargetPath = CreateTreePath(TestTreePath1), - SourceSha = EmptySha1, - TargetSha = Test2Sha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddTreeLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_AddBlobLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = EmptySha1, - TargetSha = Test2Sha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddBlobLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_DeleteBlobLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Delete, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = TestSha1, - TargetSha = EmptySha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_DeleteBlobLine2() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Delete, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = TestSha1, - TargetSha = EmptySha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree2); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_ModifyBlobLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Modify, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = TestSha1, - TargetSha = Test2Sha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyBlobLineFromDiffTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromLsTreeLine_SymLinkLine() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetIsSymLink = true, - TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), - SourceSha = null, - TargetSha = TestSha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(SymLinkLineFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_TreeLineWithBlobPath() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = true, - TargetPath = CreateTreePath(TestTreePath2), - SourceSha = null, - TargetSha = null - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineWithBlobPathFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase] - public void ParseFromDiffTreeLine_BlobLineWithTreePath() - { - DiffTreeResult expected = new DiffTreeResult() - { - Operation = DiffTreeResult.Operations.Add, - SourceIsDirectory = false, - TargetIsDirectory = false, - TargetPath = TestBlobPath2.Replace('/', Path.DirectorySeparatorChar), - SourceSha = null, - TargetSha = TestSha1 - }; - - DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineWithTreePathFromLsTree); - this.ValidateDiffTreeResult(expected, result); - } - - [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar", DiffTreeResult.TreeMarker, true)] - [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar", DiffTreeResult.BlobMarker, false)] - [TestCase("100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md", DiffTreeResult.BlobMarker, true)] - [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildScalarForMac.sh", DiffTreeResult.BlobMarker, true)] - [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildScalarForMac.sh", DiffTreeResult.BlobMarker, true)] - [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / tree file.txt", DiffTreeResult.TreeMarker, false)] - [TestCase("100755 ", DiffTreeResult.TreeMarker, false)] - public void TestGetIndexOfTypeMarker(string line, string typeMarker, bool expectedResult) - { - DiffTreeResult.IsLsTreeLineOfType(line, typeMarker).ShouldEqual(expectedResult); - } - - private static string CreateTreePath(string testPath) - { - return testPath.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; - } - - private void ValidateDiffTreeResult(DiffTreeResult expected, DiffTreeResult actual) - { - actual.Operation.ShouldEqual(expected.Operation, $"{nameof(DiffTreeResult)}.{nameof(actual.Operation)}"); - actual.SourceIsDirectory.ShouldEqual(expected.SourceIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceIsDirectory)}"); - actual.TargetIsDirectory.ShouldEqual(expected.TargetIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetIsDirectory)}"); - actual.TargetPath.ShouldEqual(expected.TargetPath, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetPath)}"); - actual.SourceSha.ShouldEqual(expected.SourceSha, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceSha)}"); - actual.TargetSha.ShouldEqual(expected.TargetSha, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetSha)}"); - } - } -} + [Category(CategoryConstants.ExceptionExpected)] + public void ParseFromDiffTreeLine_TwoPathLine() + { + Assert.Throws(() => DiffTreeResult.ParseFromDiffTreeLine(TwoPathLineFromDiffTree)); + } + + [TestCase] + public void ParseFromDiffTreeLine_ModifyTreeLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Modify, + SourceIsDirectory = true, + TargetIsDirectory = true, + TargetPath = CreateTreePath(TestTreePath1), + SourceSha = TestSha1, + TargetSha = Test2Sha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyTreeLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_DeleteTreeLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Delete, + SourceIsDirectory = true, + TargetIsDirectory = false, + TargetPath = CreateTreePath(TestTreePath1), + SourceSha = TestSha1, + TargetSha = EmptySha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteTreeLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_AddTreeLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = true, + TargetPath = CreateTreePath(TestTreePath1), + SourceSha = EmptySha1, + TargetSha = Test2Sha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddTreeLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_AddBlobLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = EmptySha1, + TargetSha = Test2Sha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(AddBlobLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_DeleteBlobLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Delete, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = TestSha1, + TargetSha = EmptySha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_DeleteBlobLine2() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Delete, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = TestSha1, + TargetSha = EmptySha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(DeleteBlobLineFromDiffTree2); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_ModifyBlobLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Modify, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestBlobPath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = TestSha1, + TargetSha = Test2Sha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromDiffTreeLine(ModifyBlobLineFromDiffTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromLsTreeLine_SymLinkLine() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetIsSymLink = true, + TargetPath = TestTreePath1.Replace('/', Path.DirectorySeparatorChar), + SourceSha = null, + TargetSha = TestSha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(SymLinkLineFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_TreeLineWithBlobPath() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = true, + TargetPath = CreateTreePath(TestTreePath2), + SourceSha = null, + TargetSha = null + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(TreeLineWithBlobPathFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase] + public void ParseFromDiffTreeLine_BlobLineWithTreePath() + { + DiffTreeResult expected = new DiffTreeResult() + { + Operation = DiffTreeResult.Operations.Add, + SourceIsDirectory = false, + TargetIsDirectory = false, + TargetPath = TestBlobPath2.Replace('/', Path.DirectorySeparatorChar), + SourceSha = null, + TargetSha = TestSha1 + }; + + DiffTreeResult result = DiffTreeResult.ParseFromLsTreeLine(BlobLineWithTreePathFromLsTree); + this.ValidateDiffTreeResult(expected, result); + } + + [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar", DiffTreeResult.TreeMarker, true)] + [TestCase("040000 tree 73b881d52b607b0f3e9e620d36f556d3d233a11d\tScalar", DiffTreeResult.BlobMarker, false)] + [TestCase("100644 blob 44c5f5cba4b29d31c2ad06eed51ea02af76c27c0\tReadme.md", DiffTreeResult.BlobMarker, true)] + [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildScalarForMac.sh", DiffTreeResult.BlobMarker, true)] + [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / BuildScalarForMac.sh", DiffTreeResult.BlobMarker, true)] + [TestCase("100755 blob 196142fbb753c0a3c7c6690323db7aa0a11f41ec\tScripts / tree file.txt", DiffTreeResult.TreeMarker, false)] + [TestCase("100755 ", DiffTreeResult.TreeMarker, false)] + public void TestGetIndexOfTypeMarker(string line, string typeMarker, bool expectedResult) + { + DiffTreeResult.IsLsTreeLineOfType(line, typeMarker).ShouldEqual(expectedResult); + } + + private static string CreateTreePath(string testPath) + { + return testPath.Replace('/', Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; + } + + private void ValidateDiffTreeResult(DiffTreeResult expected, DiffTreeResult actual) + { + actual.Operation.ShouldEqual(expected.Operation, $"{nameof(DiffTreeResult)}.{nameof(actual.Operation)}"); + actual.SourceIsDirectory.ShouldEqual(expected.SourceIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceIsDirectory)}"); + actual.TargetIsDirectory.ShouldEqual(expected.TargetIsDirectory, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetIsDirectory)}"); + actual.TargetPath.ShouldEqual(expected.TargetPath, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetPath)}"); + actual.SourceSha.ShouldEqual(expected.SourceSha, $"{nameof(DiffTreeResult)}.{nameof(actual.SourceSha)}"); + actual.TargetSha.ShouldEqual(expected.TargetSha, $"{nameof(DiffTreeResult)}.{nameof(actual.TargetSha)}"); + } + } +} diff --git a/Scalar.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs b/Scalar.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs index 1dd3ddcee8..e30bdc88e7 100644 --- a/Scalar.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs +++ b/Scalar.UnitTests/Prefetch/PrefetchPacksDeserializerTests.cs @@ -1,181 +1,181 @@ -using NUnit.Framework; -using Scalar.Common.NetworkStreams; -using Scalar.Tests.Should; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixture] - public class PrefetchPacksDeserializerTests - { - private static readonly byte[] PrefetchPackExpectedHeader - = new byte[] - { - (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', - 1 // Version - }; - - [TestCase] - public void PrefetchPacksDeserializer_No_Packs_Succeeds() - { - this.RunPrefetchPacksDeserializerTest(0, false); - } - - [TestCase] - public void PrefetchPacksDeserializer_Single_Pack_With_Index_Receives_Both() - { - this.RunPrefetchPacksDeserializerTest(1, true); - } - - [TestCase] - public void PrefetchPacksDeserializer_Single_Pack_Without_Index_Receives_Only_Pack() - { - this.RunPrefetchPacksDeserializerTest(1, false); - } - - [TestCase] - public void PrefetchPacksDeserializer_Multiple_Packs_With_Indexes() - { - this.RunPrefetchPacksDeserializerTest(10, true); - } - - [TestCase] - public void PrefetchPacksDeserializer_Multiple_Packs_Without_Indexes() - { - this.RunPrefetchPacksDeserializerTest(10, false); - } - - /// - /// A deterministic way to create somewhat unique packs - /// - private static byte[] PackForTimestamp(long timestamp) - { - unchecked - { - Random rand = new Random((int)timestamp); - byte[] data = new byte[100]; - rand.NextBytes(data); - return data; - } - } - - /// - /// A deterministic way to create somewhat unique indexes - /// - private static byte[] IndexForTimestamp(long timestamp) - { - unchecked - { - Random rand = new Random((int)-timestamp); - byte[] data = new byte[50]; - rand.NextBytes(data); - return data; - } - } - - /// - /// Implementation of the PrefetchPack spec to generate data for tests - /// - private void WriteToSpecs(Stream stream, long[] packTimestamps, bool withIndexes) - { - // Header - stream.Write(PrefetchPackExpectedHeader, 0, PrefetchPackExpectedHeader.Length); - - // PackCount - stream.Write(BitConverter.GetBytes((ushort)packTimestamps.Length), 0, 2); - - for (int i = 0; i < packTimestamps.Length; i++) - { - byte[] packContents = PackForTimestamp(packTimestamps[i]); - byte[] indexContents = IndexForTimestamp(packTimestamps[i]); - - // Pack Header - // Timestamp - stream.Write(BitConverter.GetBytes(packTimestamps[i]), 0, 8); - - // Pack length - stream.Write(BitConverter.GetBytes((long)packContents.Length), 0, 8); - - // Pack index length - if (withIndexes) - { - stream.Write(BitConverter.GetBytes((long)indexContents.Length), 0, 8); - } - else - { - stream.Write(BitConverter.GetBytes(-1L), 0, 8); - } - - // Pack data - stream.Write(packContents, 0, packContents.Length); - - if (withIndexes) - { - stream.Write(indexContents, 0, indexContents.Length); - } - } - } - - private void RunPrefetchPacksDeserializerTest(int packCount, bool withIndexes) - { - using (MemoryStream ms = new MemoryStream()) - { - long[] packTimestamps = Enumerable.Range(0, packCount).Select(x => (long)x).ToArray(); - - // Write the data to the memory stream. - this.WriteToSpecs(ms, packTimestamps, withIndexes); - ms.Position = 0; - - Dictionary>> receivedPacksAndIndexes = new Dictionary>>(); - - foreach (PrefetchPacksDeserializer.PackAndIndex pack in new PrefetchPacksDeserializer(ms).EnumeratePacks()) - { - List> packsAndIndexesByUniqueId; - if (!receivedPacksAndIndexes.TryGetValue(pack.UniqueId, out packsAndIndexesByUniqueId)) - { - packsAndIndexesByUniqueId = new List>(); - receivedPacksAndIndexes.Add(pack.UniqueId, packsAndIndexesByUniqueId); - } - - using (MemoryStream packContent = new MemoryStream()) - using (MemoryStream idxContent = new MemoryStream()) - { - pack.PackStream.CopyTo(packContent); - byte[] packData = packContent.ToArray(); - packData.ShouldMatchInOrder(PackForTimestamp(pack.Timestamp)); - packsAndIndexesByUniqueId.Add(Tuple.Create("pack", pack.Timestamp)); - - if (pack.IndexStream != null) - { - pack.IndexStream.CopyTo(idxContent); - byte[] idxData = idxContent.ToArray(); - idxData.ShouldMatchInOrder(IndexForTimestamp(pack.Timestamp)); - packsAndIndexesByUniqueId.Add(Tuple.Create("idx", pack.Timestamp)); - } - } - } - - receivedPacksAndIndexes.Count.ShouldEqual(packCount, "UniqueId count"); - - foreach (List> groupedByUniqueId in receivedPacksAndIndexes.Values) - { - if (withIndexes) - { - groupedByUniqueId.Count.ShouldEqual(2, "Both Pack and Index for UniqueId"); - - // Should only contain 1 index file - groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "idx"); - } - - // should only contain 1 pack file - groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "pack"); - - groupedByUniqueId.Select(x => x.Item2).Distinct().Count().ShouldEqual(1, "Same timestamps for a uniqueId"); - } - } - } - } -} +using NUnit.Framework; +using Scalar.Common.NetworkStreams; +using Scalar.Tests.Should; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixture] + public class PrefetchPacksDeserializerTests + { + private static readonly byte[] PrefetchPackExpectedHeader + = new byte[] + { + (byte)'G', (byte)'P', (byte)'R', (byte)'E', (byte)' ', + 1 // Version + }; + + [TestCase] + public void PrefetchPacksDeserializer_No_Packs_Succeeds() + { + this.RunPrefetchPacksDeserializerTest(0, false); + } + + [TestCase] + public void PrefetchPacksDeserializer_Single_Pack_With_Index_Receives_Both() + { + this.RunPrefetchPacksDeserializerTest(1, true); + } + + [TestCase] + public void PrefetchPacksDeserializer_Single_Pack_Without_Index_Receives_Only_Pack() + { + this.RunPrefetchPacksDeserializerTest(1, false); + } + + [TestCase] + public void PrefetchPacksDeserializer_Multiple_Packs_With_Indexes() + { + this.RunPrefetchPacksDeserializerTest(10, true); + } + + [TestCase] + public void PrefetchPacksDeserializer_Multiple_Packs_Without_Indexes() + { + this.RunPrefetchPacksDeserializerTest(10, false); + } + + /// + /// A deterministic way to create somewhat unique packs + /// + private static byte[] PackForTimestamp(long timestamp) + { + unchecked + { + Random rand = new Random((int)timestamp); + byte[] data = new byte[100]; + rand.NextBytes(data); + return data; + } + } + + /// + /// A deterministic way to create somewhat unique indexes + /// + private static byte[] IndexForTimestamp(long timestamp) + { + unchecked + { + Random rand = new Random((int)-timestamp); + byte[] data = new byte[50]; + rand.NextBytes(data); + return data; + } + } + + /// + /// Implementation of the PrefetchPack spec to generate data for tests + /// + private void WriteToSpecs(Stream stream, long[] packTimestamps, bool withIndexes) + { + // Header + stream.Write(PrefetchPackExpectedHeader, 0, PrefetchPackExpectedHeader.Length); + + // PackCount + stream.Write(BitConverter.GetBytes((ushort)packTimestamps.Length), 0, 2); + + for (int i = 0; i < packTimestamps.Length; i++) + { + byte[] packContents = PackForTimestamp(packTimestamps[i]); + byte[] indexContents = IndexForTimestamp(packTimestamps[i]); + + // Pack Header + // Timestamp + stream.Write(BitConverter.GetBytes(packTimestamps[i]), 0, 8); + + // Pack length + stream.Write(BitConverter.GetBytes((long)packContents.Length), 0, 8); + + // Pack index length + if (withIndexes) + { + stream.Write(BitConverter.GetBytes((long)indexContents.Length), 0, 8); + } + else + { + stream.Write(BitConverter.GetBytes(-1L), 0, 8); + } + + // Pack data + stream.Write(packContents, 0, packContents.Length); + + if (withIndexes) + { + stream.Write(indexContents, 0, indexContents.Length); + } + } + } + + private void RunPrefetchPacksDeserializerTest(int packCount, bool withIndexes) + { + using (MemoryStream ms = new MemoryStream()) + { + long[] packTimestamps = Enumerable.Range(0, packCount).Select(x => (long)x).ToArray(); + + // Write the data to the memory stream. + this.WriteToSpecs(ms, packTimestamps, withIndexes); + ms.Position = 0; + + Dictionary>> receivedPacksAndIndexes = new Dictionary>>(); + + foreach (PrefetchPacksDeserializer.PackAndIndex pack in new PrefetchPacksDeserializer(ms).EnumeratePacks()) + { + List> packsAndIndexesByUniqueId; + if (!receivedPacksAndIndexes.TryGetValue(pack.UniqueId, out packsAndIndexesByUniqueId)) + { + packsAndIndexesByUniqueId = new List>(); + receivedPacksAndIndexes.Add(pack.UniqueId, packsAndIndexesByUniqueId); + } + + using (MemoryStream packContent = new MemoryStream()) + using (MemoryStream idxContent = new MemoryStream()) + { + pack.PackStream.CopyTo(packContent); + byte[] packData = packContent.ToArray(); + packData.ShouldMatchInOrder(PackForTimestamp(pack.Timestamp)); + packsAndIndexesByUniqueId.Add(Tuple.Create("pack", pack.Timestamp)); + + if (pack.IndexStream != null) + { + pack.IndexStream.CopyTo(idxContent); + byte[] idxData = idxContent.ToArray(); + idxData.ShouldMatchInOrder(IndexForTimestamp(pack.Timestamp)); + packsAndIndexesByUniqueId.Add(Tuple.Create("idx", pack.Timestamp)); + } + } + } + + receivedPacksAndIndexes.Count.ShouldEqual(packCount, "UniqueId count"); + + foreach (List> groupedByUniqueId in receivedPacksAndIndexes.Values) + { + if (withIndexes) + { + groupedByUniqueId.Count.ShouldEqual(2, "Both Pack and Index for UniqueId"); + + // Should only contain 1 index file + groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "idx"); + } + + // should only contain 1 pack file + groupedByUniqueId.ShouldContainSingle(x => x.Item1 == "pack"); + + groupedByUniqueId.Select(x => x.Item2).Distinct().Count().ShouldEqual(1, "Same timestamps for a uniqueId"); + } + } + } + } +} diff --git a/Scalar.UnitTests/Prefetch/PrefetchTracingTests.cs b/Scalar.UnitTests/Prefetch/PrefetchTracingTests.cs index 40809abe63..ff3844425b 100644 --- a/Scalar.UnitTests/Prefetch/PrefetchTracingTests.cs +++ b/Scalar.UnitTests/Prefetch/PrefetchTracingTests.cs @@ -1,95 +1,95 @@ -using NUnit.Framework; -using Scalar.Common.Prefetch.Pipeline; -using Scalar.Common.Prefetch.Pipeline.Data; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.Git; -using System.Collections.Concurrent; - -namespace Scalar.UnitTests.Prefetch -{ - [TestFixture] - public class PrefetchTracingTests - { - private const string FakeSha = "fakesha"; - private const string FakeShaContents = "fakeshacontents"; - - [TestCase] - public void ErrorsForBatchObjectDownloadJob() - { - using (ITracer tracer = CreateTracer()) - { - MockScalarEnlistment enlistment = new MockScalarEnlistment(); - MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); - MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects); - - BlockingCollection input = new BlockingCollection(); - input.Add(FakeSha); - input.CompleteAdding(); - - BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); - dut.Start(); - dut.WaitForCompletion(); - - string sha; - input.TryTake(out sha).ShouldEqual(false); - - IndexPackRequest request; - dut.AvailablePacks.TryTake(out request).ShouldEqual(false); - } - } - - [TestCase] - public void SuccessForBatchObjectDownloadJob() - { - using (ITracer tracer = CreateTracer()) - { - MockScalarEnlistment enlistment = new MockScalarEnlistment(); - MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); - httpGitObjects.AddBlobContent(FakeSha, FakeShaContents); - MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects); - - BlockingCollection input = new BlockingCollection(); - input.Add(FakeSha); - input.CompleteAdding(); - - BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); - dut.Start(); - dut.WaitForCompletion(); - - string sha; - input.TryTake(out sha).ShouldEqual(false); - dut.AvailablePacks.Count.ShouldEqual(0); - - dut.AvailableObjects.Count.ShouldEqual(1); - string output = dut.AvailableObjects.Take(); - output.ShouldEqual(FakeSha); - } - } - - [TestCase] - public void ErrorsForIndexPackFile() - { - using (ITracer tracer = CreateTracer()) - { - MockScalarEnlistment enlistment = new MockScalarEnlistment(); - MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, null); - - BlockingCollection input = new BlockingCollection(); - BlobDownloadRequest downloadRequest = new BlobDownloadRequest(new string[] { FakeSha }); - input.Add(new IndexPackRequest("mock:\\path\\packFileName", downloadRequest)); - input.CompleteAdding(); - - IndexPackStage dut = new IndexPackStage(1, input, new BlockingCollection(), tracer, gitObjects); - dut.Start(); - dut.WaitForCompletion(); - } - } - - private static ITracer CreateTracer() - { - return new MockTracer(); - } - } -} +using NUnit.Framework; +using Scalar.Common.Prefetch.Pipeline; +using Scalar.Common.Prefetch.Pipeline.Data; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.Git; +using System.Collections.Concurrent; + +namespace Scalar.UnitTests.Prefetch +{ + [TestFixture] + public class PrefetchTracingTests + { + private const string FakeSha = "fakesha"; + private const string FakeShaContents = "fakeshacontents"; + + [TestCase] + public void ErrorsForBatchObjectDownloadJob() + { + using (ITracer tracer = CreateTracer()) + { + MockScalarEnlistment enlistment = new MockScalarEnlistment(); + MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects); + + BlockingCollection input = new BlockingCollection(); + input.Add(FakeSha); + input.CompleteAdding(); + + BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + + string sha; + input.TryTake(out sha).ShouldEqual(false); + + IndexPackRequest request; + dut.AvailablePacks.TryTake(out request).ShouldEqual(false); + } + } + + [TestCase] + public void SuccessForBatchObjectDownloadJob() + { + using (ITracer tracer = CreateTracer()) + { + MockScalarEnlistment enlistment = new MockScalarEnlistment(); + MockHttpGitObjects httpGitObjects = new MockHttpGitObjects(tracer, enlistment); + httpGitObjects.AddBlobContent(FakeSha, FakeShaContents); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, httpGitObjects); + + BlockingCollection input = new BlockingCollection(); + input.Add(FakeSha); + input.CompleteAdding(); + + BatchObjectDownloadStage dut = new BatchObjectDownloadStage(1, 1, input, new BlockingCollection(), tracer, enlistment, httpGitObjects, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + + string sha; + input.TryTake(out sha).ShouldEqual(false); + dut.AvailablePacks.Count.ShouldEqual(0); + + dut.AvailableObjects.Count.ShouldEqual(1); + string output = dut.AvailableObjects.Take(); + output.ShouldEqual(FakeSha); + } + } + + [TestCase] + public void ErrorsForIndexPackFile() + { + using (ITracer tracer = CreateTracer()) + { + MockScalarEnlistment enlistment = new MockScalarEnlistment(); + MockPhysicalGitObjects gitObjects = new MockPhysicalGitObjects(tracer, null, enlistment, null); + + BlockingCollection input = new BlockingCollection(); + BlobDownloadRequest downloadRequest = new BlobDownloadRequest(new string[] { FakeSha }); + input.Add(new IndexPackRequest("mock:\\path\\packFileName", downloadRequest)); + input.CompleteAdding(); + + IndexPackStage dut = new IndexPackStage(1, input, new BlockingCollection(), tracer, gitObjects); + dut.Start(); + dut.WaitForCompletion(); + } + } + + private static ITracer CreateTracer() + { + return new MockTracer(); + } + } +} diff --git a/Scalar.UnitTests/Program.cs b/Scalar.UnitTests/Program.cs index 3fbaafc65a..47c57f06cd 100644 --- a/Scalar.UnitTests/Program.cs +++ b/Scalar.UnitTests/Program.cs @@ -1,31 +1,31 @@ -using Scalar.Tests; -using Scalar.UnitTests.Category; -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace Scalar.UnitTests -{ - public class Program - { - public static void Main(string[] args) - { - NUnitRunner runner = new NUnitRunner(args); - - List excludeCategories = new List(); - - if (Debugger.IsAttached) - { - excludeCategories.Add(CategoryConstants.ExceptionExpected); - } - - Environment.ExitCode = runner.RunTests(includeCategories: null, excludeCategories: excludeCategories); - - if (Debugger.IsAttached) - { - Console.WriteLine("Tests completed. Press Enter to exit."); - Console.ReadLine(); - } - } - } +using Scalar.Tests; +using Scalar.UnitTests.Category; +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Scalar.UnitTests +{ + public class Program + { + public static void Main(string[] args) + { + NUnitRunner runner = new NUnitRunner(args); + + List excludeCategories = new List(); + + if (Debugger.IsAttached) + { + excludeCategories.Add(CategoryConstants.ExceptionExpected); + } + + Environment.ExitCode = runner.RunTests(includeCategories: null, excludeCategories: excludeCategories); + + if (Debugger.IsAttached) + { + Console.WriteLine("Tests completed. Press Enter to exit."); + Console.ReadLine(); + } + } + } } diff --git a/Scalar.UnitTests/Readme.md b/Scalar.UnitTests/Readme.md index ac230cfb8d..59919fac03 100644 --- a/Scalar.UnitTests/Readme.md +++ b/Scalar.UnitTests/Readme.md @@ -1,40 +1,40 @@ -# Scalar Unit Tests - -## Unit Test Projects - -### Scalar.UnitTests - -* Targets .NET Core -* Contains all unit tests that are .NET Standard compliant - -### Scalar.UnitTests.Windows - -* Targets .NET Framework -* Contains all unit tests that depend on .NET Framework assemblies -* Has links (in the `NetCore` folder) to all of the unit tests in Scalar.UnitTests - -Scalar.UnitTests.Windows links in all of the tests from Scalar.UnitTests to ensure that they pass on both the .NET Core and .Net Framework platforms. - -## Running Unit Tests - -**Option 1: `Scripts\RunUnitTests.bat`** - -`RunUnitTests.bat` will run both Scalar.UnitTests and Scalar.UnitTests.Windows - -**Option 2: Run individual projects from Visual Studio** - -Scalar.UnitTests and Scalar.UnitTests.Windows can both be run from Visual Studio. Simply set either as the StartUp project and run them from the IDE. - -## Adding New Tests - -### Scalar.UnitTests or Scalar.UnitTests.Windows? - -Whenever possible new unit tests should be added to Scalar.UnitTests. If the new tests are for a .NET Framework assembly (e.g. `Scalar.Platform.Windows`) -then they will need to be added to Scalar.UnitTests.Windows. - -### Adding New Test Files - -When adding new test files, keep the following in mind: - -* New test files that are added to Scalar.UnitTests will not appear in the `NetCore` folder of Scalar.UnitTests.Windows until the Scalar solution is reloaded. -* New test files that are meant to be run on both .NET platforms should be added to the **Scalar.UnitTests** project. +# Scalar Unit Tests + +## Unit Test Projects + +### Scalar.UnitTests + +* Targets .NET Core +* Contains all unit tests that are .NET Standard compliant + +### Scalar.UnitTests.Windows + +* Targets .NET Framework +* Contains all unit tests that depend on .NET Framework assemblies +* Has links (in the `NetCore` folder) to all of the unit tests in Scalar.UnitTests + +Scalar.UnitTests.Windows links in all of the tests from Scalar.UnitTests to ensure that they pass on both the .NET Core and .Net Framework platforms. + +## Running Unit Tests + +**Option 1: `Scripts\RunUnitTests.bat`** + +`RunUnitTests.bat` will run both Scalar.UnitTests and Scalar.UnitTests.Windows + +**Option 2: Run individual projects from Visual Studio** + +Scalar.UnitTests and Scalar.UnitTests.Windows can both be run from Visual Studio. Simply set either as the StartUp project and run them from the IDE. + +## Adding New Tests + +### Scalar.UnitTests or Scalar.UnitTests.Windows? + +Whenever possible new unit tests should be added to Scalar.UnitTests. If the new tests are for a .NET Framework assembly (e.g. `Scalar.Platform.Windows`) +then they will need to be added to Scalar.UnitTests.Windows. + +### Adding New Test Files + +When adding new test files, keep the following in mind: + +* New test files that are added to Scalar.UnitTests will not appear in the `NetCore` folder of Scalar.UnitTests.Windows until the Scalar solution is reloaded. +* New test files that are meant to be run on both .NET platforms should be added to the **Scalar.UnitTests** project. diff --git a/Scalar.UnitTests/Scalar.UnitTests.csproj b/Scalar.UnitTests/Scalar.UnitTests.csproj index 9ec495b8d3..796d1dfff8 100644 --- a/Scalar.UnitTests/Scalar.UnitTests.csproj +++ b/Scalar.UnitTests/Scalar.UnitTests.csproj @@ -1,59 +1,59 @@ - - - - Exe - netcoreapp2.1 - x64 - false - true - win-x64;osx-x64 - - - - true - Scalar.UnitTests - Scalar.UnitTests - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - Always - - - Always - - - Always - - - Always - - - - - - - - - - - - - all - - - - - - - - - - - + + + + Exe + netcoreapp2.1 + x64 + false + true + win-x64;osx-x64 + + + + true + Scalar.UnitTests + Scalar.UnitTests + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + Always + + + Always + + + Always + + + Always + + + + + + + + + + + + + all + + + + + + + + + + + diff --git a/Scalar.UnitTests/Service/Mac/MacServiceTests.cs b/Scalar.UnitTests/Service/Mac/MacServiceTests.cs index 88e6b4e380..16801fcddc 100644 --- a/Scalar.UnitTests/Service/Mac/MacServiceTests.cs +++ b/Scalar.UnitTests/Service/Mac/MacServiceTests.cs @@ -1,115 +1,115 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common; -using Scalar.Service; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using System.IO; - -namespace Scalar.UnitTests.Service.Mac -{ - [TestFixture] - public class MacServiceTests - { - private const string ScalarServiceName = "Scalar.Service"; - 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 ServiceStartTriggersAutoMountForCurrentUser() - { - Mock repoRegistry = new Mock(MockBehavior.Strict); - repoRegistry.Setup(r => r.AutoMountRepos(ExpectedActiveUserId.ToString(), ExpectedSessionId)); - repoRegistry.Setup(r => r.TraceStatus()); - - ScalarService service = new ScalarService( - this.tracer, - serviceName: null, - repoRegistry: repoRegistry.Object); - - service.Run(); - - repoRegistry.VerifyAll(); - } - - [TestCase] - public void RepoRegistryMountsOnlyRegisteredRepos() - { - Mock repoMounterMock = new Mock(MockBehavior.Strict); - repoMounterMock.Setup(mp => mp.MountRepository(ExpectedActiveRepoPath, ExpectedActiveUserId)).Returns(true); - - this.CreateTestRepos(ServiceDataLocation); - - RepoRegistry repoRegistry = new RepoRegistry( - this.tracer, - this.fileSystem, - ServiceDataLocation, - repoMounterMock.Object, - null); - - repoRegistry.AutoMountRepos(ExpectedActiveUserId.ToString(), ExpectedSessionId); - - repoMounterMock.VerifyAll(); - } - - [TestCase] - public void MountProcessLaunchedUsingCorrectArgs() - { - string executable = @"/bin/launchctl"; - string scalarBinPath = Path.Combine(this.scalarPlatform.Constants.ScalarBinDirectoryPath, this.scalarPlatform.Constants.ScalarExecutableName); - string expectedArgs = $"asuser {ExpectedActiveUserId} {scalarBinPath} mount {ExpectedActiveRepoPath}"; - - Mock mountLauncherMock = new Mock(MockBehavior.Strict, this.tracer); - mountLauncherMock.Setup(mp => mp.LaunchProcess( - executable, - expectedArgs, - ExpectedActiveRepoPath)) - .Returns(true); - - string errorString = null; - mountLauncherMock.Setup(mp => mp.WaitUntilMounted( - this.tracer, - ExpectedActiveRepoPath, - It.IsAny(), - out errorString)) - .Returns(true); - - ScalarMountProcess mountProcess = new ScalarMountProcess(this.tracer, mountLauncherMock.Object); - mountProcess.MountRepository(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}} - "); - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Common; +using Scalar.Service; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using System.IO; + +namespace Scalar.UnitTests.Service.Mac +{ + [TestFixture] + public class MacServiceTests + { + private const string ScalarServiceName = "Scalar.Service"; + 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 ServiceStartTriggersAutoMountForCurrentUser() + { + Mock repoRegistry = new Mock(MockBehavior.Strict); + repoRegistry.Setup(r => r.AutoMountRepos(ExpectedActiveUserId.ToString(), ExpectedSessionId)); + repoRegistry.Setup(r => r.TraceStatus()); + + ScalarService service = new ScalarService( + this.tracer, + serviceName: null, + repoRegistry: repoRegistry.Object); + + service.Run(); + + repoRegistry.VerifyAll(); + } + + [TestCase] + public void RepoRegistryMountsOnlyRegisteredRepos() + { + Mock repoMounterMock = new Mock(MockBehavior.Strict); + repoMounterMock.Setup(mp => mp.MountRepository(ExpectedActiveRepoPath, ExpectedActiveUserId)).Returns(true); + + this.CreateTestRepos(ServiceDataLocation); + + RepoRegistry repoRegistry = new RepoRegistry( + this.tracer, + this.fileSystem, + ServiceDataLocation, + repoMounterMock.Object, + null); + + repoRegistry.AutoMountRepos(ExpectedActiveUserId.ToString(), ExpectedSessionId); + + repoMounterMock.VerifyAll(); + } + + [TestCase] + public void MountProcessLaunchedUsingCorrectArgs() + { + string executable = @"/bin/launchctl"; + string scalarBinPath = Path.Combine(this.scalarPlatform.Constants.ScalarBinDirectoryPath, this.scalarPlatform.Constants.ScalarExecutableName); + string expectedArgs = $"asuser {ExpectedActiveUserId} {scalarBinPath} mount {ExpectedActiveRepoPath}"; + + Mock mountLauncherMock = new Mock(MockBehavior.Strict, this.tracer); + mountLauncherMock.Setup(mp => mp.LaunchProcess( + executable, + expectedArgs, + ExpectedActiveRepoPath)) + .Returns(true); + + string errorString = null; + mountLauncherMock.Setup(mp => mp.WaitUntilMounted( + this.tracer, + ExpectedActiveRepoPath, + It.IsAny(), + out errorString)) + .Returns(true); + + ScalarMountProcess mountProcess = new ScalarMountProcess(this.tracer, mountLauncherMock.Object); + mountProcess.MountRepository(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/RepoRegistryTests.cs b/Scalar.UnitTests/Service/RepoRegistryTests.cs index bf4798db7e..e0ee25399f 100644 --- a/Scalar.UnitTests/Service/RepoRegistryTests.cs +++ b/Scalar.UnitTests/Service/RepoRegistryTests.cs @@ -1,247 +1,247 @@ -using Moq; -using NUnit.Framework; -using Scalar.Service; -using Scalar.Service.Handlers; -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; - private Mock mockNotificationHandler; - - [SetUp] - public void Setup() - { - this.mockRepoMounter = new Mock(MockBehavior.Strict); - this.mockNotificationHandler = new Mock(MockBehavior.Strict); - } - - [TearDown] - public void TearDown() - { - this.mockRepoMounter.VerifyAll(); - this.mockNotificationHandler.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, - this.mockNotificationHandler.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 ReadRegistry_Upgrade_ExistingVersion1() - { - string dataLocation = Path.Combine("mock:", "registryDataFolder"); - MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); - - string repo1 = Path.Combine("mock:", "code", "repo1"); - string repo2 = Path.Combine("mock:", "code", "repo2"); - - // Create a version 1 registry file - fileSystem.WriteAllText( - Path.Combine(dataLocation, RepoRegistry.RegistryName), -$@"1 -{{""EnlistmentRoot"":""{repo1.Replace("\\", "\\\\")}"",""IsActive"":false}} -{{""EnlistmentRoot"":""{repo2.Replace("\\", "\\\\")}"",""IsActive"":true}} -"); - - RepoRegistry registry = new RepoRegistry( - new MockTracer(), - fileSystem, - dataLocation, - this.mockRepoMounter.Object, - this.mockNotificationHandler.Object); - registry.Upgrade(); - - Dictionary repos = registry.ReadRegistry(); - repos.Count.ShouldEqual(2); - - this.VerifyRepo(repos[repo1], expectedOwnerSID: null, expectedIsActive: false); - this.VerifyRepo(repos[repo2], expectedOwnerSID: null, expectedIsActive: true); - } - - [TestCase] - public void ReadRegistry_Upgrade_NoRegistry() - { - 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, - this.mockNotificationHandler.Object); - registry.Upgrade(); - - Dictionary repos = registry.ReadRegistry(); - repos.Count.ShouldEqual(0); - } - - [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, - this.mockNotificationHandler.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, - this.mockNotificationHandler.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, - this.mockNotificationHandler.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); - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Service; +using Scalar.Service.Handlers; +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; + private Mock mockNotificationHandler; + + [SetUp] + public void Setup() + { + this.mockRepoMounter = new Mock(MockBehavior.Strict); + this.mockNotificationHandler = new Mock(MockBehavior.Strict); + } + + [TearDown] + public void TearDown() + { + this.mockRepoMounter.VerifyAll(); + this.mockNotificationHandler.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, + this.mockNotificationHandler.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 ReadRegistry_Upgrade_ExistingVersion1() + { + string dataLocation = Path.Combine("mock:", "registryDataFolder"); + MockFileSystem fileSystem = new MockFileSystem(new MockDirectory(dataLocation, null, null)); + + string repo1 = Path.Combine("mock:", "code", "repo1"); + string repo2 = Path.Combine("mock:", "code", "repo2"); + + // Create a version 1 registry file + fileSystem.WriteAllText( + Path.Combine(dataLocation, RepoRegistry.RegistryName), +$@"1 +{{""EnlistmentRoot"":""{repo1.Replace("\\", "\\\\")}"",""IsActive"":false}} +{{""EnlistmentRoot"":""{repo2.Replace("\\", "\\\\")}"",""IsActive"":true}} +"); + + RepoRegistry registry = new RepoRegistry( + new MockTracer(), + fileSystem, + dataLocation, + this.mockRepoMounter.Object, + this.mockNotificationHandler.Object); + registry.Upgrade(); + + Dictionary repos = registry.ReadRegistry(); + repos.Count.ShouldEqual(2); + + this.VerifyRepo(repos[repo1], expectedOwnerSID: null, expectedIsActive: false); + this.VerifyRepo(repos[repo2], expectedOwnerSID: null, expectedIsActive: true); + } + + [TestCase] + public void ReadRegistry_Upgrade_NoRegistry() + { + 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, + this.mockNotificationHandler.Object); + registry.Upgrade(); + + Dictionary repos = registry.ReadRegistry(); + repos.Count.ShouldEqual(0); + } + + [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, + this.mockNotificationHandler.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, + this.mockNotificationHandler.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, + this.mockNotificationHandler.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.UnitTests/Setup.cs b/Scalar.UnitTests/Setup.cs index fd7daa915d..e07ef3d63e 100644 --- a/Scalar.UnitTests/Setup.cs +++ b/Scalar.UnitTests/Setup.cs @@ -1,16 +1,16 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.UnitTests.Mock.Common; - -namespace Scalar.UnitTests -{ - [SetUpFixture] - public class Setup - { - [OneTimeSetUp] - public void SetUp() - { - ScalarPlatform.Register(new MockPlatform()); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.UnitTests.Mock.Common; + +namespace Scalar.UnitTests +{ + [SetUpFixture] + public class Setup + { + [OneTimeSetUp] + public void SetUp() + { + ScalarPlatform.Register(new MockPlatform()); + } + } +} diff --git a/Scalar.UnitTests/Tracing/EventListenerTests.cs b/Scalar.UnitTests/Tracing/EventListenerTests.cs index 5e84b923fb..1ad98dc507 100644 --- a/Scalar.UnitTests/Tracing/EventListenerTests.cs +++ b/Scalar.UnitTests/Tracing/EventListenerTests.cs @@ -1,46 +1,46 @@ -using Moq; -using NUnit.Framework; -using Scalar.Common.Tracing; -using System; - -namespace Scalar.UnitTests.Tracing -{ - [TestFixture] - public class EventListenerTests - { - [TestCase] - public void EventListener_RecordMessage_ExceptionThrownInternally_RaisesFailureEventWithErrorMessage() - { - string expectedErrorMessage = $"test error message unique={Guid.NewGuid():N}"; - - Mock eventSink = new Mock(); - - TraceEventMessage message = new TraceEventMessage { Level = EventLevel.Error, Keywords = Keywords.None }; - TestEventListener listener = new TestEventListener(EventLevel.Informational, Keywords.Any, eventSink.Object) - { - RecordMessageInternalCallback = _ => throw new Exception(expectedErrorMessage) - }; - - listener.RecordMessage(message); - - eventSink.Verify( - x => x.OnListenerFailure(listener, It.Is(msg => msg.Contains(expectedErrorMessage))), - times: Times.Once); - } - - private class TestEventListener : EventListener - { - public TestEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) - : base(maxVerbosity, keywordFilter, eventSink) - { - } - - public Action RecordMessageInternalCallback { get; set; } - - protected override void RecordMessageInternal(TraceEventMessage message) - { - this.RecordMessageInternalCallback?.Invoke(message); - } - } - } -} +using Moq; +using NUnit.Framework; +using Scalar.Common.Tracing; +using System; + +namespace Scalar.UnitTests.Tracing +{ + [TestFixture] + public class EventListenerTests + { + [TestCase] + public void EventListener_RecordMessage_ExceptionThrownInternally_RaisesFailureEventWithErrorMessage() + { + string expectedErrorMessage = $"test error message unique={Guid.NewGuid():N}"; + + Mock eventSink = new Mock(); + + TraceEventMessage message = new TraceEventMessage { Level = EventLevel.Error, Keywords = Keywords.None }; + TestEventListener listener = new TestEventListener(EventLevel.Informational, Keywords.Any, eventSink.Object) + { + RecordMessageInternalCallback = _ => throw new Exception(expectedErrorMessage) + }; + + listener.RecordMessage(message); + + eventSink.Verify( + x => x.OnListenerFailure(listener, It.Is(msg => msg.Contains(expectedErrorMessage))), + times: Times.Once); + } + + private class TestEventListener : EventListener + { + public TestEventListener(EventLevel maxVerbosity, Keywords keywordFilter, IEventListenerEventSink eventSink) + : base(maxVerbosity, keywordFilter, eventSink) + { + } + + public Action RecordMessageInternalCallback { get; set; } + + protected override void RecordMessageInternal(TraceEventMessage message) + { + this.RecordMessageInternalCallback?.Invoke(message); + } + } + } +} diff --git a/Scalar.UnitTests/Tracing/TelemetryDaemonEventListenerTests.cs b/Scalar.UnitTests/Tracing/TelemetryDaemonEventListenerTests.cs index 03070a93c0..4dd78f3c7a 100644 --- a/Scalar.UnitTests/Tracing/TelemetryDaemonEventListenerTests.cs +++ b/Scalar.UnitTests/Tracing/TelemetryDaemonEventListenerTests.cs @@ -1,77 +1,77 @@ -using Newtonsoft.Json; -using NUnit.Framework; -using Scalar.Common.Tracing; -using Scalar.Tests.Should; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Tracing -{ - [TestFixture] - public class TelemetryDaemonEventListenerTests - { - [TestCase] - public void TraceMessageDataIsCorrectFormat() - { - const string vfsVersion = "test-vfsVersion"; - const string providerName = "test-ProviderName"; - const string eventName = "test-eventName"; - const EventLevel level = EventLevel.Error; - const EventOpcode opcode = EventOpcode.Start; - const string enlistmentId = "test-enlistmentId"; - const string mountId = "test-mountId"; - const string gitCommandSessionId = "test_sessionId"; - const string payload = "test-payload"; - - Dictionary expectedDict = new Dictionary - { - ["version"] = vfsVersion, - ["providerName"] = providerName, - ["eventName"] = eventName, - ["eventLevel"] = (int)level, - ["eventOpcode"] = (int)opcode, - ["payload"] = new Dictionary - { - ["enlistmentId"] = enlistmentId, - ["mountId"] = mountId, - ["gitCommandSessionId"] = gitCommandSessionId, - ["json"] = payload, - }, - }; - - TelemetryDaemonEventListener.PipeMessage message = new TelemetryDaemonEventListener.PipeMessage - { - Version = vfsVersion, - ProviderName = providerName, - EventName = eventName, - EventLevel = level, - EventOpcode = opcode, - Payload = new TelemetryDaemonEventListener.PipeMessage.PipeMessagePayload - { - EnlistmentId = enlistmentId, - MountId = mountId, - GitCommandSessionId = gitCommandSessionId, - Json = payload - }, - }; - - string messageJson = message.ToJson(); - - Dictionary actualDict = JsonConvert.DeserializeObject>(messageJson); - - actualDict.Count.ShouldEqual(expectedDict.Count); - actualDict["version"].ShouldEqual(expectedDict["version"]); - actualDict["providerName"].ShouldEqual(expectedDict["providerName"]); - actualDict["eventName"].ShouldEqual(expectedDict["eventName"]); - actualDict["eventLevel"].ShouldEqual(expectedDict["eventLevel"]); - actualDict["eventOpcode"].ShouldEqual(expectedDict["eventOpcode"]); - - Dictionary expectedPayloadDict = (Dictionary)expectedDict["payload"]; - Dictionary actualPayloadDict = JsonConvert.DeserializeObject>(actualDict["payload"].ToString()); - actualPayloadDict.Count.ShouldEqual(expectedPayloadDict.Count); - actualPayloadDict["enlistmentId"].ShouldEqual(expectedPayloadDict["enlistmentId"]); - actualPayloadDict["mountId"].ShouldEqual(expectedPayloadDict["mountId"]); - actualPayloadDict["gitCommandSessionId"].ShouldEqual(expectedPayloadDict["gitCommandSessionId"]); - actualPayloadDict["json"].ShouldEqual(expectedPayloadDict["json"]); - } - } -} +using Newtonsoft.Json; +using NUnit.Framework; +using Scalar.Common.Tracing; +using Scalar.Tests.Should; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Tracing +{ + [TestFixture] + public class TelemetryDaemonEventListenerTests + { + [TestCase] + public void TraceMessageDataIsCorrectFormat() + { + const string vfsVersion = "test-vfsVersion"; + const string providerName = "test-ProviderName"; + const string eventName = "test-eventName"; + const EventLevel level = EventLevel.Error; + const EventOpcode opcode = EventOpcode.Start; + const string enlistmentId = "test-enlistmentId"; + const string mountId = "test-mountId"; + const string gitCommandSessionId = "test_sessionId"; + const string payload = "test-payload"; + + Dictionary expectedDict = new Dictionary + { + ["version"] = vfsVersion, + ["providerName"] = providerName, + ["eventName"] = eventName, + ["eventLevel"] = (int)level, + ["eventOpcode"] = (int)opcode, + ["payload"] = new Dictionary + { + ["enlistmentId"] = enlistmentId, + ["mountId"] = mountId, + ["gitCommandSessionId"] = gitCommandSessionId, + ["json"] = payload, + }, + }; + + TelemetryDaemonEventListener.PipeMessage message = new TelemetryDaemonEventListener.PipeMessage + { + Version = vfsVersion, + ProviderName = providerName, + EventName = eventName, + EventLevel = level, + EventOpcode = opcode, + Payload = new TelemetryDaemonEventListener.PipeMessage.PipeMessagePayload + { + EnlistmentId = enlistmentId, + MountId = mountId, + GitCommandSessionId = gitCommandSessionId, + Json = payload + }, + }; + + string messageJson = message.ToJson(); + + Dictionary actualDict = JsonConvert.DeserializeObject>(messageJson); + + actualDict.Count.ShouldEqual(expectedDict.Count); + actualDict["version"].ShouldEqual(expectedDict["version"]); + actualDict["providerName"].ShouldEqual(expectedDict["providerName"]); + actualDict["eventName"].ShouldEqual(expectedDict["eventName"]); + actualDict["eventLevel"].ShouldEqual(expectedDict["eventLevel"]); + actualDict["eventOpcode"].ShouldEqual(expectedDict["eventOpcode"]); + + Dictionary expectedPayloadDict = (Dictionary)expectedDict["payload"]; + Dictionary actualPayloadDict = JsonConvert.DeserializeObject>(actualDict["payload"].ToString()); + actualPayloadDict.Count.ShouldEqual(expectedPayloadDict.Count); + actualPayloadDict["enlistmentId"].ShouldEqual(expectedPayloadDict["enlistmentId"]); + actualPayloadDict["mountId"].ShouldEqual(expectedPayloadDict["mountId"]); + actualPayloadDict["gitCommandSessionId"].ShouldEqual(expectedPayloadDict["gitCommandSessionId"]); + actualPayloadDict["json"].ShouldEqual(expectedPayloadDict["json"]); + } + } +} diff --git a/Scalar.UnitTests/Upgrader/ProductUpgraderTests.cs b/Scalar.UnitTests/Upgrader/ProductUpgraderTests.cs index 7cf96614bc..3c96719a08 100644 --- a/Scalar.UnitTests/Upgrader/ProductUpgraderTests.cs +++ b/Scalar.UnitTests/Upgrader/ProductUpgraderTests.cs @@ -1,130 +1,130 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using System; - -namespace Scalar.UnitTests.Upgrader -{ - [TestFixture] - public class ProductUpgraderTests : UpgradeTests - { - [SetUp] - public override void Setup() - { - base.Setup(); - } - - [TestCase] - public void UpgradeAvailableOnFastWhileOnLocalNoneRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.None, - expectedReturn: true, - expectedUpgradeVersion: null); - } - - [TestCase] - public void UpgradeAvailableOnSlowWhileOnLocalNoneRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.None, - expectedReturn: true, - expectedUpgradeVersion: null); - } - - [TestCase] - public void UpgradeAvailableOnFastWhileOnLocalSlowRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, - expectedReturn: true, - expectedUpgradeVersion: null); - } - - [TestCase] - public void UpgradeAvailableOnSlowWhileOnLocalSlowRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, - expectedReturn: true, - expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); - } - - [TestCase] - public void UpgradeAvailableOnFastWhileOnLocalFastRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, - expectedReturn: true, - expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); - } - - [TestCase] - public void UpgradeAvailableOnSlowWhileOnLocalFastRing() - { - this.SimulateUpgradeAvailable( - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, - remoteVersion: UpgradeTests.NewerThanLocalVersion, - localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, - expectedReturn: true, - expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); - } - - public override void NoneLocalRing() - { - throw new NotSupportedException(); - } - - public override void InvalidUpgradeRing() - { - throw new NotSupportedException(); - } - - public override void FetchReleaseInfo() - { - throw new NotSupportedException(); - } - - protected override ReturnCode RunUpgrade() - { - throw new NotSupportedException(); - } - - private void SimulateUpgradeAvailable( - GitHubUpgrader.GitHubUpgraderConfig.RingType remoteRing, - string remoteVersion, - GitHubUpgrader.GitHubUpgraderConfig.RingType localRing, - bool expectedReturn, - string expectedUpgradeVersion) - { - this.SetUpgradeRing(localRing.ToString()); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - remoteVersion, - remoteRing); - - Version newVersion; - string message; - this.Upgrader.TryQueryNewestVersion(out newVersion, out message).ShouldEqual(expectedReturn); - - if (string.IsNullOrEmpty(expectedUpgradeVersion)) - { - newVersion.ShouldBeNull(); - } - else - { - newVersion.ShouldNotBeNull(); - newVersion.ShouldEqual(new Version(expectedUpgradeVersion)); - } - } - } +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using System; + +namespace Scalar.UnitTests.Upgrader +{ + [TestFixture] + public class ProductUpgraderTests : UpgradeTests + { + [SetUp] + public override void Setup() + { + base.Setup(); + } + + [TestCase] + public void UpgradeAvailableOnFastWhileOnLocalNoneRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.None, + expectedReturn: true, + expectedUpgradeVersion: null); + } + + [TestCase] + public void UpgradeAvailableOnSlowWhileOnLocalNoneRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.None, + expectedReturn: true, + expectedUpgradeVersion: null); + } + + [TestCase] + public void UpgradeAvailableOnFastWhileOnLocalSlowRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, + expectedReturn: true, + expectedUpgradeVersion: null); + } + + [TestCase] + public void UpgradeAvailableOnSlowWhileOnLocalSlowRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, + expectedReturn: true, + expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); + } + + [TestCase] + public void UpgradeAvailableOnFastWhileOnLocalFastRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, + expectedReturn: true, + expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); + } + + [TestCase] + public void UpgradeAvailableOnSlowWhileOnLocalFastRing() + { + this.SimulateUpgradeAvailable( + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow, + remoteVersion: UpgradeTests.NewerThanLocalVersion, + localRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast, + expectedReturn: true, + expectedUpgradeVersion: UpgradeTests.NewerThanLocalVersion); + } + + public override void NoneLocalRing() + { + throw new NotSupportedException(); + } + + public override void InvalidUpgradeRing() + { + throw new NotSupportedException(); + } + + public override void FetchReleaseInfo() + { + throw new NotSupportedException(); + } + + protected override ReturnCode RunUpgrade() + { + throw new NotSupportedException(); + } + + private void SimulateUpgradeAvailable( + GitHubUpgrader.GitHubUpgraderConfig.RingType remoteRing, + string remoteVersion, + GitHubUpgrader.GitHubUpgraderConfig.RingType localRing, + bool expectedReturn, + string expectedUpgradeVersion) + { + this.SetUpgradeRing(localRing.ToString()); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + remoteVersion, + remoteRing); + + Version newVersion; + string message; + this.Upgrader.TryQueryNewestVersion(out newVersion, out message).ShouldEqual(expectedReturn); + + if (string.IsNullOrEmpty(expectedUpgradeVersion)) + { + newVersion.ShouldBeNull(); + } + else + { + newVersion.ShouldNotBeNull(); + newVersion.ShouldEqual(new Version(expectedUpgradeVersion)); + } + } + } } diff --git a/Scalar.UnitTests/Upgrader/UpgradeOrchestratorTests.cs b/Scalar.UnitTests/Upgrader/UpgradeOrchestratorTests.cs index 1e7698da90..c7463b602c 100644 --- a/Scalar.UnitTests/Upgrader/UpgradeOrchestratorTests.cs +++ b/Scalar.UnitTests/Upgrader/UpgradeOrchestratorTests.cs @@ -1,14 +1,14 @@ -using Moq; +using Moq; using NUnit.Framework; using Scalar.Common; -using Scalar.Common.Tracing; +using Scalar.Common.Tracing; using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using Scalar.UnitTests.Mock.Upgrader; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using Scalar.UnitTests.Mock.Upgrader; using Scalar.Upgrader; using System; -using System.Collections.Generic; +using System.Collections.Generic; namespace Scalar.UnitTests.Upgrader { @@ -134,8 +134,8 @@ public void ExecuteFailsWhenDownloadFails() })) .Returns(false); - this.orchestrator.Execute(); - + this.orchestrator.Execute(); + this.VerifyOrchestratorInvokes( upgradeAllowed: true, queryNewestVersion: true, @@ -158,8 +158,8 @@ public void ExecuteFailsWhenRunInstallerFails() })) .Returns(false); - this.orchestrator.Execute(); - + this.orchestrator.Execute(); + this.VerifyOrchestratorInvokes( upgradeAllowed: true, queryNewestVersion: true, @@ -176,10 +176,10 @@ public Mock DefaultUpgrader() { Mock mockUpgrader = new Mock(); - mockUpgrader.Setup(upgrader => upgrader.UpgradeAllowed(out It.Ref.IsAny)) - .Callback(new UpgradeAllowedCallback((out string delegateMessage) => - { - delegateMessage = string.Empty; + mockUpgrader.Setup(upgrader => upgrader.UpgradeAllowed(out It.Ref.IsAny)) + .Callback(new UpgradeAllowedCallback((out string delegateMessage) => + { + delegateMessage = string.Empty; })) .Returns(true); @@ -191,26 +191,26 @@ public Mock DefaultUpgrader() return mockUpgrader; } - public void SetUpgradeAvailable(Version newVersion, string error) - { - bool upgradeResult = string.IsNullOrEmpty(error); - + public void SetUpgradeAvailable(Version newVersion, string error) + { + bool upgradeResult = string.IsNullOrEmpty(error); + this.MoqUpgrader.Setup(upgrader => upgrader.TryQueryNewestVersion(out It.Ref.IsAny, out It.Ref.IsAny)) .Callback(new TryGetNewerVersionCallback((out System.Version delegateVersion, out string delegateMessage) => { delegateVersion = newVersion; delegateMessage = error; })) - .Returns(upgradeResult); + .Returns(upgradeResult); } - public void VerifyOrchestratorInvokes( - bool upgradeAllowed, - bool queryNewestVersion, - bool downloadNewestVersion, - bool installNewestVersion, - bool cleanup) - { + public void VerifyOrchestratorInvokes( + bool upgradeAllowed, + bool queryNewestVersion, + bool downloadNewestVersion, + bool installNewestVersion, + bool cleanup) + { this.MoqUpgrader.Verify( upgrader => upgrader.UpgradeAllowed( out It.Ref.IsAny), @@ -233,17 +233,17 @@ public void VerifyOrchestratorInvokes( out It.Ref.IsAny), installNewestVersion ? Times.Once() : Times.Never()); - this.MoqUpgrader.Verify( - upgrader => upgrader.TryCleanup( - out It.Ref.IsAny), - cleanup ? Times.Once() : Times.Never()); + this.MoqUpgrader.Verify( + upgrader => upgrader.TryCleanup( + out It.Ref.IsAny), + cleanup ? Times.Once() : Times.Never()); } - public void VerifyOutput(string expectedMessage) - { - this.Output.AllLines.ShouldContain( - new List() { expectedMessage }, - (line, expectedLine) => { return line.Contains(expectedLine); }); + public void VerifyOutput(string expectedMessage) + { + this.Output.AllLines.ShouldContain( + new List() { expectedMessage }, + (line, expectedLine) => { return line.Contains(expectedLine); }); } } } diff --git a/Scalar.UnitTests/Upgrader/UpgradeOrchestratorWithGitHubUpgraderTests.cs b/Scalar.UnitTests/Upgrader/UpgradeOrchestratorWithGitHubUpgraderTests.cs index 951838b39e..7ca3075c9d 100644 --- a/Scalar.UnitTests/Upgrader/UpgradeOrchestratorWithGitHubUpgraderTests.cs +++ b/Scalar.UnitTests/Upgrader/UpgradeOrchestratorWithGitHubUpgraderTests.cs @@ -1,324 +1,324 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using Scalar.UnitTests.Mock.Upgrader; -using Scalar.Upgrader; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Upgrader -{ - [TestFixture] - public class UpgradeOrchestratorWithGitHubUpgraderTests : UpgradeTests - { - private UpgradeOrchestrator orchestrator; - - [SetUp] - public override void Setup() - { - base.Setup(); - - this.orchestrator = new WindowsUpgradeOrchestrator( - this.Upgrader, - this.Tracer, - this.FileSystem, - this.PrerunChecker, - input: null, - output: this.Output); - this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); - } - - [TestCase] - public void UpgradeNoError() - { - this.RunUpgrade().ShouldEqual(ReturnCode.Success); - this.Tracer.RelatedErrorEvents.ShouldBeEmpty(); - } - - [TestCase] - public void AutoUnmountError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnMountRepos); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Unmount of some of the repositories failed." - }, - expectedErrors: new List - { - "Unmount of some of the repositories failed." - }); - } - - [TestCase] - public void AbortOnBlockingProcess() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.BlockingProcessesRunning); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "ERROR: Blocking processes are running.", - $"Run `scalar upgrade --confirm` again after quitting these processes - Scalar.Mount, git" - }, - expectedErrors: null, - expectedWarnings: new List - { - $"Run `scalar upgrade --confirm` again after quitting these processes - Scalar.Mount, git" - }); - } - - [TestCase] - public void DownloadDirectoryCreationError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.CreateDownloadDirectory); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Error creating download directory" - }, - expectedErrors: new List - { - "Error creating download directory" - }); - } - - [TestCase] - public void ScalarDownloadError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarDownload); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Error downloading Scalar from GitHub" - }, - expectedErrors: new List - { - "Error downloading Scalar from GitHub" - }); - } - - [TestCase] - public void GitDownloadError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitDownload); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Error downloading Git from GitHub" - }, - expectedErrors: new List - { - "Error downloading Git from GitHub" - }); - } - - [TestCase] - public void GitInstallationArgs() - { - this.RunUpgrade().ShouldEqual(ReturnCode.Success); - - Dictionary gitInstallerInfo; - this.Upgrader.InstallerArgs.ShouldBeNonEmpty(); - this.Upgrader.InstallerArgs.TryGetValue("Git", out gitInstallerInfo).ShouldBeTrue(); - - string args; - gitInstallerInfo.TryGetValue("Args", out args).ShouldBeTrue(); - args.ShouldContain(new string[] { "/VERYSILENT", "/CLOSEAPPLICATIONS", "/SUPPRESSMSGBOXES", "/NORESTART", "/Log" }); - } - - [TestCase] - public void GitInstallError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitInstall); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Git installation failed" - }, - expectedErrors: new List - { - "Git installation failed" - }); - } - - [TestCase] - public void GitInstallerAuthenticodeError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitAuthenticodeCheck); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "hash of the file does not match the hash stored in the digital signature" - }, - expectedErrors: new List - { - "hash of the file does not match the hash stored in the digital signature" - }); - } - - [TestCase] - public void ScalarInstallationArgs() - { - this.RunUpgrade().ShouldEqual(ReturnCode.Success); - - Dictionary gitInstallerInfo; - this.Upgrader.InstallerArgs.ShouldBeNonEmpty(); - this.Upgrader.InstallerArgs.TryGetValue("Scalar", out gitInstallerInfo).ShouldBeTrue(); - - string args; - gitInstallerInfo.TryGetValue("Args", out args).ShouldBeTrue(); - args.ShouldContain(new string[] { "/VERYSILENT", "/CLOSEAPPLICATIONS", "/SUPPRESSMSGBOXES", "/NORESTART", "/Log", "/REMOUNTREPOS=false" }); - } - - [TestCase] - public void ScalarInstallError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarInstall); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "Scalar installation failed" - }, - expectedErrors: new List - { - "Scalar installation failed" - }); - } - - [TestCase] - public void ScalarInstallerAuthenticodeError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarAuthenticodeCheck); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - "hash of the file does not match the hash stored in the digital signature" - }, - expectedErrors: new List - { - "hash of the file does not match the hash stored in the digital signature" - }); - } - - [TestCase] - public void ScalarCleanupError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarCleanup); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - }, - expectedErrors: new List - { - "Error deleting downloaded Scalar installer." - }); - } - - [TestCase] - public void GitCleanupError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitCleanup); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - }, - expectedErrors: new List - { - "Error deleting downloaded Git installer." - }); - } - - [TestCase] - public void RemountReposError() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.RemountRepos); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "Auto remount failed." - }, - expectedErrors: new List - { - "Auto remount failed." - }); - } - - [TestCase] - public void DryRunDoesNotRunInstallerExes() - { - this.ConfigureRunAndVerify( - configure: () => - { - this.Upgrader.SetDryRun(true); - this.Upgrader.InstallerExeLaunched = false; - this.SetUpgradeRing("Slow"); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - upgradeVersion: NewerThanLocalVersion, - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - "Installing Git", - "Installing Scalar", - "Upgrade completed successfully." - }, - expectedErrors: null); - - this.Upgrader.InstallerExeLaunched.ShouldBeFalse(); - } - - protected override ReturnCode RunUpgrade() - { - this.orchestrator.Execute(); - return this.orchestrator.ExitCode; - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using Scalar.UnitTests.Mock.Upgrader; +using Scalar.Upgrader; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Upgrader +{ + [TestFixture] + public class UpgradeOrchestratorWithGitHubUpgraderTests : UpgradeTests + { + private UpgradeOrchestrator orchestrator; + + [SetUp] + public override void Setup() + { + base.Setup(); + + this.orchestrator = new WindowsUpgradeOrchestrator( + this.Upgrader, + this.Tracer, + this.FileSystem, + this.PrerunChecker, + input: null, + output: this.Output); + this.PrerunChecker.SetCommandToRerun("`scalar upgrade --confirm`"); + } + + [TestCase] + public void UpgradeNoError() + { + this.RunUpgrade().ShouldEqual(ReturnCode.Success); + this.Tracer.RelatedErrorEvents.ShouldBeEmpty(); + } + + [TestCase] + public void AutoUnmountError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.UnMountRepos); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Unmount of some of the repositories failed." + }, + expectedErrors: new List + { + "Unmount of some of the repositories failed." + }); + } + + [TestCase] + public void AbortOnBlockingProcess() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.PrerunChecker.SetReturnTrueOnCheck(MockInstallerPrerunChecker.FailOnCheckType.BlockingProcessesRunning); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "ERROR: Blocking processes are running.", + $"Run `scalar upgrade --confirm` again after quitting these processes - Scalar.Mount, git" + }, + expectedErrors: null, + expectedWarnings: new List + { + $"Run `scalar upgrade --confirm` again after quitting these processes - Scalar.Mount, git" + }); + } + + [TestCase] + public void DownloadDirectoryCreationError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.CreateDownloadDirectory); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Error creating download directory" + }, + expectedErrors: new List + { + "Error creating download directory" + }); + } + + [TestCase] + public void ScalarDownloadError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarDownload); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Error downloading Scalar from GitHub" + }, + expectedErrors: new List + { + "Error downloading Scalar from GitHub" + }); + } + + [TestCase] + public void GitDownloadError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitDownload); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Error downloading Git from GitHub" + }, + expectedErrors: new List + { + "Error downloading Git from GitHub" + }); + } + + [TestCase] + public void GitInstallationArgs() + { + this.RunUpgrade().ShouldEqual(ReturnCode.Success); + + Dictionary gitInstallerInfo; + this.Upgrader.InstallerArgs.ShouldBeNonEmpty(); + this.Upgrader.InstallerArgs.TryGetValue("Git", out gitInstallerInfo).ShouldBeTrue(); + + string args; + gitInstallerInfo.TryGetValue("Args", out args).ShouldBeTrue(); + args.ShouldContain(new string[] { "/VERYSILENT", "/CLOSEAPPLICATIONS", "/SUPPRESSMSGBOXES", "/NORESTART", "/Log" }); + } + + [TestCase] + public void GitInstallError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitInstall); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Git installation failed" + }, + expectedErrors: new List + { + "Git installation failed" + }); + } + + [TestCase] + public void GitInstallerAuthenticodeError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitAuthenticodeCheck); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "hash of the file does not match the hash stored in the digital signature" + }, + expectedErrors: new List + { + "hash of the file does not match the hash stored in the digital signature" + }); + } + + [TestCase] + public void ScalarInstallationArgs() + { + this.RunUpgrade().ShouldEqual(ReturnCode.Success); + + Dictionary gitInstallerInfo; + this.Upgrader.InstallerArgs.ShouldBeNonEmpty(); + this.Upgrader.InstallerArgs.TryGetValue("Scalar", out gitInstallerInfo).ShouldBeTrue(); + + string args; + gitInstallerInfo.TryGetValue("Args", out args).ShouldBeTrue(); + args.ShouldContain(new string[] { "/VERYSILENT", "/CLOSEAPPLICATIONS", "/SUPPRESSMSGBOXES", "/NORESTART", "/Log", "/REMOUNTREPOS=false" }); + } + + [TestCase] + public void ScalarInstallError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarInstall); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "Scalar installation failed" + }, + expectedErrors: new List + { + "Scalar installation failed" + }); + } + + [TestCase] + public void ScalarInstallerAuthenticodeError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarAuthenticodeCheck); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + "hash of the file does not match the hash stored in the digital signature" + }, + expectedErrors: new List + { + "hash of the file does not match the hash stored in the digital signature" + }); + } + + [TestCase] + public void ScalarCleanupError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.ScalarCleanup); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + }, + expectedErrors: new List + { + "Error deleting downloaded Scalar installer." + }); + } + + [TestCase] + public void GitCleanupError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.GitCleanup); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + }, + expectedErrors: new List + { + "Error deleting downloaded Git installer." + }); + } + + [TestCase] + public void RemountReposError() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.PrerunChecker.SetReturnFalseOnCheck(MockInstallerPrerunChecker.FailOnCheckType.RemountRepos); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "Auto remount failed." + }, + expectedErrors: new List + { + "Auto remount failed." + }); + } + + [TestCase] + public void DryRunDoesNotRunInstallerExes() + { + this.ConfigureRunAndVerify( + configure: () => + { + this.Upgrader.SetDryRun(true); + this.Upgrader.InstallerExeLaunched = false; + this.SetUpgradeRing("Slow"); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + upgradeVersion: NewerThanLocalVersion, + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + "Installing Git", + "Installing Scalar", + "Upgrade completed successfully." + }, + expectedErrors: null); + + this.Upgrader.InstallerExeLaunched.ShouldBeFalse(); + } + + protected override ReturnCode RunUpgrade() + { + this.orchestrator.Execute(); + return this.orchestrator.ExitCode; + } + } +} diff --git a/Scalar.UnitTests/Upgrader/UpgradeTests.cs b/Scalar.UnitTests/Upgrader/UpgradeTests.cs index fc4eb1510e..ec8140de2e 100644 --- a/Scalar.UnitTests/Upgrader/UpgradeTests.cs +++ b/Scalar.UnitTests/Upgrader/UpgradeTests.cs @@ -1,191 +1,191 @@ -using NUnit.Framework; -using Scalar.Common; -using Scalar.Tests.Should; -using Scalar.UnitTests.Category; -using Scalar.UnitTests.Mock.Common; -using Scalar.UnitTests.Mock.FileSystem; -using Scalar.UnitTests.Mock.Upgrader; -using System; -using System.Collections.Generic; - -namespace Scalar.UnitTests.Upgrader -{ - public abstract class UpgradeTests - { - protected const string OlderThanLocalVersion = "1.0.17000.1"; - protected const string LocalScalarVersion = "1.0.18115.1"; - protected const string NewerThanLocalVersion = "1.1.18115.1"; - - protected MockTracer Tracer { get; private set; } - protected MockFileSystem FileSystem { get; private set; } - protected MockTextWriter Output { get; private set; } - protected MockInstallerPrerunChecker PrerunChecker { get; private set; } - protected MockGitHubUpgrader Upgrader { get; private set; } - protected MockLocalScalarConfig LocalConfig { get; private set; } - - public virtual void Setup() - { - this.Tracer = new MockTracer(); - this.FileSystem = new MockFileSystem(new MockDirectory(@"mock:\Scalar.Upgrades\Download", null, null)); - this.Output = new MockTextWriter(); - this.PrerunChecker = new MockInstallerPrerunChecker(this.Tracer); - this.LocalConfig = new MockLocalScalarConfig(); - - this.Upgrader = new MockGitHubUpgrader( - LocalScalarVersion, - this.Tracer, - this.FileSystem, - new GitHubUpgrader.GitHubUpgraderConfig(this.Tracer, this.LocalConfig)); - - this.PrerunChecker.Reset(); - this.Upgrader.PretendNewReleaseAvailableAtRemote( - upgradeVersion: NewerThanLocalVersion, - remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); - this.SetUpgradeRing("Slow"); - } - - [TestCase] - public virtual void NoneLocalRing() - { - string message = "Upgrade ring set to \"None\". No upgrade check was performed."; - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("None"); - }, - expectedReturn: ReturnCode.Success, - expectedOutput: new List - { - message - }, - expectedErrors: new List - { - }); - } - - [TestCase] - public virtual void InvalidUpgradeRing() - { - this.SetUpgradeRing("Invalid"); - - string expectedError = "Invalid upgrade ring `Invalid` specified in scalar config."; - string errorString; - GitHubUpgrader.Create( - this.Tracer, - this.FileSystem, - dryRun: false, - noVerify: false, - localConfig: this.LocalConfig, - error: out errorString).ShouldBeNull(); - - errorString.ShouldContain(expectedError); - } - - [TestCase] - [Category(CategoryConstants.ExceptionExpected)] - public virtual void FetchReleaseInfo() - { - string errorString = "Error fetching upgrade release info."; - this.ConfigureRunAndVerify( - configure: () => - { - this.SetUpgradeRing("Fast"); - this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.FetchReleaseInfo); - }, - expectedReturn: ReturnCode.GenericError, - expectedOutput: new List - { - errorString - }, - expectedErrors: new List - { - errorString - }); - } - - protected abstract ReturnCode RunUpgrade(); - - protected void ConfigureRunAndVerify( - Action configure, - ReturnCode expectedReturn, - List expectedOutput, - List expectedErrors, - List expectedWarnings = null) - { - configure(); - - this.RunUpgrade().ShouldEqual(expectedReturn); - - if (expectedOutput != null) - { - this.Output.AllLines.ShouldContain( - expectedOutput, - (line, expectedLine) => { return line.Contains(expectedLine); }); - } - - if (expectedErrors != null) - { - this.Tracer.RelatedErrorEvents.ShouldContain( - expectedErrors, - (error, expectedError) => { return error.Contains(expectedError); }); - } - - if (expectedWarnings != null) - { - this.Tracer.RelatedWarningEvents.ShouldContain( - expectedWarnings, - (warning, expectedWarning) => { return warning.Contains(expectedWarning); }); - } - } - - protected void SetUpgradeRing(string ringName) - { - GitHubUpgrader.GitHubUpgraderConfig.RingType ring; - if (!Enum.TryParse(ringName, ignoreCase: true, result: out ring)) - { - ring = GitHubUpgrader.GitHubUpgraderConfig.RingType.Invalid; - } - - string error; - if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow || - ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast) - { - this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); - this.VerifyConfig(ring, isUpgradeAllowed: true, isConfigError: false); - return; - } - - if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.None) - { - this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); - this.VerifyConfig(ring, isUpgradeAllowed: false, isConfigError: false); - return; - } - - if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Invalid) - { - this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); - this.VerifyConfig(ring, isUpgradeAllowed: false, isConfigError: true); - return; - } - } - - protected void VerifyConfig( - Scalar.Common.GitHubUpgrader.GitHubUpgraderConfig.RingType ring, - bool isUpgradeAllowed, - bool isConfigError) - { - string error; - this.Upgrader.Config.TryLoad(out error).ShouldBeTrue(); - - Assert.AreEqual(ring, this.Upgrader.Config.UpgradeRing); - error.ShouldBeNull(); - - bool upgradeAllowed = this.Upgrader.UpgradeAllowed(out _); - bool configError = this.Upgrader.Config.ConfigError(); - - upgradeAllowed.ShouldEqual(isUpgradeAllowed); - configError.ShouldEqual(isConfigError); - } - } -} +using NUnit.Framework; +using Scalar.Common; +using Scalar.Tests.Should; +using Scalar.UnitTests.Category; +using Scalar.UnitTests.Mock.Common; +using Scalar.UnitTests.Mock.FileSystem; +using Scalar.UnitTests.Mock.Upgrader; +using System; +using System.Collections.Generic; + +namespace Scalar.UnitTests.Upgrader +{ + public abstract class UpgradeTests + { + protected const string OlderThanLocalVersion = "1.0.17000.1"; + protected const string LocalScalarVersion = "1.0.18115.1"; + protected const string NewerThanLocalVersion = "1.1.18115.1"; + + protected MockTracer Tracer { get; private set; } + protected MockFileSystem FileSystem { get; private set; } + protected MockTextWriter Output { get; private set; } + protected MockInstallerPrerunChecker PrerunChecker { get; private set; } + protected MockGitHubUpgrader Upgrader { get; private set; } + protected MockLocalScalarConfig LocalConfig { get; private set; } + + public virtual void Setup() + { + this.Tracer = new MockTracer(); + this.FileSystem = new MockFileSystem(new MockDirectory(@"mock:\Scalar.Upgrades\Download", null, null)); + this.Output = new MockTextWriter(); + this.PrerunChecker = new MockInstallerPrerunChecker(this.Tracer); + this.LocalConfig = new MockLocalScalarConfig(); + + this.Upgrader = new MockGitHubUpgrader( + LocalScalarVersion, + this.Tracer, + this.FileSystem, + new GitHubUpgrader.GitHubUpgraderConfig(this.Tracer, this.LocalConfig)); + + this.PrerunChecker.Reset(); + this.Upgrader.PretendNewReleaseAvailableAtRemote( + upgradeVersion: NewerThanLocalVersion, + remoteRing: GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow); + this.SetUpgradeRing("Slow"); + } + + [TestCase] + public virtual void NoneLocalRing() + { + string message = "Upgrade ring set to \"None\". No upgrade check was performed."; + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("None"); + }, + expectedReturn: ReturnCode.Success, + expectedOutput: new List + { + message + }, + expectedErrors: new List + { + }); + } + + [TestCase] + public virtual void InvalidUpgradeRing() + { + this.SetUpgradeRing("Invalid"); + + string expectedError = "Invalid upgrade ring `Invalid` specified in scalar config."; + string errorString; + GitHubUpgrader.Create( + this.Tracer, + this.FileSystem, + dryRun: false, + noVerify: false, + localConfig: this.LocalConfig, + error: out errorString).ShouldBeNull(); + + errorString.ShouldContain(expectedError); + } + + [TestCase] + [Category(CategoryConstants.ExceptionExpected)] + public virtual void FetchReleaseInfo() + { + string errorString = "Error fetching upgrade release info."; + this.ConfigureRunAndVerify( + configure: () => + { + this.SetUpgradeRing("Fast"); + this.Upgrader.SetFailOnAction(MockGitHubUpgrader.ActionType.FetchReleaseInfo); + }, + expectedReturn: ReturnCode.GenericError, + expectedOutput: new List + { + errorString + }, + expectedErrors: new List + { + errorString + }); + } + + protected abstract ReturnCode RunUpgrade(); + + protected void ConfigureRunAndVerify( + Action configure, + ReturnCode expectedReturn, + List expectedOutput, + List expectedErrors, + List expectedWarnings = null) + { + configure(); + + this.RunUpgrade().ShouldEqual(expectedReturn); + + if (expectedOutput != null) + { + this.Output.AllLines.ShouldContain( + expectedOutput, + (line, expectedLine) => { return line.Contains(expectedLine); }); + } + + if (expectedErrors != null) + { + this.Tracer.RelatedErrorEvents.ShouldContain( + expectedErrors, + (error, expectedError) => { return error.Contains(expectedError); }); + } + + if (expectedWarnings != null) + { + this.Tracer.RelatedWarningEvents.ShouldContain( + expectedWarnings, + (warning, expectedWarning) => { return warning.Contains(expectedWarning); }); + } + } + + protected void SetUpgradeRing(string ringName) + { + GitHubUpgrader.GitHubUpgraderConfig.RingType ring; + if (!Enum.TryParse(ringName, ignoreCase: true, result: out ring)) + { + ring = GitHubUpgrader.GitHubUpgraderConfig.RingType.Invalid; + } + + string error; + if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Slow || + ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Fast) + { + this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); + this.VerifyConfig(ring, isUpgradeAllowed: true, isConfigError: false); + return; + } + + if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.None) + { + this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); + this.VerifyConfig(ring, isUpgradeAllowed: false, isConfigError: false); + return; + } + + if (ring == GitHubUpgrader.GitHubUpgraderConfig.RingType.Invalid) + { + this.LocalConfig.TrySetConfig("upgrade.ring", ringName, out error); + this.VerifyConfig(ring, isUpgradeAllowed: false, isConfigError: true); + return; + } + } + + protected void VerifyConfig( + Scalar.Common.GitHubUpgrader.GitHubUpgraderConfig.RingType ring, + bool isUpgradeAllowed, + bool isConfigError) + { + string error; + this.Upgrader.Config.TryLoad(out error).ShouldBeTrue(); + + Assert.AreEqual(ring, this.Upgrader.Config.UpgradeRing); + error.ShouldBeNull(); + + bool upgradeAllowed = this.Upgrader.UpgradeAllowed(out _); + bool configError = this.Upgrader.Config.ConfigError(); + + upgradeAllowed.ShouldEqual(isUpgradeAllowed); + configError.ShouldEqual(isConfigError); + } + } +} diff --git a/Scalar.Upgrader/App.config b/Scalar.Upgrader/App.config index 9ac0876abb..ece138ecf8 100644 --- a/Scalar.Upgrader/App.config +++ b/Scalar.Upgrader/App.config @@ -1,14 +1,14 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + diff --git a/Scalar.Upgrader/MacUpgradeOrchestrator.cs b/Scalar.Upgrader/MacUpgradeOrchestrator.cs index 68b87e399d..2951771e36 100644 --- a/Scalar.Upgrader/MacUpgradeOrchestrator.cs +++ b/Scalar.Upgrader/MacUpgradeOrchestrator.cs @@ -1,17 +1,17 @@ -namespace Scalar.Upgrader -{ - public class MacUpgradeOrchestrator : UpgradeOrchestrator - { - public MacUpgradeOrchestrator(UpgradeOptions options) - : base(options) - { - } - - protected override bool TryMountRepositories(out string consoleError) - { - // Mac upgrader does not mount repositories - consoleError = null; - return true; - } - } -} +namespace Scalar.Upgrader +{ + public class MacUpgradeOrchestrator : UpgradeOrchestrator + { + public MacUpgradeOrchestrator(UpgradeOptions options) + : base(options) + { + } + + protected override bool TryMountRepositories(out string consoleError) + { + // Mac upgrader does not mount repositories + consoleError = null; + return true; + } + } +} diff --git a/Scalar.Upgrader/Program.cs b/Scalar.Upgrader/Program.cs index 1126eea7fc..b42f178855 100644 --- a/Scalar.Upgrader/Program.cs +++ b/Scalar.Upgrader/Program.cs @@ -1,16 +1,16 @@ -using CommandLine; -using Scalar.PlatformLoader; - -namespace Scalar.Upgrader -{ - public class Program - { - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - - Parser.Default.ParseArguments(args) - .WithParsed(options => UpgradeOrchestratorFactory.Create(options).Execute()); - } - } -} +using CommandLine; +using Scalar.PlatformLoader; + +namespace Scalar.Upgrader +{ + public class Program + { + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + + Parser.Default.ParseArguments(args) + .WithParsed(options => UpgradeOrchestratorFactory.Create(options).Execute()); + } + } +} diff --git a/Scalar.Upgrader/Properties/AssemblyInfo.cs b/Scalar.Upgrader/Properties/AssemblyInfo.cs index 76a7e23588..952f1cdf81 100644 --- a/Scalar.Upgrader/Properties/AssemblyInfo.cs +++ b/Scalar.Upgrader/Properties/AssemblyInfo.cs @@ -1,22 +1,22 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar.Upgrader")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar.Upgrader")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("aecec217-2499-403d-b0bb-2962b9be5970")] +using System.Reflection; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar.Upgrader")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar.Upgrader")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("aecec217-2499-403d-b0bb-2962b9be5970")] diff --git a/Scalar.Upgrader/Scalar.Upgrader.csproj b/Scalar.Upgrader/Scalar.Upgrader.csproj index eb4187f8fb..e09fe352fc 100644 --- a/Scalar.Upgrader/Scalar.Upgrader.csproj +++ b/Scalar.Upgrader/Scalar.Upgrader.csproj @@ -1,68 +1,68 @@ - - - - - Exe - Scalar.Upgrader - - x64 - x64 - Scalar.Upgrader - false - - true - - - - $(ScalarVersion) - - - $(ScalarVersion) - - - - - - - net461;netcoreapp2.1 - $(DefineConstants);WINDOWS_BUILD - - - - - PlatformLoader.Windows.cs - - - - - - netcoreapp2.1 - osx-x64 - $(DefineConstants);MACOS_BUILD - - - - - PlatformLoader.Mac.cs - - - - - - - - - - - - - - - - - all - - - - + + + + + Exe + Scalar.Upgrader + + x64 + x64 + Scalar.Upgrader + false + + true + + + + $(ScalarVersion) + + + $(ScalarVersion) + + + + + + + net461;netcoreapp2.1 + $(DefineConstants);WINDOWS_BUILD + + + + + PlatformLoader.Windows.cs + + + + + + netcoreapp2.1 + osx-x64 + $(DefineConstants);MACOS_BUILD + + + + + PlatformLoader.Mac.cs + + + + + + + + + + + + + + + + + all + + + + diff --git a/Scalar.Upgrader/UpgradeOptions.cs b/Scalar.Upgrader/UpgradeOptions.cs index 7a6d93e64b..ed8ff3ba66 100644 --- a/Scalar.Upgrader/UpgradeOptions.cs +++ b/Scalar.Upgrader/UpgradeOptions.cs @@ -1,22 +1,22 @@ -using CommandLine; - -namespace Scalar.Upgrader -{ - [Verb("UpgradeOrchestrator", HelpText = "Upgrade Scalar.")] - public class UpgradeOptions - { - [Option( - "dry-run", - Default = false, - Required = false, - HelpText = "Display progress and errors, but don't install Scalar")] - public bool DryRun { get; set; } - - [Option( - "no-verify", - Default = false, - Required = false, - HelpText = "Don't verify authenticode signature of installers")] - public bool NoVerify { get; set; } - } -} +using CommandLine; + +namespace Scalar.Upgrader +{ + [Verb("UpgradeOrchestrator", HelpText = "Upgrade Scalar.")] + public class UpgradeOptions + { + [Option( + "dry-run", + Default = false, + Required = false, + HelpText = "Display progress and errors, but don't install Scalar")] + public bool DryRun { get; set; } + + [Option( + "no-verify", + Default = false, + Required = false, + HelpText = "Don't verify authenticode signature of installers")] + public bool NoVerify { get; set; } + } +} diff --git a/Scalar.Upgrader/UpgradeOrchestrator.cs b/Scalar.Upgrader/UpgradeOrchestrator.cs index f09bafc7a6..8a795d206d 100644 --- a/Scalar.Upgrader/UpgradeOrchestrator.cs +++ b/Scalar.Upgrader/UpgradeOrchestrator.cs @@ -1,388 +1,388 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.IO; -using System.Text; - -namespace Scalar.Upgrader -{ - public abstract class UpgradeOrchestrator - { - protected InstallerPreRunChecker preRunChecker; - protected bool mount; - protected ITracer tracer; - - private const EventLevel DefaultEventLevel = EventLevel.Informational; - - private ProductUpgrader upgrader; - private string logDirectory = ProductUpgraderInfo.GetLogDirectoryPath(); - private string installationId; - private PhysicalFileSystem fileSystem; - private TextWriter output; - private TextReader input; - - public UpgradeOrchestrator( - ProductUpgrader upgrader, - ITracer tracer, - PhysicalFileSystem fileSystem, - InstallerPreRunChecker preRunChecker, - TextReader input, - TextWriter output) - { - this.upgrader = upgrader; - this.tracer = tracer; - this.fileSystem = fileSystem; - this.preRunChecker = preRunChecker; - this.output = output; - this.input = input; - this.mount = false; - this.ExitCode = ReturnCode.Success; - this.installationId = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - } - - public UpgradeOrchestrator(UpgradeOptions options) - : this() - { - this.DryRun = options.DryRun; - this.NoVerify = options.NoVerify; - } - - public UpgradeOrchestrator() - { - // CommandLine's Parser will create multiple instances of UpgradeOrchestrator, and we don't want - // multiple log files to get created. Defer tracer (and preRunChecker) creation until Execute() - this.tracer = null; - this.preRunChecker = null; - - this.fileSystem = new PhysicalFileSystem(); - this.output = Console.Out; - this.input = Console.In; - this.mount = false; - this.ExitCode = ReturnCode.Success; - this.installationId = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - } - - public ReturnCode ExitCode { get; private set; } - - public bool DryRun { get; } - - public bool NoVerify { get; } - - public void Execute() - { - string error = null; - string mountError = null; - Version newVersion = null; - - if (this.tracer == null) - { - this.tracer = this.CreateTracer(); - } - - if (this.preRunChecker == null) - { - this.preRunChecker = new InstallerPreRunChecker(this.tracer, ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm); - } - - try - { - if (this.TryInitialize(out error)) - { - try - { - if (!this.TryRunUpgrade(out newVersion, out error)) - { - this.ExitCode = ReturnCode.GenericError; - } - } - finally - { - if (!this.TryMountRepositories(out mountError)) - { - mountError = Environment.NewLine + "WARNING: " + mountError; - this.output.WriteLine(mountError); - } - - this.DeletedDownloadedAssets(); - } - } - else - { - this.ExitCode = ReturnCode.GenericError; - } - - if (this.ExitCode == ReturnCode.GenericError) - { - StringBuilder sb = new StringBuilder(); - sb.AppendLine(); - sb.Append("ERROR: " + error); - - sb.AppendLine(); - sb.AppendLine(); - - sb.AppendLine($"Upgrade logs can be found at: {this.logDirectory} with file names that end with the installation ID: {this.installationId}."); - - this.output.WriteLine(sb.ToString()); - } - else - { - if (newVersion != null) - { - this.output.WriteLine($"{Environment.NewLine}Upgrade completed successfully{(string.IsNullOrEmpty(mountError) ? "." : ", but one or more repositories will need to be mounted manually.")}"); - } - } - } - finally - { - this.upgrader?.Dispose(); - } - - if (this.input == Console.In) - { - this.output.WriteLine("Press Enter to exit."); - this.input.ReadLine(); - } - - Environment.ExitCode = (int)this.ExitCode; - } - - protected bool LaunchInsideSpinner(Func method, string message) - { - return ConsoleHelper.ShowStatusWhileRunning( - method, - message, - this.output, - this.output == Console.Out && !ScalarPlatform.Instance.IsConsoleOutputRedirectedToFile(), - null); - } - - protected abstract bool TryMountRepositories(out string consoleError); - - private JsonTracer CreateTracer() - { - string logFilePath = ScalarEnlistment.GetNewScalarLogFileName( - this.logDirectory, - ScalarConstants.LogFileTypes.UpgradeProcess, - this.fileSystem); - - JsonTracer jsonTracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "UpgradeProcess"); - - jsonTracer.AddLogFileEventListener( - logFilePath, - DefaultEventLevel, - Keywords.Any); - - return jsonTracer; - } - - private bool TryInitialize(out string errorMessage) - { - if (this.upgrader == null) - { - string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (string.IsNullOrEmpty(gitBinPath)) - { - errorMessage = $"nameof(this.TryInitialize): Unable to locate git installation. Ensure git is installed and try again."; - return false; - } - - ICredentialStore credentialStore = new GitProcess(gitBinPath, workingDirectoryRoot: null); - - ProductUpgrader upgrader; - if (!ProductUpgrader.TryCreateUpgrader(this.tracer, this.fileSystem, new LocalScalarConfig(), credentialStore, this.DryRun, this.NoVerify, out upgrader, out errorMessage)) - { - return false; - } - - // Configure the upgrader to have installer logs written to the same directory - // as the upgrader. - upgrader.UpgradeInstanceId = this.installationId; - this.upgrader = upgrader; - } - - errorMessage = null; - return true; - } - - private bool TryRunUpgrade(out Version newVersion, out string consoleError) - { - Version newScalarVersion = null; - string error = null; - - if (!this.upgrader.UpgradeAllowed(out error)) - { - ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( - this.tracer, - this.fileSystem); - productUpgraderInfo.DeleteAllInstallerDownloads(); - this.output.WriteLine(error); - consoleError = null; - newVersion = null; - return true; - } - - if (!this.LaunchInsideSpinner( - () => - { - if (!this.preRunChecker.TryRunPreUpgradeChecks(out error)) - { - return false; - } - - if (!this.TryCheckIfUpgradeAvailable(out newScalarVersion, out error)) - { - return false; - } - - this.LogInstalledVersionInfo(); - - if (newScalarVersion != null && !this.TryDownloadUpgrade(newScalarVersion, out error)) - { - return false; - } - - return true; - }, - "Downloading")) - { - newVersion = null; - consoleError = error; - return false; - } - - if (newScalarVersion == null) - { - newVersion = null; - consoleError = null; - return true; - } - - if (!this.LaunchInsideSpinner( - () => - { - if (!this.preRunChecker.TryUnmountAllScalarRepos(out error)) - { - return false; - } - - this.mount = true; - - return true; - }, - "Unmounting repositories")) - { - newVersion = null; - consoleError = error; - return false; - } - - if (!this.LaunchInsideSpinner( - () => - { - if (!this.preRunChecker.IsInstallationBlockedByRunningProcess(out error)) - { - return false; - } - - return true; - }, - "Checking for blocking processes.")) - { - newVersion = null; - consoleError = error; - return false; - } - - if (!this.upgrader.TryRunInstaller(this.LaunchInsideSpinner, out consoleError)) - { - newVersion = null; - return false; - } - - newVersion = newScalarVersion; - consoleError = null; - return true; - } - - private void DeletedDownloadedAssets() - { - string downloadsCleanupError; - if (!this.upgrader.TryCleanup(out downloadsCleanupError)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Upgrade Step", nameof(this.DeletedDownloadedAssets)); - metadata.Add("Download cleanup error", downloadsCleanupError); - this.tracer.RelatedError(metadata, $"{nameof(this.DeletedDownloadedAssets)} failed."); - } - } - - private bool TryCheckIfUpgradeAvailable(out Version newestVersion, out string consoleError) - { - newestVersion = null; - consoleError = null; - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckIfUpgradeAvailable), EventLevel.Informational)) - { - string message; - if (!this.upgrader.TryQueryNewestVersion(out newestVersion, out message)) - { - consoleError = message; - EventMetadata metadata = new EventMetadata(); - metadata.Add("Upgrade Step", nameof(this.TryCheckIfUpgradeAvailable)); - this.tracer.RelatedError(metadata, $"{nameof(this.upgrader.TryQueryNewestVersion)} failed. {consoleError}"); - return false; - } - - if (newestVersion == null) - { - this.output.WriteLine(message); - this.tracer.RelatedInfo($"No new upgrade releases available. {message}"); - return true; - } - - activity.RelatedInfo("New release found - latest available version: {0}", newestVersion); - } - - return true; - } - - private bool TryDownloadUpgrade(Version version, out string consoleError) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Upgrade Step", nameof(this.TryDownloadUpgrade)); - metadata.Add("Version", version.ToString()); - - using (ITracer activity = this.tracer.StartActivity($"{nameof(this.TryDownloadUpgrade)}", EventLevel.Informational, metadata)) - { - if (!this.upgrader.TryDownloadNewestVersion(out consoleError)) - { - this.tracer.RelatedError(metadata, $"{nameof(this.upgrader.TryDownloadNewestVersion)} failed. {consoleError}"); - return false; - } - - activity.RelatedInfo("Successfully downloaded version: " + version.ToString()); - } - - return true; - } - - private void LogInstalledVersionInfo() - { - EventMetadata metadata = new EventMetadata(); - string installedScalarVersion = ProcessHelper.GetCurrentProcessVersion(); - metadata.Add(nameof(installedScalarVersion), installedScalarVersion); - - GitVersion installedGitVersion = null; - string error = null; - string gitPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (!string.IsNullOrEmpty(gitPath) && GitProcess.TryGetVersion(gitPath, out installedGitVersion, out error)) - { - metadata.Add(nameof(installedGitVersion), installedGitVersion.ToString()); - } - - this.tracer.RelatedEvent(EventLevel.Informational, "Installed Version", metadata); - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.IO; +using System.Text; + +namespace Scalar.Upgrader +{ + public abstract class UpgradeOrchestrator + { + protected InstallerPreRunChecker preRunChecker; + protected bool mount; + protected ITracer tracer; + + private const EventLevel DefaultEventLevel = EventLevel.Informational; + + private ProductUpgrader upgrader; + private string logDirectory = ProductUpgraderInfo.GetLogDirectoryPath(); + private string installationId; + private PhysicalFileSystem fileSystem; + private TextWriter output; + private TextReader input; + + public UpgradeOrchestrator( + ProductUpgrader upgrader, + ITracer tracer, + PhysicalFileSystem fileSystem, + InstallerPreRunChecker preRunChecker, + TextReader input, + TextWriter output) + { + this.upgrader = upgrader; + this.tracer = tracer; + this.fileSystem = fileSystem; + this.preRunChecker = preRunChecker; + this.output = output; + this.input = input; + this.mount = false; + this.ExitCode = ReturnCode.Success; + this.installationId = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + } + + public UpgradeOrchestrator(UpgradeOptions options) + : this() + { + this.DryRun = options.DryRun; + this.NoVerify = options.NoVerify; + } + + public UpgradeOrchestrator() + { + // CommandLine's Parser will create multiple instances of UpgradeOrchestrator, and we don't want + // multiple log files to get created. Defer tracer (and preRunChecker) creation until Execute() + this.tracer = null; + this.preRunChecker = null; + + this.fileSystem = new PhysicalFileSystem(); + this.output = Console.Out; + this.input = Console.In; + this.mount = false; + this.ExitCode = ReturnCode.Success; + this.installationId = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + } + + public ReturnCode ExitCode { get; private set; } + + public bool DryRun { get; } + + public bool NoVerify { get; } + + public void Execute() + { + string error = null; + string mountError = null; + Version newVersion = null; + + if (this.tracer == null) + { + this.tracer = this.CreateTracer(); + } + + if (this.preRunChecker == null) + { + this.preRunChecker = new InstallerPreRunChecker(this.tracer, ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm); + } + + try + { + if (this.TryInitialize(out error)) + { + try + { + if (!this.TryRunUpgrade(out newVersion, out error)) + { + this.ExitCode = ReturnCode.GenericError; + } + } + finally + { + if (!this.TryMountRepositories(out mountError)) + { + mountError = Environment.NewLine + "WARNING: " + mountError; + this.output.WriteLine(mountError); + } + + this.DeletedDownloadedAssets(); + } + } + else + { + this.ExitCode = ReturnCode.GenericError; + } + + if (this.ExitCode == ReturnCode.GenericError) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + sb.Append("ERROR: " + error); + + sb.AppendLine(); + sb.AppendLine(); + + sb.AppendLine($"Upgrade logs can be found at: {this.logDirectory} with file names that end with the installation ID: {this.installationId}."); + + this.output.WriteLine(sb.ToString()); + } + else + { + if (newVersion != null) + { + this.output.WriteLine($"{Environment.NewLine}Upgrade completed successfully{(string.IsNullOrEmpty(mountError) ? "." : ", but one or more repositories will need to be mounted manually.")}"); + } + } + } + finally + { + this.upgrader?.Dispose(); + } + + if (this.input == Console.In) + { + this.output.WriteLine("Press Enter to exit."); + this.input.ReadLine(); + } + + Environment.ExitCode = (int)this.ExitCode; + } + + protected bool LaunchInsideSpinner(Func method, string message) + { + return ConsoleHelper.ShowStatusWhileRunning( + method, + message, + this.output, + this.output == Console.Out && !ScalarPlatform.Instance.IsConsoleOutputRedirectedToFile(), + null); + } + + protected abstract bool TryMountRepositories(out string consoleError); + + private JsonTracer CreateTracer() + { + string logFilePath = ScalarEnlistment.GetNewScalarLogFileName( + this.logDirectory, + ScalarConstants.LogFileTypes.UpgradeProcess, + this.fileSystem); + + JsonTracer jsonTracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "UpgradeProcess"); + + jsonTracer.AddLogFileEventListener( + logFilePath, + DefaultEventLevel, + Keywords.Any); + + return jsonTracer; + } + + private bool TryInitialize(out string errorMessage) + { + if (this.upgrader == null) + { + string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (string.IsNullOrEmpty(gitBinPath)) + { + errorMessage = $"nameof(this.TryInitialize): Unable to locate git installation. Ensure git is installed and try again."; + return false; + } + + ICredentialStore credentialStore = new GitProcess(gitBinPath, workingDirectoryRoot: null); + + ProductUpgrader upgrader; + if (!ProductUpgrader.TryCreateUpgrader(this.tracer, this.fileSystem, new LocalScalarConfig(), credentialStore, this.DryRun, this.NoVerify, out upgrader, out errorMessage)) + { + return false; + } + + // Configure the upgrader to have installer logs written to the same directory + // as the upgrader. + upgrader.UpgradeInstanceId = this.installationId; + this.upgrader = upgrader; + } + + errorMessage = null; + return true; + } + + private bool TryRunUpgrade(out Version newVersion, out string consoleError) + { + Version newScalarVersion = null; + string error = null; + + if (!this.upgrader.UpgradeAllowed(out error)) + { + ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( + this.tracer, + this.fileSystem); + productUpgraderInfo.DeleteAllInstallerDownloads(); + this.output.WriteLine(error); + consoleError = null; + newVersion = null; + return true; + } + + if (!this.LaunchInsideSpinner( + () => + { + if (!this.preRunChecker.TryRunPreUpgradeChecks(out error)) + { + return false; + } + + if (!this.TryCheckIfUpgradeAvailable(out newScalarVersion, out error)) + { + return false; + } + + this.LogInstalledVersionInfo(); + + if (newScalarVersion != null && !this.TryDownloadUpgrade(newScalarVersion, out error)) + { + return false; + } + + return true; + }, + "Downloading")) + { + newVersion = null; + consoleError = error; + return false; + } + + if (newScalarVersion == null) + { + newVersion = null; + consoleError = null; + return true; + } + + if (!this.LaunchInsideSpinner( + () => + { + if (!this.preRunChecker.TryUnmountAllScalarRepos(out error)) + { + return false; + } + + this.mount = true; + + return true; + }, + "Unmounting repositories")) + { + newVersion = null; + consoleError = error; + return false; + } + + if (!this.LaunchInsideSpinner( + () => + { + if (!this.preRunChecker.IsInstallationBlockedByRunningProcess(out error)) + { + return false; + } + + return true; + }, + "Checking for blocking processes.")) + { + newVersion = null; + consoleError = error; + return false; + } + + if (!this.upgrader.TryRunInstaller(this.LaunchInsideSpinner, out consoleError)) + { + newVersion = null; + return false; + } + + newVersion = newScalarVersion; + consoleError = null; + return true; + } + + private void DeletedDownloadedAssets() + { + string downloadsCleanupError; + if (!this.upgrader.TryCleanup(out downloadsCleanupError)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Upgrade Step", nameof(this.DeletedDownloadedAssets)); + metadata.Add("Download cleanup error", downloadsCleanupError); + this.tracer.RelatedError(metadata, $"{nameof(this.DeletedDownloadedAssets)} failed."); + } + } + + private bool TryCheckIfUpgradeAvailable(out Version newestVersion, out string consoleError) + { + newestVersion = null; + consoleError = null; + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckIfUpgradeAvailable), EventLevel.Informational)) + { + string message; + if (!this.upgrader.TryQueryNewestVersion(out newestVersion, out message)) + { + consoleError = message; + EventMetadata metadata = new EventMetadata(); + metadata.Add("Upgrade Step", nameof(this.TryCheckIfUpgradeAvailable)); + this.tracer.RelatedError(metadata, $"{nameof(this.upgrader.TryQueryNewestVersion)} failed. {consoleError}"); + return false; + } + + if (newestVersion == null) + { + this.output.WriteLine(message); + this.tracer.RelatedInfo($"No new upgrade releases available. {message}"); + return true; + } + + activity.RelatedInfo("New release found - latest available version: {0}", newestVersion); + } + + return true; + } + + private bool TryDownloadUpgrade(Version version, out string consoleError) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Upgrade Step", nameof(this.TryDownloadUpgrade)); + metadata.Add("Version", version.ToString()); + + using (ITracer activity = this.tracer.StartActivity($"{nameof(this.TryDownloadUpgrade)}", EventLevel.Informational, metadata)) + { + if (!this.upgrader.TryDownloadNewestVersion(out consoleError)) + { + this.tracer.RelatedError(metadata, $"{nameof(this.upgrader.TryDownloadNewestVersion)} failed. {consoleError}"); + return false; + } + + activity.RelatedInfo("Successfully downloaded version: " + version.ToString()); + } + + return true; + } + + private void LogInstalledVersionInfo() + { + EventMetadata metadata = new EventMetadata(); + string installedScalarVersion = ProcessHelper.GetCurrentProcessVersion(); + metadata.Add(nameof(installedScalarVersion), installedScalarVersion); + + GitVersion installedGitVersion = null; + string error = null; + string gitPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (!string.IsNullOrEmpty(gitPath) && GitProcess.TryGetVersion(gitPath, out installedGitVersion, out error)) + { + metadata.Add(nameof(installedGitVersion), installedGitVersion.ToString()); + } + + this.tracer.RelatedEvent(EventLevel.Informational, "Installed Version", metadata); + } + } +} diff --git a/Scalar.Upgrader/WindowsUpgradeOrchestrator.cs b/Scalar.Upgrader/WindowsUpgradeOrchestrator.cs index 444af72f9f..184c723691 100644 --- a/Scalar.Upgrader/WindowsUpgradeOrchestrator.cs +++ b/Scalar.Upgrader/WindowsUpgradeOrchestrator.cs @@ -1,55 +1,55 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System.IO; - -namespace Scalar.Upgrader -{ - public class WindowsUpgradeOrchestrator : UpgradeOrchestrator - { - public WindowsUpgradeOrchestrator( - ProductUpgrader upgrader, - ITracer tracer, - PhysicalFileSystem fileSystem, - InstallerPreRunChecker preRunChecker, - TextReader input, - TextWriter output) - : base(upgrader, tracer, fileSystem, preRunChecker, input, output) - { - } - - public WindowsUpgradeOrchestrator(UpgradeOptions options) - : base(options) - { - } - - protected override bool TryMountRepositories(out string consoleError) - { - string errorMessage = string.Empty; - if (this.mount && !this.LaunchInsideSpinner( - () => - { - string mountError; - if (!this.preRunChecker.TryMountAllScalarRepos(out mountError)) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Upgrade Step", nameof(this.TryMountRepositories)); - metadata.Add("Mount Error", mountError); - this.tracer.RelatedError(metadata, $"{nameof(this.preRunChecker.TryMountAllScalarRepos)} failed."); - errorMessage += mountError; - return false; - } - - return true; - }, - "Mounting repositories")) - { - consoleError = errorMessage; - return false; - } - - consoleError = null; - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System.IO; + +namespace Scalar.Upgrader +{ + public class WindowsUpgradeOrchestrator : UpgradeOrchestrator + { + public WindowsUpgradeOrchestrator( + ProductUpgrader upgrader, + ITracer tracer, + PhysicalFileSystem fileSystem, + InstallerPreRunChecker preRunChecker, + TextReader input, + TextWriter output) + : base(upgrader, tracer, fileSystem, preRunChecker, input, output) + { + } + + public WindowsUpgradeOrchestrator(UpgradeOptions options) + : base(options) + { + } + + protected override bool TryMountRepositories(out string consoleError) + { + string errorMessage = string.Empty; + if (this.mount && !this.LaunchInsideSpinner( + () => + { + string mountError; + if (!this.preRunChecker.TryMountAllScalarRepos(out mountError)) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Upgrade Step", nameof(this.TryMountRepositories)); + metadata.Add("Mount Error", mountError); + this.tracer.RelatedError(metadata, $"{nameof(this.preRunChecker.TryMountAllScalarRepos)} failed."); + errorMessage += mountError; + return false; + } + + return true; + }, + "Mounting repositories")) + { + consoleError = errorMessage; + return false; + } + + consoleError = null; + return true; + } + } +} diff --git a/Scalar.Upgrader/packages.config b/Scalar.Upgrader/packages.config index e8b8dc9514..3bdbcd90e8 100644 --- a/Scalar.Upgrader/packages.config +++ b/Scalar.Upgrader/packages.config @@ -1,18 +1,18 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/Scalar.sln b/Scalar.sln index d91a2713a0..beb929f081 100644 --- a/Scalar.sln +++ b/Scalar.sln @@ -1,308 +1,308 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.27428.2015 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DCE11095-DA5F-4878-B58D-2702765560F5}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig - .gitattributes = .gitattributes - .gitignore = .gitignore - AuthoringTests.md = AuthoringTests.md - nuget.config = nuget.config - Protocol.md = Protocol.md - Readme.md = Readme.md - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scalar", "Scalar", "{2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C}" - ProjectSection(SolutionItems) = preProject - Scalar.Build\LibGit2Sharp.NativeBinaries.props = Scalar.Build\LibGit2Sharp.NativeBinaries.props - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Common", "Scalar.Common\Scalar.Common.csproj", "{374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}" - ProjectSection(ProjectDependencies) = postProject - {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Tests", "Scalar.Tests\Scalar.Tests.csproj", "{72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}" - ProjectSection(ProjectDependencies) = postProject - {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scalar Tests", "Scalar Tests", "{C41F10F9-1163-4CFA-A465-EA728F75E9FA}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.UnitTests.Windows", "Scalar.UnitTests.Windows\Scalar.UnitTests.Windows.csproj", "{8E0D0989-21F6-4DD8-946C-39F992523CC6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Service.Windows", "Scalar.Service\Scalar.Service.Windows.csproj", "{B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}" - ProjectSection(ProjectDependencies) = postProject - {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Service.Mac", "Scalar.Service\Scalar.Service.Mac.csproj", "{03769A07-F216-456B-886B-E07CAF6C5E81}" -EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Scalar.ReadObjectHook.Windows", "Scalar.ReadObjectHook\Scalar.ReadObjectHook.Windows.vcxproj", "{5A6656D5-81C7-472C-9DC8-32D071CB2258}" - ProjectSection(ProjectDependencies) = postProject - {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} - EndProjectSection -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{28674A4B-1223-4633-A460-C8CC39B09318}" - ProjectSection(SolutionItems) = preProject - Scripts\CreateCommonAssemblyVersion.bat = Scripts\CreateCommonAssemblyVersion.bat - Scripts\CreateCommonCliAssemblyVersion.bat = Scripts\CreateCommonCliAssemblyVersion.bat - Scripts\CreateCommonVersionHeader.bat = Scripts\CreateCommonVersionHeader.bat - Scripts\RunFunctionalTests.bat = Scripts\RunFunctionalTests.bat - Scripts\RunUnitTests.bat = Scripts\RunUnitTests.bat - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Service.UI", "Scalar.Service.UI\Scalar.Service.UI.csproj", "{93B403FD-DAFB-46C5-9636-B122792A548A}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.PreBuild", "Scalar.Build\Scalar.PreBuild.csproj", "{A4984251-840E-4622-AD0C-66DFCE2B2574}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{AB0D9230-3893-4486-8899-F9C871FB5D5F}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Installer.Windows", "Scalar.Installer.Windows\Scalar.Installer.Windows.csproj", "{3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}" - ProjectSection(ProjectDependencies) = postProject - {2F63B22B-EE26-4266-BF17-28A9146483A1} = {2F63B22B-EE26-4266-BF17-28A9146483A1} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Installer.Mac", "Scalar.Installer.Mac\Scalar.Installer.Mac.csproj", "{25229A04-6554-49B1-A95A-3F3B76C5B0C8}" - ProjectSection(ProjectDependencies) = postProject - {03769A07-F216-456B-886B-E07CAF6C5E81} = {03769A07-F216-456B-886B-E07CAF6C5E81} - {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {28939122-7263-41E7-A7E2-CBFB01AD6A04} - {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} = {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} - {AECEC217-2499-403D-B0BB-2962B9BE5970} = {AECEC217-2499-403D-B0BB-2962B9BE5970} - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.SignFiles", "Scalar.SignFiles\Scalar.SignFiles.csproj", "{2F63B22B-EE26-4266-BF17-28A9146483A1}" - ProjectSection(ProjectDependencies) = postProject - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} - {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {32220664-594C-4425-B9A0-88E0BE2F3D2A} - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} - {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} - {4CE404E7-D3FC-471C-993C-64615861EA63} = {4CE404E7-D3FC-471C-993C-64615861EA63} - {93B403FD-DAFB-46C5-9636-B122792A548A} = {93B403FD-DAFB-46C5-9636-B122792A548A} - {AECEC217-2499-403D-B0BB-2962B9BE5970} = {AECEC217-2499-403D-B0BB-2962B9BE5970} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Platform.Mac", "Scalar.Platform.Mac\Scalar.Platform.Mac.csproj", "{1DAC3DA6-3D21-4917-B9A8-D60C8712252A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Platform.POSIX", "Scalar.Platform.POSIX\Scalar.Platform.POSIX.csproj", "{15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.UnitTests", "Scalar.UnitTests\Scalar.UnitTests.csproj", "{0D434FA7-6D8C-481E-B0CE-779B59EAEF53}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Platform.Windows", "Scalar.Platform.Windows\Scalar.Platform.Windows.csproj", "{4CE404E7-D3FC-471C-993C-64615861EA63}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Mac", "Scalar\Scalar.Mac.csproj", "{28939122-7263-41E7-A7E2-CBFB01AD6A04}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Windows", "Scalar\Scalar.Windows.csproj", "{32220664-594C-4425-B9A0-88E0BE2F3D2A}" - ProjectSection(ProjectDependencies) = postProject - {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Mount.Windows", "Scalar.Mount\Scalar.Mount.Windows.csproj", "{17498502-AEFF-4E70-90CC-1D0B56A8ADF5}" - ProjectSection(ProjectDependencies) = postProject - {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Mount.Mac", "Scalar.Mount\Scalar.Mount.Mac.csproj", "{35CA4DFB-1320-4055-B8F6-F12E0F252FF0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.FunctionalTests", "Scalar.FunctionalTests\Scalar.FunctionalTests.csproj", "{BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}" - ProjectSection(ProjectDependencies) = postProject - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} - {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {28939122-7263-41E7-A7E2-CBFB01AD6A04} - {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {32220664-594C-4425-B9A0-88E0BE2F3D2A} - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} - EndProjectSection -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Upgrader", "Scalar.Upgrader\Scalar.Upgrader.csproj", "{AECEC217-2499-403D-B0BB-2962B9BE5970}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug.Mac|x64 = Debug.Mac|x64 - Debug.Windows|x64 = Debug.Windows|x64 - Release.Mac|x64 = Release.Mac|x64 - Release.Windows|x64 = Release.Windows|x64 - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Mac|x64.Build.0 = Debug|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Windows|x64.Build.0 = Debug|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Mac|x64.ActiveCfg = Release|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Mac|x64.Build.0 = Release|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Windows|x64.ActiveCfg = Release|x64 - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Windows|x64.Build.0 = Release|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Mac|x64.Build.0 = Debug|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Windows|x64.Build.0 = Debug|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Mac|x64.ActiveCfg = Release|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Mac|x64.Build.0 = Release|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Windows|x64.ActiveCfg = Release|x64 - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Windows|x64.Build.0 = Release|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Windows|x64.Build.0 = Debug|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Mac|x64.ActiveCfg = Release|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Windows|x64.ActiveCfg = Release|x64 - {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Windows|x64.Build.0 = Release|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Windows|x64.Build.0 = Debug|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Mac|x64.ActiveCfg = Release|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Windows|x64.ActiveCfg = Release|x64 - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Windows|x64.Build.0 = Release|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Mac|x64.Build.0 = Debug|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Windows|x64.Build.0 = Debug|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Mac|x64.ActiveCfg = Release|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Mac|x64.Build.0 = Release|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Windows|x64.ActiveCfg = Release|x64 - {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Windows|x64.Build.0 = Release|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Windows|x64.Build.0 = Debug|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Mac|x64.ActiveCfg = Release|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Windows|x64.ActiveCfg = Release|x64 - {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Windows|x64.Build.0 = Release|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Windows|x64.Build.0 = Debug|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Mac|x64.ActiveCfg = Release|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Windows|x64.ActiveCfg = Release|x64 - {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Windows|x64.Build.0 = Release|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Windows|x64.Build.0 = Debug|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Mac|x64.ActiveCfg = Release|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Windows|x64.ActiveCfg = Release|x64 - {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Windows|x64.Build.0 = Release|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Windows|x64.Build.0 = Debug|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Mac|x64.ActiveCfg = Release|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Windows|x64.ActiveCfg = Release|x64 - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Windows|x64.Build.0 = Release|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Mac|x64.Build.0 = Debug|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Mac|x64.ActiveCfg = Release|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Mac|x64.Build.0 = Release|x64 - {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Windows|x64.ActiveCfg = Release|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Windows|x64.Build.0 = Debug|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Mac|x64.ActiveCfg = Release|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Windows|x64.ActiveCfg = Release|x64 - {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Windows|x64.Build.0 = Release|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Mac|x64.Build.0 = Debug|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Windows|x64.Build.0 = Debug|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Mac|x64.ActiveCfg = Release|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Mac|x64.Build.0 = Release|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Windows|x64.ActiveCfg = Release|x64 - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Windows|x64.Build.0 = Release|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Mac|x64.Build.0 = Debug|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Windows|x64.Build.0 = Debug|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Mac|x64.ActiveCfg = Release|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Mac|x64.Build.0 = Release|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Windows|x64.ActiveCfg = Release|x64 - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Windows|x64.Build.0 = Release|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Mac|x64.Build.0 = Debug|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Windows|x64.Build.0 = Debug|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Mac|x64.ActiveCfg = Release|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Mac|x64.Build.0 = Release|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Windows|x64.ActiveCfg = Release|x64 - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Windows|x64.Build.0 = Release|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Windows|x64.Build.0 = Debug|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Mac|x64.ActiveCfg = Release|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Windows|x64.ActiveCfg = Release|x64 - {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Windows|x64.Build.0 = Release|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Mac|x64.Build.0 = Debug|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Windows|x64.Build.0 = Debug|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Mac|x64.ActiveCfg = Release|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Mac|x64.Build.0 = Release|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Windows|x64.ActiveCfg = Release|x64 - {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Windows|x64.Build.0 = Release|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Windows|x64.Build.0 = Debug|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Mac|x64.ActiveCfg = Release|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Windows|x64.ActiveCfg = Release|x64 - {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Windows|x64.Build.0 = Release|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Windows|x64.Build.0 = Debug|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Mac|x64.ActiveCfg = Release|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Windows|x64.ActiveCfg = Release|x64 - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Windows|x64.Build.0 = Release|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Mac|x64.Build.0 = Debug|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Windows|x64.Build.0 = Debug|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Mac|x64.ActiveCfg = Release|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Mac|x64.Build.0 = Release|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Windows|x64.ActiveCfg = Release|x64 - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Windows|x64.Build.0 = Release|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Mac|x64.Build.0 = Debug|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Windows|x64.Build.0 = Debug|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Mac|x64.ActiveCfg = Release|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Mac|x64.Build.0 = Release|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Windows|x64.ActiveCfg = Release|x64 - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Windows|x64.Build.0 = Release|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Mac|x64.ActiveCfg = Debug|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Windows|x64.ActiveCfg = Debug|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Windows|x64.Build.0 = Debug|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Mac|x64.ActiveCfg = Release|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Windows|x64.ActiveCfg = Release|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Windows|x64.Build.0 = Release|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Mac|x64.Build.0 = Debug|x64 - {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Mac|x64.Build.0 = Release|x64 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} - {8E0D0989-21F6-4DD8-946C-39F992523CC6} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} - {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {03769A07-F216-456B-886B-E07CAF6C5E81} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {28674A4B-1223-4633-A460-C8CC39B09318} = {DCE11095-DA5F-4878-B58D-2702765560F5} - {93B403FD-DAFB-46C5-9636-B122792A548A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {A4984251-840E-4622-AD0C-66DFCE2B2574} = {AB0D9230-3893-4486-8899-F9C871FB5D5F} - {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {25229A04-6554-49B1-A95A-3F3B76C5B0C8} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {2F63B22B-EE26-4266-BF17-28A9146483A1} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {0D434FA7-6D8C-481E-B0CE-779B59EAEF53} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} - {4CE404E7-D3FC-471C-993C-64615861EA63} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} - {AECEC217-2499-403D-B0BB-2962B9BE5970} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {A025908B-DAB1-46CB-83A3-56F3B863D8FA} - EndGlobalSection -EndGlobal +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.27428.2015 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{DCE11095-DA5F-4878-B58D-2702765560F5}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + AuthoringTests.md = AuthoringTests.md + nuget.config = nuget.config + Protocol.md = Protocol.md + Readme.md = Readme.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scalar", "Scalar", "{2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C}" + ProjectSection(SolutionItems) = preProject + Scalar.Build\LibGit2Sharp.NativeBinaries.props = Scalar.Build\LibGit2Sharp.NativeBinaries.props + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Common", "Scalar.Common\Scalar.Common.csproj", "{374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}" + ProjectSection(ProjectDependencies) = postProject + {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Tests", "Scalar.Tests\Scalar.Tests.csproj", "{72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}" + ProjectSection(ProjectDependencies) = postProject + {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scalar Tests", "Scalar Tests", "{C41F10F9-1163-4CFA-A465-EA728F75E9FA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.UnitTests.Windows", "Scalar.UnitTests.Windows\Scalar.UnitTests.Windows.csproj", "{8E0D0989-21F6-4DD8-946C-39F992523CC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Service.Windows", "Scalar.Service\Scalar.Service.Windows.csproj", "{B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}" + ProjectSection(ProjectDependencies) = postProject + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Service.Mac", "Scalar.Service\Scalar.Service.Mac.csproj", "{03769A07-F216-456B-886B-E07CAF6C5E81}" +EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Scalar.ReadObjectHook.Windows", "Scalar.ReadObjectHook\Scalar.ReadObjectHook.Windows.vcxproj", "{5A6656D5-81C7-472C-9DC8-32D071CB2258}" + ProjectSection(ProjectDependencies) = postProject + {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Scripts", "Scripts", "{28674A4B-1223-4633-A460-C8CC39B09318}" + ProjectSection(SolutionItems) = preProject + Scripts\CreateCommonAssemblyVersion.bat = Scripts\CreateCommonAssemblyVersion.bat + Scripts\CreateCommonCliAssemblyVersion.bat = Scripts\CreateCommonCliAssemblyVersion.bat + Scripts\CreateCommonVersionHeader.bat = Scripts\CreateCommonVersionHeader.bat + Scripts\RunFunctionalTests.bat = Scripts\RunFunctionalTests.bat + Scripts\RunUnitTests.bat = Scripts\RunUnitTests.bat + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Service.UI", "Scalar.Service.UI\Scalar.Service.UI.csproj", "{93B403FD-DAFB-46C5-9636-B122792A548A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.PreBuild", "Scalar.Build\Scalar.PreBuild.csproj", "{A4984251-840E-4622-AD0C-66DFCE2B2574}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{AB0D9230-3893-4486-8899-F9C871FB5D5F}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Installer.Windows", "Scalar.Installer.Windows\Scalar.Installer.Windows.csproj", "{3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}" + ProjectSection(ProjectDependencies) = postProject + {2F63B22B-EE26-4266-BF17-28A9146483A1} = {2F63B22B-EE26-4266-BF17-28A9146483A1} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Installer.Mac", "Scalar.Installer.Mac\Scalar.Installer.Mac.csproj", "{25229A04-6554-49B1-A95A-3F3B76C5B0C8}" + ProjectSection(ProjectDependencies) = postProject + {03769A07-F216-456B-886B-E07CAF6C5E81} = {03769A07-F216-456B-886B-E07CAF6C5E81} + {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {28939122-7263-41E7-A7E2-CBFB01AD6A04} + {A4984251-840E-4622-AD0C-66DFCE2B2574} = {A4984251-840E-4622-AD0C-66DFCE2B2574} + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} = {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} + {AECEC217-2499-403D-B0BB-2962B9BE5970} = {AECEC217-2499-403D-B0BB-2962B9BE5970} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.SignFiles", "Scalar.SignFiles\Scalar.SignFiles.csproj", "{2F63B22B-EE26-4266-BF17-28A9146483A1}" + ProjectSection(ProjectDependencies) = postProject + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} + {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {32220664-594C-4425-B9A0-88E0BE2F3D2A} + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} + {4CE404E7-D3FC-471C-993C-64615861EA63} = {4CE404E7-D3FC-471C-993C-64615861EA63} + {93B403FD-DAFB-46C5-9636-B122792A548A} = {93B403FD-DAFB-46C5-9636-B122792A548A} + {AECEC217-2499-403D-B0BB-2962B9BE5970} = {AECEC217-2499-403D-B0BB-2962B9BE5970} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Platform.Mac", "Scalar.Platform.Mac\Scalar.Platform.Mac.csproj", "{1DAC3DA6-3D21-4917-B9A8-D60C8712252A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Platform.POSIX", "Scalar.Platform.POSIX\Scalar.Platform.POSIX.csproj", "{15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.UnitTests", "Scalar.UnitTests\Scalar.UnitTests.csproj", "{0D434FA7-6D8C-481E-B0CE-779B59EAEF53}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Platform.Windows", "Scalar.Platform.Windows\Scalar.Platform.Windows.csproj", "{4CE404E7-D3FC-471C-993C-64615861EA63}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Mac", "Scalar\Scalar.Mac.csproj", "{28939122-7263-41E7-A7E2-CBFB01AD6A04}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Windows", "Scalar\Scalar.Windows.csproj", "{32220664-594C-4425-B9A0-88E0BE2F3D2A}" + ProjectSection(ProjectDependencies) = postProject + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + EndProjectSection +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Scalar.Mount.Windows", "Scalar.Mount\Scalar.Mount.Windows.csproj", "{17498502-AEFF-4E70-90CC-1D0B56A8ADF5}" + ProjectSection(ProjectDependencies) = postProject + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {5A6656D5-81C7-472C-9DC8-32D071CB2258} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Mount.Mac", "Scalar.Mount\Scalar.Mount.Mac.csproj", "{35CA4DFB-1320-4055-B8F6-F12E0F252FF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.FunctionalTests", "Scalar.FunctionalTests\Scalar.FunctionalTests.csproj", "{BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}" + ProjectSection(ProjectDependencies) = postProject + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} + {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {28939122-7263-41E7-A7E2-CBFB01AD6A04} + {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {32220664-594C-4425-B9A0-88E0BE2F3D2A} + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Scalar.Upgrader", "Scalar.Upgrader\Scalar.Upgrader.csproj", "{AECEC217-2499-403D-B0BB-2962B9BE5970}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug.Mac|x64 = Debug.Mac|x64 + Debug.Windows|x64 = Debug.Windows|x64 + Release.Mac|x64 = Release.Mac|x64 + Release.Windows|x64 = Release.Windows|x64 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Mac|x64.Build.0 = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Debug.Windows|x64.Build.0 = Debug|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Mac|x64.ActiveCfg = Release|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Mac|x64.Build.0 = Release|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Windows|x64.ActiveCfg = Release|x64 + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09}.Release.Windows|x64.Build.0 = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Mac|x64.Build.0 = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Debug.Windows|x64.Build.0 = Debug|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Mac|x64.ActiveCfg = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Mac|x64.Build.0 = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Windows|x64.ActiveCfg = Release|x64 + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC}.Release.Windows|x64.Build.0 = Release|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Debug.Windows|x64.Build.0 = Debug|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Mac|x64.ActiveCfg = Release|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Windows|x64.ActiveCfg = Release|x64 + {8E0D0989-21F6-4DD8-946C-39F992523CC6}.Release.Windows|x64.Build.0 = Release|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Debug.Windows|x64.Build.0 = Debug|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Mac|x64.ActiveCfg = Release|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Windows|x64.ActiveCfg = Release|x64 + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B}.Release.Windows|x64.Build.0 = Release|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Mac|x64.Build.0 = Debug|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Debug.Windows|x64.Build.0 = Debug|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Mac|x64.ActiveCfg = Release|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Mac|x64.Build.0 = Release|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Windows|x64.ActiveCfg = Release|x64 + {03769A07-F216-456B-886B-E07CAF6C5E81}.Release.Windows|x64.Build.0 = Release|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Debug.Windows|x64.Build.0 = Debug|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Mac|x64.ActiveCfg = Release|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Windows|x64.ActiveCfg = Release|x64 + {5A6656D5-81C7-472C-9DC8-32D071CB2258}.Release.Windows|x64.Build.0 = Release|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Debug.Windows|x64.Build.0 = Debug|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Mac|x64.ActiveCfg = Release|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Windows|x64.ActiveCfg = Release|x64 + {93B403FD-DAFB-46C5-9636-B122792A548A}.Release.Windows|x64.Build.0 = Release|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Debug.Windows|x64.Build.0 = Debug|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Mac|x64.ActiveCfg = Release|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Windows|x64.ActiveCfg = Release|x64 + {A4984251-840E-4622-AD0C-66DFCE2B2574}.Release.Windows|x64.Build.0 = Release|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Debug.Windows|x64.Build.0 = Debug|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Mac|x64.ActiveCfg = Release|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Windows|x64.ActiveCfg = Release|x64 + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893}.Release.Windows|x64.Build.0 = Release|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Mac|x64.Build.0 = Debug|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Mac|x64.ActiveCfg = Release|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Mac|x64.Build.0 = Release|x64 + {25229A04-6554-49B1-A95A-3F3B76C5B0C8}.Release.Windows|x64.ActiveCfg = Release|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Debug.Windows|x64.Build.0 = Debug|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Mac|x64.ActiveCfg = Release|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Windows|x64.ActiveCfg = Release|x64 + {2F63B22B-EE26-4266-BF17-28A9146483A1}.Release.Windows|x64.Build.0 = Release|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Mac|x64.Build.0 = Debug|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Debug.Windows|x64.Build.0 = Debug|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Mac|x64.ActiveCfg = Release|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Mac|x64.Build.0 = Release|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Windows|x64.ActiveCfg = Release|x64 + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A}.Release.Windows|x64.Build.0 = Release|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Mac|x64.Build.0 = Debug|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Debug.Windows|x64.Build.0 = Debug|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Mac|x64.ActiveCfg = Release|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Mac|x64.Build.0 = Release|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Windows|x64.ActiveCfg = Release|x64 + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6}.Release.Windows|x64.Build.0 = Release|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Mac|x64.Build.0 = Debug|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Debug.Windows|x64.Build.0 = Debug|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Mac|x64.ActiveCfg = Release|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Mac|x64.Build.0 = Release|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Windows|x64.ActiveCfg = Release|x64 + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53}.Release.Windows|x64.Build.0 = Release|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Debug.Windows|x64.Build.0 = Debug|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Mac|x64.ActiveCfg = Release|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Windows|x64.ActiveCfg = Release|x64 + {4CE404E7-D3FC-471C-993C-64615861EA63}.Release.Windows|x64.Build.0 = Release|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Mac|x64.Build.0 = Debug|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Debug.Windows|x64.Build.0 = Debug|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Mac|x64.ActiveCfg = Release|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Mac|x64.Build.0 = Release|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Windows|x64.ActiveCfg = Release|x64 + {28939122-7263-41E7-A7E2-CBFB01AD6A04}.Release.Windows|x64.Build.0 = Release|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Debug.Windows|x64.Build.0 = Debug|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Mac|x64.ActiveCfg = Release|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Windows|x64.ActiveCfg = Release|x64 + {32220664-594C-4425-B9A0-88E0BE2F3D2A}.Release.Windows|x64.Build.0 = Release|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Debug.Windows|x64.Build.0 = Debug|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Mac|x64.ActiveCfg = Release|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Windows|x64.ActiveCfg = Release|x64 + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5}.Release.Windows|x64.Build.0 = Release|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Mac|x64.Build.0 = Debug|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Debug.Windows|x64.Build.0 = Debug|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Mac|x64.ActiveCfg = Release|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Mac|x64.Build.0 = Release|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Windows|x64.ActiveCfg = Release|x64 + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0}.Release.Windows|x64.Build.0 = Release|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Mac|x64.Build.0 = Debug|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Debug.Windows|x64.Build.0 = Debug|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Mac|x64.ActiveCfg = Release|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Mac|x64.Build.0 = Release|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Windows|x64.ActiveCfg = Release|x64 + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF}.Release.Windows|x64.Build.0 = Release|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Mac|x64.ActiveCfg = Debug|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Windows|x64.ActiveCfg = Debug|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Windows|x64.Build.0 = Debug|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Mac|x64.ActiveCfg = Release|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Windows|x64.ActiveCfg = Release|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Windows|x64.Build.0 = Release|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Debug.Mac|x64.Build.0 = Debug|x64 + {AECEC217-2499-403D-B0BB-2962B9BE5970}.Release.Mac|x64.Build.0 = Release|x64 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {374BF1E5-0B2D-4D4A-BD5E-4212299DEF09} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {72701BC3-5DA9-4C7A-BF10-9E98C9FC8EAC} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {8E0D0989-21F6-4DD8-946C-39F992523CC6} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {B8C1DFBA-CAFD-4F7E-A1A3-E11907B5467B} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {03769A07-F216-456B-886B-E07CAF6C5E81} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {5A6656D5-81C7-472C-9DC8-32D071CB2258} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {28674A4B-1223-4633-A460-C8CC39B09318} = {DCE11095-DA5F-4878-B58D-2702765560F5} + {93B403FD-DAFB-46C5-9636-B122792A548A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {A4984251-840E-4622-AD0C-66DFCE2B2574} = {AB0D9230-3893-4486-8899-F9C871FB5D5F} + {3AB4FB1F-9E23-4CD8-BFAC-8A2221C8F893} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {25229A04-6554-49B1-A95A-3F3B76C5B0C8} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {2F63B22B-EE26-4266-BF17-28A9146483A1} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {1DAC3DA6-3D21-4917-B9A8-D60C8712252A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {15FAE44C-0D21-4312-9FD3-28F05A5AB7A6} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {0D434FA7-6D8C-481E-B0CE-779B59EAEF53} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {4CE404E7-D3FC-471C-993C-64615861EA63} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {28939122-7263-41E7-A7E2-CBFB01AD6A04} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {32220664-594C-4425-B9A0-88E0BE2F3D2A} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {17498502-AEFF-4E70-90CC-1D0B56A8ADF5} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {35CA4DFB-1320-4055-B8F6-F12E0F252FF0} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + {BD7C5776-82F2-40C6-AF01-B3CC1E2D83AF} = {C41F10F9-1163-4CFA-A465-EA728F75E9FA} + {AECEC217-2499-403D-B0BB-2962B9BE5970} = {2EF2EC94-3A68-4ED7-9A58-B7057ADBA01C} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A025908B-DAB1-46CB-83A3-56F3B863D8FA} + EndGlobalSection +EndGlobal diff --git a/Scalar/App.config b/Scalar/App.config index d65b3ad80c..8cca790893 100644 --- a/Scalar/App.config +++ b/Scalar/App.config @@ -1,14 +1,14 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/Scalar/CommandLine/CacheServerVerb.cs b/Scalar/CommandLine/CacheServerVerb.cs index 6cc650943c..f21d7a946e 100644 --- a/Scalar/CommandLine/CacheServerVerb.cs +++ b/Scalar/CommandLine/CacheServerVerb.cs @@ -1,96 +1,96 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Scalar.CommandLine -{ - [Verb(CacheVerbName, HelpText = "Manages the cache server configuration for an existing repo.")] - public class CacheServerVerb : ScalarVerb.ForExistingEnlistment - { - private const string CacheVerbName = "cache-server"; - - [Option( - "set", - Default = null, - Required = false, - HelpText = "Sets the cache server to the supplied name or url")] - public string CacheToSet { get; set; } - - [Option("get", Required = false, HelpText = "Outputs the current cache server information. This is the default.")] - public bool OutputCurrentInfo { get; set; } - - [Option( - "list", - Required = false, - HelpText = "List available cache servers for the remote repo")] - public bool ListCacheServers { get; set; } - - protected override string VerbName - { - get { return CacheVerbName; } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - this.BlockEmptyCacheServerUrl(this.CacheToSet); - - RetryConfig retryConfig = new RetryConfig(RetryConfig.DefaultMaxRetries, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - - using (ITracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "CacheVerb")) - { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); - } - - ServerScalarConfig serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - string error = null; - - if (this.CacheToSet != null) - { - CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); - cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverScalarConfig); - - if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) - { - this.ReportErrorAndExit("Failed to save cache to config: " + error); - } - - this.Output.WriteLine("You must remount Scalar for this to take effect."); - } - else if (this.ListCacheServers) - { - List cacheServers = serverScalarConfig.CacheServers.ToList(); - - if (cacheServers != null && cacheServers.Any()) - { - this.Output.WriteLine(); - this.Output.WriteLine("Available cache servers for: " + enlistment.RepoUrl); - foreach (CacheServerInfo cacheServer in cacheServers) - { - this.Output.WriteLine(cacheServer); - } - } - else - { - this.Output.WriteLine("There are no available cache servers for: " + enlistment.RepoUrl); - } - } - else - { - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); - CacheServerInfo cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverScalarConfig); - - this.Output.WriteLine("Using cache server: " + cacheServer); - } - } - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Scalar.CommandLine +{ + [Verb(CacheVerbName, HelpText = "Manages the cache server configuration for an existing repo.")] + public class CacheServerVerb : ScalarVerb.ForExistingEnlistment + { + private const string CacheVerbName = "cache-server"; + + [Option( + "set", + Default = null, + Required = false, + HelpText = "Sets the cache server to the supplied name or url")] + public string CacheToSet { get; set; } + + [Option("get", Required = false, HelpText = "Outputs the current cache server information. This is the default.")] + public bool OutputCurrentInfo { get; set; } + + [Option( + "list", + Required = false, + HelpText = "List available cache servers for the remote repo")] + public bool ListCacheServers { get; set; } + + protected override string VerbName + { + get { return CacheVerbName; } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + this.BlockEmptyCacheServerUrl(this.CacheToSet); + + RetryConfig retryConfig = new RetryConfig(RetryConfig.DefaultMaxRetries, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); + + using (ITracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "CacheVerb")) + { + string authErrorMessage; + if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) + { + this.ReportErrorAndExit(tracer, "Authentication failed: " + authErrorMessage); + } + + ServerScalarConfig serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); + + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + string error = null; + + if (this.CacheToSet != null) + { + CacheServerInfo cacheServer = cacheServerResolver.ParseUrlOrFriendlyName(this.CacheToSet); + cacheServer = this.ResolveCacheServer(tracer, cacheServer, cacheServerResolver, serverScalarConfig); + + if (!cacheServerResolver.TrySaveUrlToLocalConfig(cacheServer, out error)) + { + this.ReportErrorAndExit("Failed to save cache to config: " + error); + } + + this.Output.WriteLine("You must remount Scalar for this to take effect."); + } + else if (this.ListCacheServers) + { + List cacheServers = serverScalarConfig.CacheServers.ToList(); + + if (cacheServers != null && cacheServers.Any()) + { + this.Output.WriteLine(); + this.Output.WriteLine("Available cache servers for: " + enlistment.RepoUrl); + foreach (CacheServerInfo cacheServer in cacheServers) + { + this.Output.WriteLine(cacheServer); + } + } + else + { + this.Output.WriteLine("There are no available cache servers for: " + enlistment.RepoUrl); + } + } + else + { + string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); + CacheServerInfo cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverScalarConfig); + + this.Output.WriteLine("Using cache server: " + cacheServer); + } + } + } + } +} diff --git a/Scalar/CommandLine/ConfigVerb.cs b/Scalar/CommandLine/ConfigVerb.cs index 95392d3d8c..e09314cfb6 100644 --- a/Scalar/CommandLine/ConfigVerb.cs +++ b/Scalar/CommandLine/ConfigVerb.cs @@ -1,151 +1,151 @@ -using CommandLine; -using Scalar.Common; -using System; -using System.Collections.Generic; - -namespace Scalar.CommandLine -{ - [Verb(ConfigVerbName, HelpText = "Get and set Scalar options.")] - public class ConfigVerb : ScalarVerb.ForNoEnlistment - { - private const string ConfigVerbName = "config"; - private LocalScalarConfig localConfig; - - [Option( - 'l', - "list", - Required = false, - HelpText = "Show all settings")] - public bool List { get; set; } - - [Option( - 'd', - "delete", - Required = false, - HelpText = "Name of setting to delete")] - public string KeyToDelete { get; set; } - - [Value( - 0, - Required = false, - MetaName = "Setting name", - HelpText = "Name of setting that is to be set or read")] - public string Key { get; set; } - - [Value( - 1, - Required = false, - MetaName = "Setting value", - HelpText = "Value of setting to be set")] - public string Value { get; set; } - - protected override string VerbName - { - get { return ConfigVerbName; } - } - - public override void Execute() - { - if (!ScalarPlatform.Instance.UnderConstruction.SupportsScalarConfig) - { - this.ReportErrorAndExit("`scalar config` is not yet implemented on this operating system."); - } - - this.localConfig = new LocalScalarConfig(); - string error = null; - - if (this.IsMutuallyExclusiveOptionsSet(out error)) - { - this.ReportErrorAndExit(error); - } - - if (this.List) - { - Dictionary allSettings; - if (!this.localConfig.TryGetAllConfig(out allSettings, out error)) - { - this.ReportErrorAndExit(error); - } - - const string ConfigOutputFormat = "{0}={1}"; - foreach (KeyValuePair setting in allSettings) - { - Console.WriteLine(ConfigOutputFormat, setting.Key, setting.Value); - } - } - else if (!string.IsNullOrEmpty(this.KeyToDelete)) - { - if (!ScalarPlatform.Instance.IsElevated()) - { - this.ReportErrorAndExit("`scalar config` must be run from an elevated command prompt when deleting settings."); - } - - if (!this.localConfig.TryRemoveConfig(this.KeyToDelete, out error)) - { - this.ReportErrorAndExit(error); - } - } - else if (!string.IsNullOrEmpty(this.Key)) - { - bool valueSpecified = !string.IsNullOrEmpty(this.Value); - if (valueSpecified) - { - if (!ScalarPlatform.Instance.IsElevated()) - { - this.ReportErrorAndExit("`scalar config` must be run from an elevated command prompt when configuring settings."); - } - - if (!this.localConfig.TrySetConfig(this.Key, this.Value, out error)) - { - this.ReportErrorAndExit(error); - } - } - else - { - string valueRead = null; - if (!this.localConfig.TryGetConfig(this.Key, out valueRead, out error) || - string.IsNullOrEmpty(valueRead)) - { - this.ReportErrorAndExit(error); - } - else - { - Console.WriteLine(valueRead); - } - } - } - else - { - this.ReportErrorAndExit("You must specify an option. Run `scalar config --help` for details."); - } - } - - private bool IsMutuallyExclusiveOptionsSet(out string consoleMessage) - { - bool deleteSpecified = !string.IsNullOrEmpty(this.KeyToDelete); - bool setOrReadSpecified = !string.IsNullOrEmpty(this.Key); - bool listSpecified = this.List; - - if (deleteSpecified && listSpecified) - { - consoleMessage = "You cannot delete and list settings at the same time."; - return true; - } - - if (setOrReadSpecified && listSpecified) - { - consoleMessage = "You cannot list all and view (or update) individual settings at the same time."; - return true; - } - - if (setOrReadSpecified && deleteSpecified) - { - consoleMessage = "You cannot delete a setting and view (or update) individual settings at the same time."; - return true; - } - - consoleMessage = null; - return false; - } - } +using CommandLine; +using Scalar.Common; +using System; +using System.Collections.Generic; + +namespace Scalar.CommandLine +{ + [Verb(ConfigVerbName, HelpText = "Get and set Scalar options.")] + public class ConfigVerb : ScalarVerb.ForNoEnlistment + { + private const string ConfigVerbName = "config"; + private LocalScalarConfig localConfig; + + [Option( + 'l', + "list", + Required = false, + HelpText = "Show all settings")] + public bool List { get; set; } + + [Option( + 'd', + "delete", + Required = false, + HelpText = "Name of setting to delete")] + public string KeyToDelete { get; set; } + + [Value( + 0, + Required = false, + MetaName = "Setting name", + HelpText = "Name of setting that is to be set or read")] + public string Key { get; set; } + + [Value( + 1, + Required = false, + MetaName = "Setting value", + HelpText = "Value of setting to be set")] + public string Value { get; set; } + + protected override string VerbName + { + get { return ConfigVerbName; } + } + + public override void Execute() + { + if (!ScalarPlatform.Instance.UnderConstruction.SupportsScalarConfig) + { + this.ReportErrorAndExit("`scalar config` is not yet implemented on this operating system."); + } + + this.localConfig = new LocalScalarConfig(); + string error = null; + + if (this.IsMutuallyExclusiveOptionsSet(out error)) + { + this.ReportErrorAndExit(error); + } + + if (this.List) + { + Dictionary allSettings; + if (!this.localConfig.TryGetAllConfig(out allSettings, out error)) + { + this.ReportErrorAndExit(error); + } + + const string ConfigOutputFormat = "{0}={1}"; + foreach (KeyValuePair setting in allSettings) + { + Console.WriteLine(ConfigOutputFormat, setting.Key, setting.Value); + } + } + else if (!string.IsNullOrEmpty(this.KeyToDelete)) + { + if (!ScalarPlatform.Instance.IsElevated()) + { + this.ReportErrorAndExit("`scalar config` must be run from an elevated command prompt when deleting settings."); + } + + if (!this.localConfig.TryRemoveConfig(this.KeyToDelete, out error)) + { + this.ReportErrorAndExit(error); + } + } + else if (!string.IsNullOrEmpty(this.Key)) + { + bool valueSpecified = !string.IsNullOrEmpty(this.Value); + if (valueSpecified) + { + if (!ScalarPlatform.Instance.IsElevated()) + { + this.ReportErrorAndExit("`scalar config` must be run from an elevated command prompt when configuring settings."); + } + + if (!this.localConfig.TrySetConfig(this.Key, this.Value, out error)) + { + this.ReportErrorAndExit(error); + } + } + else + { + string valueRead = null; + if (!this.localConfig.TryGetConfig(this.Key, out valueRead, out error) || + string.IsNullOrEmpty(valueRead)) + { + this.ReportErrorAndExit(error); + } + else + { + Console.WriteLine(valueRead); + } + } + } + else + { + this.ReportErrorAndExit("You must specify an option. Run `scalar config --help` for details."); + } + } + + private bool IsMutuallyExclusiveOptionsSet(out string consoleMessage) + { + bool deleteSpecified = !string.IsNullOrEmpty(this.KeyToDelete); + bool setOrReadSpecified = !string.IsNullOrEmpty(this.Key); + bool listSpecified = this.List; + + if (deleteSpecified && listSpecified) + { + consoleMessage = "You cannot delete and list settings at the same time."; + return true; + } + + if (setOrReadSpecified && listSpecified) + { + consoleMessage = "You cannot list all and view (or update) individual settings at the same time."; + return true; + } + + if (setOrReadSpecified && deleteSpecified) + { + consoleMessage = "You cannot delete a setting and view (or update) individual settings at the same time."; + return true; + } + + consoleMessage = null; + return false; + } + } } diff --git a/Scalar/CommandLine/DehydrateVerb.cs b/Scalar/CommandLine/DehydrateVerb.cs index 9040866e75..f272d59cfb 100644 --- a/Scalar/CommandLine/DehydrateVerb.cs +++ b/Scalar/CommandLine/DehydrateVerb.cs @@ -1,500 +1,500 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Maintenance; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.DiskLayoutUpgrades; -using System; -using System.IO; -using System.Linq; -using System.Text; - -namespace Scalar.CommandLine -{ - [Verb(DehydrateVerb.DehydrateVerbName, HelpText = "EXPERIMENTAL FEATURE - Fully dehydrate a Scalar repo")] - public class DehydrateVerb : ScalarVerb.ForExistingEnlistment - { - private const string DehydrateVerbName = "dehydrate"; - - [Option( - "confirm", - Default = false, - Required = false, - HelpText = "Pass in this flag to actually do the dehydrate")] - public bool Confirmed { get; set; } - - [Option( - "no-status", - Default = false, - Required = false, - HelpText = "Skip 'git status' before dehydrating")] - public bool NoStatus { get; set; } - - protected override string VerbName - { - get { return DehydrateVerb.DehydrateVerbName; } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "Dehydrate")) - { - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Dehydrate), - EventLevel.Informational, - Keywords.Any); - tracer.WriteStartEvent( - enlistment.EnlistmentRoot, - enlistment.RepoUrl, - CacheServerResolver.GetUrlFromConfig(enlistment), - new EventMetadata - { - { "Confirmed", this.Confirmed }, - { "NoStatus", this.NoStatus }, - { "NamedPipeName", enlistment.NamedPipeName }, - { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, - }); - - // This is only intended to be run by functional tests - if (this.MaintenanceJob != null) - { - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig: null, serverScalarConfig: null, cacheServer: null); - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - using (GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem)) - using (ScalarContext context = new ScalarContext(tracer, fileSystem, gitRepo, enlistment)) - { - switch (this.MaintenanceJob) - { - case "LooseObjects": - (new LooseObjectsStep(context, forceRun: true)).Execute(); - return; - - case "PackfileMaintenance": - (new PackfileMaintenanceStep( - context, - forceRun: true, - batchSize: this.PackfileMaintenanceBatchSize ?? PackfileMaintenanceStep.DefaultBatchSize)).Execute(); - return; - - case "PostFetch": - (new PostFetchStep(context, new System.Collections.Generic.List(), requireObjectCacheLock: false)).Execute(); - return; - - default: - this.ReportErrorAndExit($"Unknown maintenance job requested: {this.MaintenanceJob}"); - break; - } - } - } - - if (!this.Confirmed) - { - this.Output.WriteLine( -@"WARNING: THIS IS AN EXPERIMENTAL FEATURE - -Dehydrate will back up your src folder, and then create a new, empty src folder -with a fresh virtualization of the repo. All of your downloaded objects, branches, -and siblings of the src folder will be preserved. Your modified working directory -files will be moved to the backup, and your new working directory will not have -any of your uncommitted changes. - -Before you dehydrate, make sure you have committed any working directory changes -you want to keep. If you choose not to, you can still find your uncommitted changes -in the backup folder, but it will be harder to find them because 'git status' -will not work in the backup. - -To actually execute the dehydrate, run 'scalar dehydrate --confirm' from the parent -of your enlistment's src folder. -"); - - return; - } - - this.CheckGitStatus(tracer, enlistment); - - string backupRoot = Path.GetFullPath(Path.Combine(enlistment.EnlistmentRoot, "dehydrate_backup", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); - this.Output.WriteLine(); - this.WriteMessage(tracer, "Starting dehydration. All of your existing files will be backed up in " + backupRoot); - this.WriteMessage(tracer, "WARNING: If you abort the dehydrate after this point, the repo may become corrupt"); - this.Output.WriteLine(); - - this.Unmount(tracer); - - string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - RetryConfig retryConfig; - if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); - } - - string errorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out errorMessage)) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - - // Local cache and objects paths are required for TryDownloadGitObjects - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig: null, cacheServer: null); - - if (this.TryBackupFiles(tracer, enlistment, backupRoot)) - { - if (this.TryDownloadGitObjects(tracer, enlistment, retryConfig) && - this.TryRecreateIndex(tracer, enlistment)) - { - this.Mount(tracer); - - this.Output.WriteLine(); - this.WriteMessage(tracer, "The repo was successfully dehydrated and remounted"); - } - } - else - { - this.Output.WriteLine(); - this.WriteMessage(tracer, "ERROR: Backup failed. We will attempt to mount, but you may need to reclone if that fails"); - - this.Mount(tracer); - this.WriteMessage(tracer, "Dehydrate failed, but remounting succeeded"); - } - } - } - - private void CheckGitStatus(ITracer tracer, ScalarEnlistment enlistment) - { - if (!this.NoStatus) - { - this.WriteMessage(tracer, "Running git status before dehydrating to make sure you don't have any pending changes."); - this.WriteMessage(tracer, "If this takes too long, you can abort and run dehydrate with --no-status to skip this safety check."); - this.Output.WriteLine(); - - bool isMounted = false; - GitProcess.Result statusResult = null; - if (!this.ShowStatusWhileRunning( - () => - { - if (this.ExecuteScalarVerb(tracer) != ReturnCode.Success) - { - return false; - } - - isMounted = true; - - GitProcess git = new GitProcess(enlistment); - statusResult = git.Status(allowObjectDownloads: false, useStatusCache: false, showUntracked: true); - if (statusResult.ExitCodeIsFailure) - { - return false; - } - - if (!statusResult.Output.Contains("nothing to commit, working tree clean")) - { - return false; - } - - return true; - }, - "Running git status", - suppressGvfsLogMessage: true)) - { - this.Output.WriteLine(); - - if (!isMounted) - { - this.WriteMessage(tracer, "Failed to run git status because the repo is not mounted"); - this.WriteMessage(tracer, "Either mount first, or run with --no-status"); - } - else if (statusResult.ExitCodeIsFailure) - { - this.WriteMessage(tracer, "Failed to run git status: " + statusResult.Errors); - } - else - { - this.WriteMessage(tracer, statusResult.Output); - this.WriteMessage(tracer, "git status reported that you have dirty files"); - this.WriteMessage(tracer, "Either commit your changes or run dehydrate with --no-status"); - } - - this.ReportErrorAndExit(tracer, "Dehydrate was aborted"); - } - } - } - - private void Unmount(ITracer tracer) - { - if (!this.ShowStatusWhileRunning( - () => - { - return - this.ExecuteScalarVerb(tracer) != ReturnCode.Success || - this.ExecuteScalarVerb(tracer) == ReturnCode.Success; - }, - "Unmounting", - suppressGvfsLogMessage: true)) - { - this.ReportErrorAndExit(tracer, "Unable to unmount."); - } - } - - private void Mount(ITracer tracer) - { - if (!this.ShowStatusWhileRunning( - () => - { - return this.ExecuteScalarVerb(tracer) == ReturnCode.Success; - }, - "Mounting")) - { - this.ReportErrorAndExit(tracer, "Failed to mount after dehydrating."); - } - } - - private bool TryBackupFiles(ITracer tracer, ScalarEnlistment enlistment, string backupRoot) - { - string backupSrc = Path.Combine(backupRoot, "src"); - string backupGit = Path.Combine(backupRoot, ".git"); - string backupGvfs = Path.Combine(backupRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); - string backupDatabases = Path.Combine(backupGvfs, ScalarConstants.DotScalar.Databases.Name); - - string errorMessage = string.Empty; - if (!this.ShowStatusWhileRunning( - () => - { - string ioError; - if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) || - !this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) || - !this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .scalar directory", out ioError) || - !this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .scalar databases directory", out ioError)) - { - errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError; - return false; - } - - // Move the current src folder to the backup location... - if (!this.TryIO(tracer, () => Directory.Move(enlistment.WorkingDirectoryRoot, backupSrc), "Move the src folder", out ioError)) - { - errorMessage = "Failed to move the src folder: " + ioError + Environment.NewLine; - errorMessage += "Make sure you have no open handles or running processes in the src folder"; - return false; - } - - // ... but move the .git folder back to the new src folder so we can preserve objects, refs, logs... - if (!this.TryIO(tracer, () => Directory.CreateDirectory(enlistment.WorkingDirectoryRoot), "Create new src folder", out errorMessage) || - !this.TryIO(tracer, () => Directory.Move(Path.Combine(backupSrc, ".git"), enlistment.DotGitRoot), "Keep existing .git folder", out errorMessage)) - { - return false; - } - - // ... backup the .scalar hydration-related data structures... - string databasesFolder = Path.Combine(enlistment.DotScalarRoot, ScalarConstants.DotScalar.Databases.Name); - if (!this.TryBackupFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat")) - { - return false; - } - - // ... backup everything related to the .git\index... - if (!this.TryIO( - tracer, - () => File.Move( - Path.Combine(enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName), - Path.Combine(backupGit, ScalarConstants.DotGit.IndexName)), - "Backup the git index", - out errorMessage)) - { - return false; - } - - // ... backup all .git\*.lock files - if (!this.TryBackupFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock")) - { - return false; - } - - return true; - }, - "Backing up your files")) - { - this.Output.WriteLine(); - this.WriteMessage(tracer, "ERROR: " + errorMessage); - - return false; - } - - return true; - } - - private bool TryBackupFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip) - { - string errorMessage; - foreach (string file in Directory.GetFiles(folderPath, searchPattern)) - { - string fileName = Path.GetFileName(file); - if (!filenamesToSkip.Any(x => x.Equals(fileName, StringComparison.OrdinalIgnoreCase))) - { - if (!this.TryIO( - tracer, - () => File.Move(file, file.Replace(folderPath, backupPath)), - $"Backing up {Path.GetFileName(file)}", - out errorMessage)) - { - return false; - } - } - } - - return true; - } - - private bool TryDownloadGitObjects(ITracer tracer, ScalarEnlistment enlistment, RetryConfig retryConfig) - { - string errorMessage = null; - - if (!this.ShowStatusWhileRunning( - () => - { - CacheServerInfo cacheServer = new CacheServerInfo(enlistment.RepoUrl, null); - using (GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig)) - { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); - ScalarGitObjects gitObjects = new ScalarGitObjects(new ScalarContext(tracer, fileSystem, gitRepo, enlistment), objectRequestor); - - GitProcess.Result revParseResult = enlistment.CreateGitProcess().RevParse("HEAD"); - if (revParseResult.ExitCodeIsFailure) - { - errorMessage = "Unable to determine HEAD commit id: " + revParseResult.Errors; - return false; - } - - string headCommit = revParseResult.Output.TrimEnd('\n'); - - if (!this.TryDownloadCommit(headCommit, enlistment, objectRequestor, gitObjects, gitRepo, out errorMessage) || - !this.TryDownloadRootGitAttributes(enlistment, gitObjects, gitRepo, out errorMessage)) - { - return false; - } - } - - return true; - }, - "Downloading git objects", - suppressGvfsLogMessage: true)) - { - this.WriteMessage(tracer, errorMessage); - return false; - } - - return true; - } - - private bool TryRecreateIndex(ITracer tracer, ScalarEnlistment enlistment) - { - string errorMessage = null; - - if (!this.ShowStatusWhileRunning( - () => - { - // Create a new index based on the new minimal modified paths - GitProcess git = new GitProcess(enlistment); - GitProcess.Result checkoutResult = git.ForceCheckout("HEAD"); - - errorMessage = checkoutResult.Errors; - return checkoutResult.ExitCodeIsSuccess; - }, - "Recreating git index", - suppressGvfsLogMessage: true)) - { - this.WriteMessage(tracer, "Failed to recreate index: " + errorMessage); - return false; - } - - return true; - } - - private void WriteMessage(ITracer tracer, string message) - { - this.Output.WriteLine(message); - tracer.RelatedEvent( - EventLevel.Informational, - "Dehydrate", - new EventMetadata - { - { TracingConstants.MessageKey.InfoMessage, message } - }); - } - - private ReturnCode ExecuteScalarVerb(ITracer tracer) - where TVerb : ScalarVerb, new() - { - try - { - ReturnCode returnCode; - StringBuilder commandOutput = new StringBuilder(); - using (StringWriter writer = new StringWriter(commandOutput)) - { - returnCode = this.Execute(this.EnlistmentRootPathParameter, verb => verb.Output = writer); - } - - tracer.RelatedEvent( - EventLevel.Informational, - typeof(TVerb).Name, - new EventMetadata - { - { "Output", commandOutput.ToString() }, - { "ReturnCode", returnCode } - }); - - return returnCode; - } - catch (Exception e) - { - tracer.RelatedError( - new EventMetadata - { - { "Verb", typeof(TVerb).Name }, - { "Exception", e.ToString() } - }, - "ExecuteScalarVerb: Caught exception"); - - return ReturnCode.GenericError; - } - } - - private bool TryIO(ITracer tracer, Action action, string description, out string error) - { - try - { - action(); - tracer.RelatedEvent( - EventLevel.Informational, - "TryIO", - new EventMetadata - { - { "Description", description } - }); - - error = null; - return true; - } - catch (Exception e) - { - error = e.Message; - tracer.RelatedError( - new EventMetadata - { - { "Description", description }, - { "Error", error } - }, - "TryIO: Caught exception performing action"); - } - - return false; - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Maintenance; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.DiskLayoutUpgrades; +using System; +using System.IO; +using System.Linq; +using System.Text; + +namespace Scalar.CommandLine +{ + [Verb(DehydrateVerb.DehydrateVerbName, HelpText = "EXPERIMENTAL FEATURE - Fully dehydrate a Scalar repo")] + public class DehydrateVerb : ScalarVerb.ForExistingEnlistment + { + private const string DehydrateVerbName = "dehydrate"; + + [Option( + "confirm", + Default = false, + Required = false, + HelpText = "Pass in this flag to actually do the dehydrate")] + public bool Confirmed { get; set; } + + [Option( + "no-status", + Default = false, + Required = false, + HelpText = "Skip 'git status' before dehydrating")] + public bool NoStatus { get; set; } + + protected override string VerbName + { + get { return DehydrateVerb.DehydrateVerbName; } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "Dehydrate")) + { + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Dehydrate), + EventLevel.Informational, + Keywords.Any); + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + CacheServerResolver.GetUrlFromConfig(enlistment), + new EventMetadata + { + { "Confirmed", this.Confirmed }, + { "NoStatus", this.NoStatus }, + { "NamedPipeName", enlistment.NamedPipeName }, + { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, + }); + + // This is only intended to be run by functional tests + if (this.MaintenanceJob != null) + { + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig: null, serverScalarConfig: null, cacheServer: null); + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + using (GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem)) + using (ScalarContext context = new ScalarContext(tracer, fileSystem, gitRepo, enlistment)) + { + switch (this.MaintenanceJob) + { + case "LooseObjects": + (new LooseObjectsStep(context, forceRun: true)).Execute(); + return; + + case "PackfileMaintenance": + (new PackfileMaintenanceStep( + context, + forceRun: true, + batchSize: this.PackfileMaintenanceBatchSize ?? PackfileMaintenanceStep.DefaultBatchSize)).Execute(); + return; + + case "PostFetch": + (new PostFetchStep(context, new System.Collections.Generic.List(), requireObjectCacheLock: false)).Execute(); + return; + + default: + this.ReportErrorAndExit($"Unknown maintenance job requested: {this.MaintenanceJob}"); + break; + } + } + } + + if (!this.Confirmed) + { + this.Output.WriteLine( +@"WARNING: THIS IS AN EXPERIMENTAL FEATURE + +Dehydrate will back up your src folder, and then create a new, empty src folder +with a fresh virtualization of the repo. All of your downloaded objects, branches, +and siblings of the src folder will be preserved. Your modified working directory +files will be moved to the backup, and your new working directory will not have +any of your uncommitted changes. + +Before you dehydrate, make sure you have committed any working directory changes +you want to keep. If you choose not to, you can still find your uncommitted changes +in the backup folder, but it will be harder to find them because 'git status' +will not work in the backup. + +To actually execute the dehydrate, run 'scalar dehydrate --confirm' from the parent +of your enlistment's src folder. +"); + + return; + } + + this.CheckGitStatus(tracer, enlistment); + + string backupRoot = Path.GetFullPath(Path.Combine(enlistment.EnlistmentRoot, "dehydrate_backup", DateTime.Now.ToString("yyyyMMdd_HHmmss"))); + this.Output.WriteLine(); + this.WriteMessage(tracer, "Starting dehydration. All of your existing files will be backed up in " + backupRoot); + this.WriteMessage(tracer, "WARNING: If you abort the dehydrate after this point, the repo may become corrupt"); + this.Output.WriteLine(); + + this.Unmount(tracer); + + string error; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer, enlistment.EnlistmentRoot, out error)) + { + this.ReportErrorAndExit(tracer, error); + } + + RetryConfig retryConfig; + if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); + } + + string errorMessage; + if (!this.TryAuthenticate(tracer, enlistment, out errorMessage)) + { + this.ReportErrorAndExit(tracer, errorMessage); + } + + // Local cache and objects paths are required for TryDownloadGitObjects + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig: null, cacheServer: null); + + if (this.TryBackupFiles(tracer, enlistment, backupRoot)) + { + if (this.TryDownloadGitObjects(tracer, enlistment, retryConfig) && + this.TryRecreateIndex(tracer, enlistment)) + { + this.Mount(tracer); + + this.Output.WriteLine(); + this.WriteMessage(tracer, "The repo was successfully dehydrated and remounted"); + } + } + else + { + this.Output.WriteLine(); + this.WriteMessage(tracer, "ERROR: Backup failed. We will attempt to mount, but you may need to reclone if that fails"); + + this.Mount(tracer); + this.WriteMessage(tracer, "Dehydrate failed, but remounting succeeded"); + } + } + } + + private void CheckGitStatus(ITracer tracer, ScalarEnlistment enlistment) + { + if (!this.NoStatus) + { + this.WriteMessage(tracer, "Running git status before dehydrating to make sure you don't have any pending changes."); + this.WriteMessage(tracer, "If this takes too long, you can abort and run dehydrate with --no-status to skip this safety check."); + this.Output.WriteLine(); + + bool isMounted = false; + GitProcess.Result statusResult = null; + if (!this.ShowStatusWhileRunning( + () => + { + if (this.ExecuteScalarVerb(tracer) != ReturnCode.Success) + { + return false; + } + + isMounted = true; + + GitProcess git = new GitProcess(enlistment); + statusResult = git.Status(allowObjectDownloads: false, useStatusCache: false, showUntracked: true); + if (statusResult.ExitCodeIsFailure) + { + return false; + } + + if (!statusResult.Output.Contains("nothing to commit, working tree clean")) + { + return false; + } + + return true; + }, + "Running git status", + suppressGvfsLogMessage: true)) + { + this.Output.WriteLine(); + + if (!isMounted) + { + this.WriteMessage(tracer, "Failed to run git status because the repo is not mounted"); + this.WriteMessage(tracer, "Either mount first, or run with --no-status"); + } + else if (statusResult.ExitCodeIsFailure) + { + this.WriteMessage(tracer, "Failed to run git status: " + statusResult.Errors); + } + else + { + this.WriteMessage(tracer, statusResult.Output); + this.WriteMessage(tracer, "git status reported that you have dirty files"); + this.WriteMessage(tracer, "Either commit your changes or run dehydrate with --no-status"); + } + + this.ReportErrorAndExit(tracer, "Dehydrate was aborted"); + } + } + } + + private void Unmount(ITracer tracer) + { + if (!this.ShowStatusWhileRunning( + () => + { + return + this.ExecuteScalarVerb(tracer) != ReturnCode.Success || + this.ExecuteScalarVerb(tracer) == ReturnCode.Success; + }, + "Unmounting", + suppressGvfsLogMessage: true)) + { + this.ReportErrorAndExit(tracer, "Unable to unmount."); + } + } + + private void Mount(ITracer tracer) + { + if (!this.ShowStatusWhileRunning( + () => + { + return this.ExecuteScalarVerb(tracer) == ReturnCode.Success; + }, + "Mounting")) + { + this.ReportErrorAndExit(tracer, "Failed to mount after dehydrating."); + } + } + + private bool TryBackupFiles(ITracer tracer, ScalarEnlistment enlistment, string backupRoot) + { + string backupSrc = Path.Combine(backupRoot, "src"); + string backupGit = Path.Combine(backupRoot, ".git"); + string backupGvfs = Path.Combine(backupRoot, ScalarPlatform.Instance.Constants.DotScalarRoot); + string backupDatabases = Path.Combine(backupGvfs, ScalarConstants.DotScalar.Databases.Name); + + string errorMessage = string.Empty; + if (!this.ShowStatusWhileRunning( + () => + { + string ioError; + if (!this.TryIO(tracer, () => Directory.CreateDirectory(backupRoot), "Create backup directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupGit), "Create backup .git directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupGvfs), "Create backup .scalar directory", out ioError) || + !this.TryIO(tracer, () => Directory.CreateDirectory(backupDatabases), "Create backup .scalar databases directory", out ioError)) + { + errorMessage = "Failed to create backup folders at " + backupRoot + ": " + ioError; + return false; + } + + // Move the current src folder to the backup location... + if (!this.TryIO(tracer, () => Directory.Move(enlistment.WorkingDirectoryRoot, backupSrc), "Move the src folder", out ioError)) + { + errorMessage = "Failed to move the src folder: " + ioError + Environment.NewLine; + errorMessage += "Make sure you have no open handles or running processes in the src folder"; + return false; + } + + // ... but move the .git folder back to the new src folder so we can preserve objects, refs, logs... + if (!this.TryIO(tracer, () => Directory.CreateDirectory(enlistment.WorkingDirectoryRoot), "Create new src folder", out errorMessage) || + !this.TryIO(tracer, () => Directory.Move(Path.Combine(backupSrc, ".git"), enlistment.DotGitRoot), "Keep existing .git folder", out errorMessage)) + { + return false; + } + + // ... backup the .scalar hydration-related data structures... + string databasesFolder = Path.Combine(enlistment.DotScalarRoot, ScalarConstants.DotScalar.Databases.Name); + if (!this.TryBackupFilesInFolder(tracer, databasesFolder, backupDatabases, searchPattern: "*", filenamesToSkip: "RepoMetadata.dat")) + { + return false; + } + + // ... backup everything related to the .git\index... + if (!this.TryIO( + tracer, + () => File.Move( + Path.Combine(enlistment.DotGitRoot, ScalarConstants.DotGit.IndexName), + Path.Combine(backupGit, ScalarConstants.DotGit.IndexName)), + "Backup the git index", + out errorMessage)) + { + return false; + } + + // ... backup all .git\*.lock files + if (!this.TryBackupFilesInFolder(tracer, enlistment.DotGitRoot, backupGit, searchPattern: "*.lock")) + { + return false; + } + + return true; + }, + "Backing up your files")) + { + this.Output.WriteLine(); + this.WriteMessage(tracer, "ERROR: " + errorMessage); + + return false; + } + + return true; + } + + private bool TryBackupFilesInFolder(ITracer tracer, string folderPath, string backupPath, string searchPattern, params string[] filenamesToSkip) + { + string errorMessage; + foreach (string file in Directory.GetFiles(folderPath, searchPattern)) + { + string fileName = Path.GetFileName(file); + if (!filenamesToSkip.Any(x => x.Equals(fileName, StringComparison.OrdinalIgnoreCase))) + { + if (!this.TryIO( + tracer, + () => File.Move(file, file.Replace(folderPath, backupPath)), + $"Backing up {Path.GetFileName(file)}", + out errorMessage)) + { + return false; + } + } + } + + return true; + } + + private bool TryDownloadGitObjects(ITracer tracer, ScalarEnlistment enlistment, RetryConfig retryConfig) + { + string errorMessage = null; + + if (!this.ShowStatusWhileRunning( + () => + { + CacheServerInfo cacheServer = new CacheServerInfo(enlistment.RepoUrl, null); + using (GitObjectsHttpRequestor objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig)) + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); + ScalarGitObjects gitObjects = new ScalarGitObjects(new ScalarContext(tracer, fileSystem, gitRepo, enlistment), objectRequestor); + + GitProcess.Result revParseResult = enlistment.CreateGitProcess().RevParse("HEAD"); + if (revParseResult.ExitCodeIsFailure) + { + errorMessage = "Unable to determine HEAD commit id: " + revParseResult.Errors; + return false; + } + + string headCommit = revParseResult.Output.TrimEnd('\n'); + + if (!this.TryDownloadCommit(headCommit, enlistment, objectRequestor, gitObjects, gitRepo, out errorMessage) || + !this.TryDownloadRootGitAttributes(enlistment, gitObjects, gitRepo, out errorMessage)) + { + return false; + } + } + + return true; + }, + "Downloading git objects", + suppressGvfsLogMessage: true)) + { + this.WriteMessage(tracer, errorMessage); + return false; + } + + return true; + } + + private bool TryRecreateIndex(ITracer tracer, ScalarEnlistment enlistment) + { + string errorMessage = null; + + if (!this.ShowStatusWhileRunning( + () => + { + // Create a new index based on the new minimal modified paths + GitProcess git = new GitProcess(enlistment); + GitProcess.Result checkoutResult = git.ForceCheckout("HEAD"); + + errorMessage = checkoutResult.Errors; + return checkoutResult.ExitCodeIsSuccess; + }, + "Recreating git index", + suppressGvfsLogMessage: true)) + { + this.WriteMessage(tracer, "Failed to recreate index: " + errorMessage); + return false; + } + + return true; + } + + private void WriteMessage(ITracer tracer, string message) + { + this.Output.WriteLine(message); + tracer.RelatedEvent( + EventLevel.Informational, + "Dehydrate", + new EventMetadata + { + { TracingConstants.MessageKey.InfoMessage, message } + }); + } + + private ReturnCode ExecuteScalarVerb(ITracer tracer) + where TVerb : ScalarVerb, new() + { + try + { + ReturnCode returnCode; + StringBuilder commandOutput = new StringBuilder(); + using (StringWriter writer = new StringWriter(commandOutput)) + { + returnCode = this.Execute(this.EnlistmentRootPathParameter, verb => verb.Output = writer); + } + + tracer.RelatedEvent( + EventLevel.Informational, + typeof(TVerb).Name, + new EventMetadata + { + { "Output", commandOutput.ToString() }, + { "ReturnCode", returnCode } + }); + + return returnCode; + } + catch (Exception e) + { + tracer.RelatedError( + new EventMetadata + { + { "Verb", typeof(TVerb).Name }, + { "Exception", e.ToString() } + }, + "ExecuteScalarVerb: Caught exception"); + + return ReturnCode.GenericError; + } + } + + private bool TryIO(ITracer tracer, Action action, string description, out string error) + { + try + { + action(); + tracer.RelatedEvent( + EventLevel.Informational, + "TryIO", + new EventMetadata + { + { "Description", description } + }); + + error = null; + return true; + } + catch (Exception e) + { + error = e.Message; + tracer.RelatedError( + new EventMetadata + { + { "Description", description }, + { "Error", error } + }, + "TryIO: Caught exception performing action"); + } + + return false; + } + } +} diff --git a/Scalar/CommandLine/DiagnoseVerb.cs b/Scalar/CommandLine/DiagnoseVerb.cs index e397130f93..5c715bcd0c 100644 --- a/Scalar/CommandLine/DiagnoseVerb.cs +++ b/Scalar/CommandLine/DiagnoseVerb.cs @@ -1,580 +1,580 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; - -namespace Scalar.CommandLine -{ - [Verb(DiagnoseVerb.DiagnoseVerbName, HelpText = "Diagnose issues with a Scalar repo")] - public class DiagnoseVerb : ScalarVerb.ForExistingEnlistment - { - private const string DiagnoseVerbName = "diagnose"; - private const string DeprecatedUpgradeLogsDirectory = "Logs"; - - private TextWriter diagnosticLogFileWriter; - private PhysicalFileSystem fileSystem; - - public DiagnoseVerb() : base(false) - { - this.fileSystem = new PhysicalFileSystem(); - } - - protected override string VerbName - { - get { return DiagnoseVerbName; } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - string diagnosticsRoot = Path.Combine(enlistment.DotScalarRoot, "diagnostics"); - - if (!Directory.Exists(diagnosticsRoot)) - { - Directory.CreateDirectory(diagnosticsRoot); - } - - string archiveFolderPath = Path.Combine(diagnosticsRoot, "scalar_" + DateTime.Now.ToString("yyyyMMdd_HHmmss")); - Directory.CreateDirectory(archiveFolderPath); - - using (FileStream diagnosticLogFile = new FileStream(Path.Combine(archiveFolderPath, "diagnostics.log"), FileMode.CreateNew)) - using (this.diagnosticLogFileWriter = new StreamWriter(diagnosticLogFile)) - { - this.WriteMessage("Collecting diagnostic info into temp folder " + archiveFolderPath); - - this.WriteMessage(string.Empty); - this.WriteMessage("scalar version " + ProcessHelper.GetCurrentProcessVersion()); - - GitVersion gitVersion = null; - string error = null; - if (!string.IsNullOrEmpty(enlistment.GitBinPath) && GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out error)) - { - this.WriteMessage("git version " + gitVersion.ToString()); - } - else - { - this.WriteMessage("Could not determine git version. " + error); - } - - this.WriteMessage(enlistment.GitBinPath); - this.WriteMessage(string.Empty); - this.WriteMessage("Enlistment root: " + enlistment.EnlistmentRoot); - this.WriteMessage("Cache Server: " + CacheServerResolver.GetCacheServerFromConfig(enlistment)); - - string localCacheRoot; - string gitObjectsRoot; - this.GetLocalCachePaths(enlistment, out localCacheRoot, out gitObjectsRoot); - string actualLocalCacheRoot = !string.IsNullOrWhiteSpace(localCacheRoot) ? localCacheRoot : gitObjectsRoot; - this.WriteMessage("Local Cache: " + actualLocalCacheRoot); - this.WriteMessage(string.Empty); - - this.PrintDiskSpaceInfo(actualLocalCacheRoot, this.EnlistmentRootPathParameter); - - this.RecordVersionInformation(); - - this.ShowStatusWhileRunning( - () => - this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_status.txt") != ReturnCode.Success || - this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_unmount.txt", verb => verb.SkipLock = true) == ReturnCode.Success, - "Unmounting", - suppressGvfsLogMessage: true); - - this.ShowStatusWhileRunning( - () => - { - // .scalar - this.CopyAllFiles(enlistment.EnlistmentRoot, archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot, copySubFolders: false); - - // .git - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Root, copySubFolders: false); - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Hooks.Root, copySubFolders: false); - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Info.Root, copySubFolders: false); - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Logs.Root, copySubFolders: true); - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Refs.Root, copySubFolders: true); - this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Objects.Info.Root, copySubFolders: false); - this.LogDirectoryEnumeration(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, ScalarConstants.DotGit.Objects.Root), ScalarConstants.DotGit.Objects.Pack.Root, "packs-local.txt"); - this.LogLooseObjectCount(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, ScalarConstants.DotGit.Objects.Root), ScalarConstants.DotGit.Objects.Root, "objects-local.txt"); - - // databases - this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), ScalarConstants.DotScalar.Databases.Name, copySubFolders: false); - - // local cache - this.CopyLocalCacheData(archiveFolderPath, localCacheRoot, gitObjectsRoot); - - // corrupt objects - this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), ScalarConstants.DotScalar.CorruptObjectsName, copySubFolders: false); - - // service - this.CopyAllFiles( - ScalarPlatform.Instance.GetDataRootForScalar(), - archiveFolderPath, - this.ServiceName, - copySubFolders: true); - - if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarUpgrade) - { - // upgrader - this.CopyAllFiles( - ProductUpgraderInfo.GetParentLogDirectoryPath(), - archiveFolderPath, - DeprecatedUpgradeLogsDirectory, - copySubFolders: true, - targetFolderName: Path.Combine(ProductUpgraderInfo.UpgradeDirectoryName, DeprecatedUpgradeLogsDirectory)); - - this.CopyAllFiles( - ProductUpgraderInfo.GetParentLogDirectoryPath(), - archiveFolderPath, - ProductUpgraderInfo.LogDirectory, - copySubFolders: true, - targetFolderName: Path.Combine(ProductUpgraderInfo.UpgradeDirectoryName, ProductUpgraderInfo.LogDirectory)); - - this.LogDirectoryEnumeration( - ProductUpgraderInfo.GetUpgradeProtectedDataDirectory(), - Path.Combine(archiveFolderPath, ProductUpgraderInfo.UpgradeDirectoryName), - ProductUpgraderInfo.DownloadDirectory, - "downloaded-assets.txt"); - } - - if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarConfig) - { - this.CopyFile(ScalarPlatform.Instance.GetDataRootForScalar(), archiveFolderPath, LocalScalarConfig.FileName); - } - - return true; - }, - "Copying logs"); - - this.ShowStatusWhileRunning( - () => this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_mount.txt") == ReturnCode.Success, - "Mounting", - suppressGvfsLogMessage: true); - - this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), "logs", copySubFolders: false); - } - - string zipFilePath = archiveFolderPath + ".zip"; - this.ShowStatusWhileRunning( - () => - { - ZipFile.CreateFromDirectory(archiveFolderPath, zipFilePath); - this.fileSystem.DeleteDirectory(archiveFolderPath); - - return true; - }, - "Creating zip file", - suppressGvfsLogMessage: true); - - this.Output.WriteLine(); - this.Output.WriteLine("Diagnostics complete. All of the gathered info, as well as all of the output above, is captured in"); - this.Output.WriteLine(zipFilePath); - } - - private void WriteMessage(string message, bool skipStdout = false) - { - message = message.TrimEnd('\r', '\n'); - - if (!skipStdout) - { - this.Output.WriteLine(message); - } - - this.diagnosticLogFileWriter.WriteLine(message); - } - - private void RecordVersionInformation() - { - string information = ScalarPlatform.Instance.GetOSVersionInformation(); - this.diagnosticLogFileWriter.WriteLine(information); - } - - private void CopyFile( - string sourceRoot, - string targetRoot, - string fileName) - { - string sourceFile = Path.Combine(sourceRoot, fileName); - string targetFile = Path.Combine(targetRoot, fileName); - - try - { - if (!File.Exists(sourceFile)) - { - return; - } - - File.Copy(sourceFile, targetFile); - } - catch (Exception e) - { - this.WriteMessage( - string.Format( - "Failed to copy file {0} in {1} with exception {2}", - fileName, - sourceRoot, - e)); - } - } - - private void CopyAllFiles( - string sourceRoot, - string targetRoot, - string folderName, - bool copySubFolders, - bool hideErrorsFromStdout = false, - string targetFolderName = null) - { - string sourceFolder = Path.Combine(sourceRoot, folderName); - string targetFolder = Path.Combine(targetRoot, targetFolderName ?? folderName); - - try - { - if (!Directory.Exists(sourceFolder)) - { - return; - } - - this.RecursiveFileCopyImpl(sourceFolder, targetFolder, copySubFolders, hideErrorsFromStdout); - } - catch (Exception e) - { - this.WriteMessage( - string.Format( - "Failed to copy folder {0} in {1} with exception {2}. copySubFolders: {3}", - folderName, - sourceRoot, - e, - copySubFolders), - hideErrorsFromStdout); - } - } - - private void GetLocalCachePaths(ScalarEnlistment enlistment, out string localCacheRoot, out string gitObjectsRoot) - { - localCacheRoot = null; - gitObjectsRoot = null; - - try - { - using (ITracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "DiagnoseVerb")) - { - string error; - if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot), out error)) - { - RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error); - RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error); - } - else - { - this.WriteMessage("Failed to determine local cache path and git objects root, RepoMetadata error: " + error); - } - } - } - catch (Exception e) - { - this.WriteMessage(string.Format("Failed to determine local cache path and git objects root, Exception: {0}", e)); - } - finally - { - RepoMetadata.Shutdown(); - } - } - - private void CopyLocalCacheData(string archiveFolderPath, string localCacheRoot, string gitObjectsRoot) - { - try - { - string localCacheArchivePath = Path.Combine(archiveFolderPath, ScalarConstants.DefaultScalarCacheFolderName); - Directory.CreateDirectory(localCacheArchivePath); - - if (!string.IsNullOrWhiteSpace(localCacheRoot)) - { - // Copy all mapping.dat files in the local cache folder (i.e. mapping.dat, mapping.dat.tmp, mapping.dat.lock) - foreach (string filePath in Directory.EnumerateFiles(localCacheRoot, "mapping.dat*")) - { - string fileName = Path.GetFileName(filePath); - try - { - File.Copy(filePath, Path.Combine(localCacheArchivePath, fileName)); - } - catch (Exception e) - { - this.WriteMessage(string.Format( - "Failed to copy '{0}' from {1} to {2} with exception {3}", - fileName, - localCacheRoot, - archiveFolderPath, - e)); - } - } - } - - if (!string.IsNullOrWhiteSpace(gitObjectsRoot)) - { - this.LogDirectoryEnumeration(gitObjectsRoot, localCacheArchivePath, ScalarConstants.DotGit.Objects.Pack.Name, "packs-cached.txt"); - this.LogLooseObjectCount(gitObjectsRoot, localCacheArchivePath, string.Empty, "objects-cached.txt"); - - // Store all commit-graph files - this.CopyAllFiles(gitObjectsRoot, localCacheArchivePath, ScalarConstants.DotGit.Objects.Info.Root, copySubFolders: true); - } - } - catch (Exception e) - { - this.WriteMessage(string.Format("Failed to copy local cache data with exception: {0}", e)); - } - } - - private void LogDirectoryEnumeration(string sourceRoot, string targetRoot, string folderName, string logfile) - { - try - { - if (!Directory.Exists(targetRoot)) - { - Directory.CreateDirectory(targetRoot); - } - - string folder = Path.Combine(sourceRoot, folderName); - string targetLog = Path.Combine(targetRoot, logfile); - - List lines = new List(); - - if (Directory.Exists(folder)) - { - DirectoryInfo packDirectory = new DirectoryInfo(folder); - - lines.Add($"Contents of {folder}:"); - foreach (FileInfo file in packDirectory.EnumerateFiles()) - { - lines.Add($"{file.Name, -70} {file.Length, 16}"); - } - } - - File.WriteAllLines(targetLog, lines.ToArray()); - } - catch (Exception e) - { - this.WriteMessage(string.Format( - "Failed to log file sizes for {0} in {1} with exception {2}. logfile: {3}", - folderName, - sourceRoot, - e, - logfile)); - } - } - - private void LogLooseObjectCount(string sourceRoot, string targetRoot, string folderName, string logfile) - { - try - { - if (!Directory.Exists(targetRoot)) - { - Directory.CreateDirectory(targetRoot); - } - - string objectFolder = Path.Combine(sourceRoot, folderName); - string targetLog = Path.Combine(targetRoot, logfile); - - List lines = new List(); - - if (Directory.Exists(objectFolder)) - { - DirectoryInfo objectDirectory = new DirectoryInfo(objectFolder); - - int countLoose = 0; - int countFolders = 0; - - lines.Add($"Object directory stats for {objectFolder}:"); - - foreach (DirectoryInfo directory in objectDirectory.EnumerateDirectories()) - { - if (GitObjects.IsLooseObjectsDirectory(directory.Name)) - { - countFolders++; - int numObjects = directory.EnumerateFiles().Count(); - lines.Add($"{directory.Name} : {numObjects, 7} objects"); - countLoose += numObjects; - } - } - - lines.Add($"Total: {countLoose} loose objects"); - } - - File.WriteAllLines(targetLog, lines.ToArray()); - } - catch (Exception e) - { - this.WriteMessage(string.Format( - "Failed to log loose object count for {0} in {1} with exception {2}. logfile: {3}", - folderName, - sourceRoot, - e, - logfile)); - } - } - - private void RecursiveFileCopyImpl(string sourcePath, string targetPath, bool copySubFolders, bool hideErrorsFromStdout) - { - if (!Directory.Exists(targetPath)) - { - Directory.CreateDirectory(targetPath); - } - - foreach (string filePath in Directory.EnumerateFiles(sourcePath)) - { - string fileName = Path.GetFileName(filePath); - try - { - string sourceFilePath = Path.Combine(sourcePath, fileName); - if (!ScalarPlatform.Instance.FileSystem.IsSocket(sourceFilePath) && - !ScalarPlatform.Instance.FileSystem.IsExecutable(sourceFilePath)) - { - File.Copy( - Path.Combine(sourcePath, fileName), - Path.Combine(targetPath, fileName)); - } - } - catch (Exception e) - { - this.WriteMessage( - string.Format( - "Failed to copy '{0}' in {1} with exception {2}", - fileName, - sourcePath, - e), - hideErrorsFromStdout); - } - } - - if (copySubFolders) - { - DirectoryInfo dir = new DirectoryInfo(sourcePath); - foreach (DirectoryInfo subdir in dir.GetDirectories()) - { - string targetFolderPath = Path.Combine(targetPath, subdir.Name); - try - { - this.RecursiveFileCopyImpl(subdir.FullName, targetFolderPath, copySubFolders, hideErrorsFromStdout); - } - catch (Exception e) - { - this.WriteMessage( - string.Format( - "Failed to copy subfolder '{0}' to '{1}' with exception {2}", - subdir.FullName, - targetFolderPath, - e), - hideErrorsFromStdout); - } - } - } - } - - private ReturnCode RunAndRecordScalarVerb(string archiveFolderPath, string outputFileName, Action configureVerb = null) - where TVerb : ScalarVerb, new() - { - try - { - using (FileStream file = new FileStream(Path.Combine(archiveFolderPath, outputFileName), FileMode.CreateNew)) - using (StreamWriter writer = new StreamWriter(file)) - { - return this.Execute( - this.EnlistmentRootPathParameter, - verb => - { - if (configureVerb != null) - { - configureVerb(verb); - } - - verb.Output = writer; - }); - } - } - catch (Exception e) - { - this.WriteMessage(string.Format( - "Verb {0} failed with exception {1}", - typeof(TVerb), - e)); - - return ReturnCode.GenericError; - } - } - - private void PrintDiskSpaceInfo(string localCacheRoot, string enlistmentRootParameter) - { - try - { - string enlistmentNormalizedPathRoot; - string localCacheNormalizedPathRoot; - string enlistmentErrorMessage; - string localCacheErrorMessage; - - bool enlistmentSuccess = ScalarPlatform.Instance.TryGetNormalizedPathRoot(enlistmentRootParameter, out enlistmentNormalizedPathRoot, out enlistmentErrorMessage); - bool localCacheSuccess = ScalarPlatform.Instance.TryGetNormalizedPathRoot(localCacheRoot, out localCacheNormalizedPathRoot, out localCacheErrorMessage); - - if (!enlistmentSuccess || !localCacheSuccess) - { - this.WriteMessage("Failed to acquire disk space information:"); - if (!string.IsNullOrEmpty(enlistmentErrorMessage)) - { - this.WriteMessage(enlistmentErrorMessage); - } - - if (!string.IsNullOrEmpty(localCacheErrorMessage)) - { - this.WriteMessage(localCacheErrorMessage); - } - - this.WriteMessage(string.Empty); - return; - } - - DriveInfo enlistmentDrive = new DriveInfo(enlistmentNormalizedPathRoot); - string enlistmentDriveDiskSpace = this.FormatByteCount(enlistmentDrive.AvailableFreeSpace); - - if (string.Equals(enlistmentNormalizedPathRoot, localCacheNormalizedPathRoot, StringComparison.OrdinalIgnoreCase)) - { - this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment and local cache): " + enlistmentDriveDiskSpace); - } - else - { - this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment): " + enlistmentDriveDiskSpace); - - DriveInfo cacheDrive = new DriveInfo(localCacheRoot); - string cacheDriveDiskSpace = this.FormatByteCount(cacheDrive.AvailableFreeSpace); - this.WriteMessage("Available space on " + cacheDrive.Name + " drive(local cache): " + cacheDriveDiskSpace); - } - - this.WriteMessage(string.Empty); - } - catch (Exception e) - { - this.WriteMessage("Failed to acquire disk space information, exception: " + e.ToString()); - this.WriteMessage(string.Empty); - } - } - - private string FormatByteCount(double byteCount) - { - const int Divisor = 1024; - const string ByteCountFormat = "0.00"; - string[] unitStrings = { " B", " KB", " MB", " GB", " TB" }; - - int unitIndex = 0; - - while (byteCount >= Divisor && unitIndex < unitStrings.Length - 1) - { - unitIndex++; - byteCount = byteCount / Divisor; - } - - return byteCount.ToString(ByteCountFormat) + unitStrings[unitIndex]; - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; + +namespace Scalar.CommandLine +{ + [Verb(DiagnoseVerb.DiagnoseVerbName, HelpText = "Diagnose issues with a Scalar repo")] + public class DiagnoseVerb : ScalarVerb.ForExistingEnlistment + { + private const string DiagnoseVerbName = "diagnose"; + private const string DeprecatedUpgradeLogsDirectory = "Logs"; + + private TextWriter diagnosticLogFileWriter; + private PhysicalFileSystem fileSystem; + + public DiagnoseVerb() : base(false) + { + this.fileSystem = new PhysicalFileSystem(); + } + + protected override string VerbName + { + get { return DiagnoseVerbName; } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + string diagnosticsRoot = Path.Combine(enlistment.DotScalarRoot, "diagnostics"); + + if (!Directory.Exists(diagnosticsRoot)) + { + Directory.CreateDirectory(diagnosticsRoot); + } + + string archiveFolderPath = Path.Combine(diagnosticsRoot, "scalar_" + DateTime.Now.ToString("yyyyMMdd_HHmmss")); + Directory.CreateDirectory(archiveFolderPath); + + using (FileStream diagnosticLogFile = new FileStream(Path.Combine(archiveFolderPath, "diagnostics.log"), FileMode.CreateNew)) + using (this.diagnosticLogFileWriter = new StreamWriter(diagnosticLogFile)) + { + this.WriteMessage("Collecting diagnostic info into temp folder " + archiveFolderPath); + + this.WriteMessage(string.Empty); + this.WriteMessage("scalar version " + ProcessHelper.GetCurrentProcessVersion()); + + GitVersion gitVersion = null; + string error = null; + if (!string.IsNullOrEmpty(enlistment.GitBinPath) && GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out error)) + { + this.WriteMessage("git version " + gitVersion.ToString()); + } + else + { + this.WriteMessage("Could not determine git version. " + error); + } + + this.WriteMessage(enlistment.GitBinPath); + this.WriteMessage(string.Empty); + this.WriteMessage("Enlistment root: " + enlistment.EnlistmentRoot); + this.WriteMessage("Cache Server: " + CacheServerResolver.GetCacheServerFromConfig(enlistment)); + + string localCacheRoot; + string gitObjectsRoot; + this.GetLocalCachePaths(enlistment, out localCacheRoot, out gitObjectsRoot); + string actualLocalCacheRoot = !string.IsNullOrWhiteSpace(localCacheRoot) ? localCacheRoot : gitObjectsRoot; + this.WriteMessage("Local Cache: " + actualLocalCacheRoot); + this.WriteMessage(string.Empty); + + this.PrintDiskSpaceInfo(actualLocalCacheRoot, this.EnlistmentRootPathParameter); + + this.RecordVersionInformation(); + + this.ShowStatusWhileRunning( + () => + this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_status.txt") != ReturnCode.Success || + this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_unmount.txt", verb => verb.SkipLock = true) == ReturnCode.Success, + "Unmounting", + suppressGvfsLogMessage: true); + + this.ShowStatusWhileRunning( + () => + { + // .scalar + this.CopyAllFiles(enlistment.EnlistmentRoot, archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot, copySubFolders: false); + + // .git + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Hooks.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Info.Root, copySubFolders: false); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Logs.Root, copySubFolders: true); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Refs.Root, copySubFolders: true); + this.CopyAllFiles(enlistment.WorkingDirectoryRoot, archiveFolderPath, ScalarConstants.DotGit.Objects.Info.Root, copySubFolders: false); + this.LogDirectoryEnumeration(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, ScalarConstants.DotGit.Objects.Root), ScalarConstants.DotGit.Objects.Pack.Root, "packs-local.txt"); + this.LogLooseObjectCount(enlistment.WorkingDirectoryRoot, Path.Combine(archiveFolderPath, ScalarConstants.DotGit.Objects.Root), ScalarConstants.DotGit.Objects.Root, "objects-local.txt"); + + // databases + this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), ScalarConstants.DotScalar.Databases.Name, copySubFolders: false); + + // local cache + this.CopyLocalCacheData(archiveFolderPath, localCacheRoot, gitObjectsRoot); + + // corrupt objects + this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), ScalarConstants.DotScalar.CorruptObjectsName, copySubFolders: false); + + // service + this.CopyAllFiles( + ScalarPlatform.Instance.GetDataRootForScalar(), + archiveFolderPath, + this.ServiceName, + copySubFolders: true); + + if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarUpgrade) + { + // upgrader + this.CopyAllFiles( + ProductUpgraderInfo.GetParentLogDirectoryPath(), + archiveFolderPath, + DeprecatedUpgradeLogsDirectory, + copySubFolders: true, + targetFolderName: Path.Combine(ProductUpgraderInfo.UpgradeDirectoryName, DeprecatedUpgradeLogsDirectory)); + + this.CopyAllFiles( + ProductUpgraderInfo.GetParentLogDirectoryPath(), + archiveFolderPath, + ProductUpgraderInfo.LogDirectory, + copySubFolders: true, + targetFolderName: Path.Combine(ProductUpgraderInfo.UpgradeDirectoryName, ProductUpgraderInfo.LogDirectory)); + + this.LogDirectoryEnumeration( + ProductUpgraderInfo.GetUpgradeProtectedDataDirectory(), + Path.Combine(archiveFolderPath, ProductUpgraderInfo.UpgradeDirectoryName), + ProductUpgraderInfo.DownloadDirectory, + "downloaded-assets.txt"); + } + + if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarConfig) + { + this.CopyFile(ScalarPlatform.Instance.GetDataRootForScalar(), archiveFolderPath, LocalScalarConfig.FileName); + } + + return true; + }, + "Copying logs"); + + this.ShowStatusWhileRunning( + () => this.RunAndRecordScalarVerb(archiveFolderPath, "scalar_mount.txt") == ReturnCode.Success, + "Mounting", + suppressGvfsLogMessage: true); + + this.CopyAllFiles(enlistment.DotScalarRoot, Path.Combine(archiveFolderPath, ScalarPlatform.Instance.Constants.DotScalarRoot), "logs", copySubFolders: false); + } + + string zipFilePath = archiveFolderPath + ".zip"; + this.ShowStatusWhileRunning( + () => + { + ZipFile.CreateFromDirectory(archiveFolderPath, zipFilePath); + this.fileSystem.DeleteDirectory(archiveFolderPath); + + return true; + }, + "Creating zip file", + suppressGvfsLogMessage: true); + + this.Output.WriteLine(); + this.Output.WriteLine("Diagnostics complete. All of the gathered info, as well as all of the output above, is captured in"); + this.Output.WriteLine(zipFilePath); + } + + private void WriteMessage(string message, bool skipStdout = false) + { + message = message.TrimEnd('\r', '\n'); + + if (!skipStdout) + { + this.Output.WriteLine(message); + } + + this.diagnosticLogFileWriter.WriteLine(message); + } + + private void RecordVersionInformation() + { + string information = ScalarPlatform.Instance.GetOSVersionInformation(); + this.diagnosticLogFileWriter.WriteLine(information); + } + + private void CopyFile( + string sourceRoot, + string targetRoot, + string fileName) + { + string sourceFile = Path.Combine(sourceRoot, fileName); + string targetFile = Path.Combine(targetRoot, fileName); + + try + { + if (!File.Exists(sourceFile)) + { + return; + } + + File.Copy(sourceFile, targetFile); + } + catch (Exception e) + { + this.WriteMessage( + string.Format( + "Failed to copy file {0} in {1} with exception {2}", + fileName, + sourceRoot, + e)); + } + } + + private void CopyAllFiles( + string sourceRoot, + string targetRoot, + string folderName, + bool copySubFolders, + bool hideErrorsFromStdout = false, + string targetFolderName = null) + { + string sourceFolder = Path.Combine(sourceRoot, folderName); + string targetFolder = Path.Combine(targetRoot, targetFolderName ?? folderName); + + try + { + if (!Directory.Exists(sourceFolder)) + { + return; + } + + this.RecursiveFileCopyImpl(sourceFolder, targetFolder, copySubFolders, hideErrorsFromStdout); + } + catch (Exception e) + { + this.WriteMessage( + string.Format( + "Failed to copy folder {0} in {1} with exception {2}. copySubFolders: {3}", + folderName, + sourceRoot, + e, + copySubFolders), + hideErrorsFromStdout); + } + } + + private void GetLocalCachePaths(ScalarEnlistment enlistment, out string localCacheRoot, out string gitObjectsRoot) + { + localCacheRoot = null; + gitObjectsRoot = null; + + try + { + using (ITracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "DiagnoseVerb")) + { + string error; + if (RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot), out error)) + { + RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error); + RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error); + } + else + { + this.WriteMessage("Failed to determine local cache path and git objects root, RepoMetadata error: " + error); + } + } + } + catch (Exception e) + { + this.WriteMessage(string.Format("Failed to determine local cache path and git objects root, Exception: {0}", e)); + } + finally + { + RepoMetadata.Shutdown(); + } + } + + private void CopyLocalCacheData(string archiveFolderPath, string localCacheRoot, string gitObjectsRoot) + { + try + { + string localCacheArchivePath = Path.Combine(archiveFolderPath, ScalarConstants.DefaultScalarCacheFolderName); + Directory.CreateDirectory(localCacheArchivePath); + + if (!string.IsNullOrWhiteSpace(localCacheRoot)) + { + // Copy all mapping.dat files in the local cache folder (i.e. mapping.dat, mapping.dat.tmp, mapping.dat.lock) + foreach (string filePath in Directory.EnumerateFiles(localCacheRoot, "mapping.dat*")) + { + string fileName = Path.GetFileName(filePath); + try + { + File.Copy(filePath, Path.Combine(localCacheArchivePath, fileName)); + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to copy '{0}' from {1} to {2} with exception {3}", + fileName, + localCacheRoot, + archiveFolderPath, + e)); + } + } + } + + if (!string.IsNullOrWhiteSpace(gitObjectsRoot)) + { + this.LogDirectoryEnumeration(gitObjectsRoot, localCacheArchivePath, ScalarConstants.DotGit.Objects.Pack.Name, "packs-cached.txt"); + this.LogLooseObjectCount(gitObjectsRoot, localCacheArchivePath, string.Empty, "objects-cached.txt"); + + // Store all commit-graph files + this.CopyAllFiles(gitObjectsRoot, localCacheArchivePath, ScalarConstants.DotGit.Objects.Info.Root, copySubFolders: true); + } + } + catch (Exception e) + { + this.WriteMessage(string.Format("Failed to copy local cache data with exception: {0}", e)); + } + } + + private void LogDirectoryEnumeration(string sourceRoot, string targetRoot, string folderName, string logfile) + { + try + { + if (!Directory.Exists(targetRoot)) + { + Directory.CreateDirectory(targetRoot); + } + + string folder = Path.Combine(sourceRoot, folderName); + string targetLog = Path.Combine(targetRoot, logfile); + + List lines = new List(); + + if (Directory.Exists(folder)) + { + DirectoryInfo packDirectory = new DirectoryInfo(folder); + + lines.Add($"Contents of {folder}:"); + foreach (FileInfo file in packDirectory.EnumerateFiles()) + { + lines.Add($"{file.Name, -70} {file.Length, 16}"); + } + } + + File.WriteAllLines(targetLog, lines.ToArray()); + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to log file sizes for {0} in {1} with exception {2}. logfile: {3}", + folderName, + sourceRoot, + e, + logfile)); + } + } + + private void LogLooseObjectCount(string sourceRoot, string targetRoot, string folderName, string logfile) + { + try + { + if (!Directory.Exists(targetRoot)) + { + Directory.CreateDirectory(targetRoot); + } + + string objectFolder = Path.Combine(sourceRoot, folderName); + string targetLog = Path.Combine(targetRoot, logfile); + + List lines = new List(); + + if (Directory.Exists(objectFolder)) + { + DirectoryInfo objectDirectory = new DirectoryInfo(objectFolder); + + int countLoose = 0; + int countFolders = 0; + + lines.Add($"Object directory stats for {objectFolder}:"); + + foreach (DirectoryInfo directory in objectDirectory.EnumerateDirectories()) + { + if (GitObjects.IsLooseObjectsDirectory(directory.Name)) + { + countFolders++; + int numObjects = directory.EnumerateFiles().Count(); + lines.Add($"{directory.Name} : {numObjects, 7} objects"); + countLoose += numObjects; + } + } + + lines.Add($"Total: {countLoose} loose objects"); + } + + File.WriteAllLines(targetLog, lines.ToArray()); + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Failed to log loose object count for {0} in {1} with exception {2}. logfile: {3}", + folderName, + sourceRoot, + e, + logfile)); + } + } + + private void RecursiveFileCopyImpl(string sourcePath, string targetPath, bool copySubFolders, bool hideErrorsFromStdout) + { + if (!Directory.Exists(targetPath)) + { + Directory.CreateDirectory(targetPath); + } + + foreach (string filePath in Directory.EnumerateFiles(sourcePath)) + { + string fileName = Path.GetFileName(filePath); + try + { + string sourceFilePath = Path.Combine(sourcePath, fileName); + if (!ScalarPlatform.Instance.FileSystem.IsSocket(sourceFilePath) && + !ScalarPlatform.Instance.FileSystem.IsExecutable(sourceFilePath)) + { + File.Copy( + Path.Combine(sourcePath, fileName), + Path.Combine(targetPath, fileName)); + } + } + catch (Exception e) + { + this.WriteMessage( + string.Format( + "Failed to copy '{0}' in {1} with exception {2}", + fileName, + sourcePath, + e), + hideErrorsFromStdout); + } + } + + if (copySubFolders) + { + DirectoryInfo dir = new DirectoryInfo(sourcePath); + foreach (DirectoryInfo subdir in dir.GetDirectories()) + { + string targetFolderPath = Path.Combine(targetPath, subdir.Name); + try + { + this.RecursiveFileCopyImpl(subdir.FullName, targetFolderPath, copySubFolders, hideErrorsFromStdout); + } + catch (Exception e) + { + this.WriteMessage( + string.Format( + "Failed to copy subfolder '{0}' to '{1}' with exception {2}", + subdir.FullName, + targetFolderPath, + e), + hideErrorsFromStdout); + } + } + } + } + + private ReturnCode RunAndRecordScalarVerb(string archiveFolderPath, string outputFileName, Action configureVerb = null) + where TVerb : ScalarVerb, new() + { + try + { + using (FileStream file = new FileStream(Path.Combine(archiveFolderPath, outputFileName), FileMode.CreateNew)) + using (StreamWriter writer = new StreamWriter(file)) + { + return this.Execute( + this.EnlistmentRootPathParameter, + verb => + { + if (configureVerb != null) + { + configureVerb(verb); + } + + verb.Output = writer; + }); + } + } + catch (Exception e) + { + this.WriteMessage(string.Format( + "Verb {0} failed with exception {1}", + typeof(TVerb), + e)); + + return ReturnCode.GenericError; + } + } + + private void PrintDiskSpaceInfo(string localCacheRoot, string enlistmentRootParameter) + { + try + { + string enlistmentNormalizedPathRoot; + string localCacheNormalizedPathRoot; + string enlistmentErrorMessage; + string localCacheErrorMessage; + + bool enlistmentSuccess = ScalarPlatform.Instance.TryGetNormalizedPathRoot(enlistmentRootParameter, out enlistmentNormalizedPathRoot, out enlistmentErrorMessage); + bool localCacheSuccess = ScalarPlatform.Instance.TryGetNormalizedPathRoot(localCacheRoot, out localCacheNormalizedPathRoot, out localCacheErrorMessage); + + if (!enlistmentSuccess || !localCacheSuccess) + { + this.WriteMessage("Failed to acquire disk space information:"); + if (!string.IsNullOrEmpty(enlistmentErrorMessage)) + { + this.WriteMessage(enlistmentErrorMessage); + } + + if (!string.IsNullOrEmpty(localCacheErrorMessage)) + { + this.WriteMessage(localCacheErrorMessage); + } + + this.WriteMessage(string.Empty); + return; + } + + DriveInfo enlistmentDrive = new DriveInfo(enlistmentNormalizedPathRoot); + string enlistmentDriveDiskSpace = this.FormatByteCount(enlistmentDrive.AvailableFreeSpace); + + if (string.Equals(enlistmentNormalizedPathRoot, localCacheNormalizedPathRoot, StringComparison.OrdinalIgnoreCase)) + { + this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment and local cache): " + enlistmentDriveDiskSpace); + } + else + { + this.WriteMessage("Available space on " + enlistmentDrive.Name + " drive(enlistment): " + enlistmentDriveDiskSpace); + + DriveInfo cacheDrive = new DriveInfo(localCacheRoot); + string cacheDriveDiskSpace = this.FormatByteCount(cacheDrive.AvailableFreeSpace); + this.WriteMessage("Available space on " + cacheDrive.Name + " drive(local cache): " + cacheDriveDiskSpace); + } + + this.WriteMessage(string.Empty); + } + catch (Exception e) + { + this.WriteMessage("Failed to acquire disk space information, exception: " + e.ToString()); + this.WriteMessage(string.Empty); + } + } + + private string FormatByteCount(double byteCount) + { + const int Divisor = 1024; + const string ByteCountFormat = "0.00"; + string[] unitStrings = { " B", " KB", " MB", " GB", " TB" }; + + int unitIndex = 0; + + while (byteCount >= Divisor && unitIndex < unitStrings.Length - 1) + { + unitIndex++; + byteCount = byteCount / Divisor; + } + + return byteCount.ToString(ByteCountFormat) + unitStrings[unitIndex]; + } + } +} diff --git a/Scalar/CommandLine/LogVerb.cs b/Scalar/CommandLine/LogVerb.cs index 43bc4db353..0dfc8cb589 100644 --- a/Scalar/CommandLine/LogVerb.cs +++ b/Scalar/CommandLine/LogVerb.cs @@ -1,143 +1,143 @@ -using CommandLine; -using Scalar.Common; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.CommandLine -{ - [Verb(LogVerb.LogVerbName, HelpText = "Show the most recent Scalar log files")] - public class LogVerb : ScalarVerb - { - private const string LogVerbName = "log"; - private static readonly int LogNameConsoleOutputFormatWidth = GetMaxLogNameLength(); - - [Value( - 0, - Required = false, - Default = "", - MetaName = "Enlistment Root Path", - HelpText = "Full or relative path to the Scalar enlistment root")] - public override string EnlistmentRootPathParameter { get; set; } - - [Option( - "type", - Default = null, - HelpText = "The type of log file to display on the console")] - public string LogType { get; set; } - - protected override string VerbName - { - get { return LogVerbName; } - } - - public override void Execute() - { - this.ValidatePathParameter(this.EnlistmentRootPathParameter); - - this.Output.WriteLine("Most recent log files:"); - - string errorMessage; - string enlistmentRoot; - if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) - { - this.ReportErrorAndExit( - "Error: '{0}' is not a valid Scalar enlistment", - this.EnlistmentRootPathParameter); - } - - string scalarLogsRoot = Path.Combine( - enlistmentRoot, - ScalarPlatform.Instance.Constants.DotScalarRoot, - ScalarConstants.DotScalar.LogName); - - if (this.LogType == null) - { - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Clone); - - // By using MountPrefix ("mount") DisplayMostRecent will display either mount_verb, mount_upgrade, or mount_process, whichever is more recent - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.MountPrefix); - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Prefetch); - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Dehydrate); - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Repair); - this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Sparse); - - string serviceLogsRoot = Path.Combine( - ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.ServiceName), - ScalarConstants.Service.LogDirectory); - this.DisplayMostRecent(serviceLogsRoot, ScalarConstants.LogFileTypes.Service); - - this.DisplayMostRecent(ProductUpgraderInfo.GetLogDirectoryPath(), ScalarConstants.LogFileTypes.UpgradePrefix); - } - else - { - string logFile = FindNewestFileInFolder(scalarLogsRoot, this.LogType); - if (logFile == null) - { - this.ReportErrorAndExit("No log file found"); - } - else - { - foreach (string line in File.ReadAllLines(logFile)) - { - this.Output.WriteLine(line); - } - } - } - } - - private static string FindNewestFileInFolder(string folderName, string logFileType) - { - string logFilePattern = GetLogFilePatternForType(logFileType); - - DirectoryInfo logDirectory = new DirectoryInfo(folderName); - if (!logDirectory.Exists) - { - return null; - } - - FileInfo[] files = logDirectory.GetFiles(logFilePattern ?? "*"); - if (files.Length == 0) - { - return null; - } - - return - files - .OrderByDescending(fileInfo => fileInfo.CreationTime) - .First() - .FullName; - } - - private static string GetLogFilePatternForType(string logFileType) - { - return "scalar_" + logFileType + "_*.log"; - } - - private static int GetMaxLogNameLength() - { - List lognames = new List - { - ScalarConstants.LogFileTypes.Clone, - ScalarConstants.LogFileTypes.MountPrefix, - ScalarConstants.LogFileTypes.Prefetch, - ScalarConstants.LogFileTypes.Dehydrate, - ScalarConstants.LogFileTypes.Repair, - ScalarConstants.LogFileTypes.Sparse, - ScalarConstants.LogFileTypes.Service, - ScalarConstants.LogFileTypes.UpgradePrefix, - }; - - return lognames.Max(s => s.Length) + 1; - } - - private void DisplayMostRecent(string logFolder, string logFileType) - { - string logFile = FindNewestFileInFolder(logFolder, logFileType); - this.Output.WriteLine( - $" {{0, -{LogNameConsoleOutputFormatWidth}}}: {{1}}", - logFileType, - logFile == null ? "None" : logFile); - } - } -} +using CommandLine; +using Scalar.Common; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.CommandLine +{ + [Verb(LogVerb.LogVerbName, HelpText = "Show the most recent Scalar log files")] + public class LogVerb : ScalarVerb + { + private const string LogVerbName = "log"; + private static readonly int LogNameConsoleOutputFormatWidth = GetMaxLogNameLength(); + + [Value( + 0, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the Scalar enlistment root")] + public override string EnlistmentRootPathParameter { get; set; } + + [Option( + "type", + Default = null, + HelpText = "The type of log file to display on the console")] + public string LogType { get; set; } + + protected override string VerbName + { + get { return LogVerbName; } + } + + public override void Execute() + { + this.ValidatePathParameter(this.EnlistmentRootPathParameter); + + this.Output.WriteLine("Most recent log files:"); + + string errorMessage; + string enlistmentRoot; + if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid Scalar enlistment", + this.EnlistmentRootPathParameter); + } + + string scalarLogsRoot = Path.Combine( + enlistmentRoot, + ScalarPlatform.Instance.Constants.DotScalarRoot, + ScalarConstants.DotScalar.LogName); + + if (this.LogType == null) + { + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Clone); + + // By using MountPrefix ("mount") DisplayMostRecent will display either mount_verb, mount_upgrade, or mount_process, whichever is more recent + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.MountPrefix); + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Prefetch); + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Dehydrate); + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Repair); + this.DisplayMostRecent(scalarLogsRoot, ScalarConstants.LogFileTypes.Sparse); + + string serviceLogsRoot = Path.Combine( + ScalarPlatform.Instance.GetDataRootForScalarComponent(ScalarConstants.Service.ServiceName), + ScalarConstants.Service.LogDirectory); + this.DisplayMostRecent(serviceLogsRoot, ScalarConstants.LogFileTypes.Service); + + this.DisplayMostRecent(ProductUpgraderInfo.GetLogDirectoryPath(), ScalarConstants.LogFileTypes.UpgradePrefix); + } + else + { + string logFile = FindNewestFileInFolder(scalarLogsRoot, this.LogType); + if (logFile == null) + { + this.ReportErrorAndExit("No log file found"); + } + else + { + foreach (string line in File.ReadAllLines(logFile)) + { + this.Output.WriteLine(line); + } + } + } + } + + private static string FindNewestFileInFolder(string folderName, string logFileType) + { + string logFilePattern = GetLogFilePatternForType(logFileType); + + DirectoryInfo logDirectory = new DirectoryInfo(folderName); + if (!logDirectory.Exists) + { + return null; + } + + FileInfo[] files = logDirectory.GetFiles(logFilePattern ?? "*"); + if (files.Length == 0) + { + return null; + } + + return + files + .OrderByDescending(fileInfo => fileInfo.CreationTime) + .First() + .FullName; + } + + private static string GetLogFilePatternForType(string logFileType) + { + return "scalar_" + logFileType + "_*.log"; + } + + private static int GetMaxLogNameLength() + { + List lognames = new List + { + ScalarConstants.LogFileTypes.Clone, + ScalarConstants.LogFileTypes.MountPrefix, + ScalarConstants.LogFileTypes.Prefetch, + ScalarConstants.LogFileTypes.Dehydrate, + ScalarConstants.LogFileTypes.Repair, + ScalarConstants.LogFileTypes.Sparse, + ScalarConstants.LogFileTypes.Service, + ScalarConstants.LogFileTypes.UpgradePrefix, + }; + + return lognames.Max(s => s.Length) + 1; + } + + private void DisplayMostRecent(string logFolder, string logFileType) + { + string logFile = FindNewestFileInFolder(logFolder, logFileType); + this.Output.WriteLine( + $" {{0, -{LogNameConsoleOutputFormatWidth}}}: {{1}}", + logFileType, + logFile == null ? "None" : logFile); + } + } +} diff --git a/Scalar/CommandLine/MountVerb.cs b/Scalar/CommandLine/MountVerb.cs index 4e495f5ed7..63b68132c0 100644 --- a/Scalar/CommandLine/MountVerb.cs +++ b/Scalar/CommandLine/MountVerb.cs @@ -1,329 +1,329 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.DiskLayoutUpgrades; -using System; -using System.IO; - -namespace Scalar.CommandLine -{ - [Verb(MountVerb.MountVerbName, HelpText = "Mount a Scalar virtual repo")] - public class MountVerb : ScalarVerb.ForExistingEnlistment - { - private const string MountVerbName = "mount"; - - [Option( - 'v', - ScalarConstants.VerbParameters.Mount.Verbosity, - Default = ScalarConstants.VerbParameters.Mount.DefaultVerbosity, - Required = false, - HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] - public string Verbosity { get; set; } - - [Option( - 'k', - ScalarConstants.VerbParameters.Mount.Keywords, - Default = ScalarConstants.VerbParameters.Mount.DefaultKeywords, - Required = false, - HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] - public string KeywordsCsv { get; set; } - - public bool SkipMountedCheck { get; set; } - public bool SkipVersionCheck { get; set; } - public CacheServerInfo ResolvedCacheServer { get; set; } - public ServerScalarConfig DownloadedScalarConfig { get; set; } - - protected override string VerbName - { - get { return MountVerbName; } - } - - public override void InitializeDefaultParameterValues() - { - this.Verbosity = ScalarConstants.VerbParameters.Mount.DefaultVerbosity; - this.KeywordsCsv = ScalarConstants.VerbParameters.Mount.DefaultKeywords; - } - - protected override void PreCreateEnlistment() - { - string errorMessage; - string enlistmentRoot; - if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) - { - this.ReportErrorAndExit("Error: '{0}' is not a valid Scalar enlistment", this.EnlistmentRootPathParameter); - } - - if (!this.SkipMountedCheck) - { - if (this.IsExistingPipeListening(enlistmentRoot)) - { - this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.Success, error: $"The repo at '{enlistmentRoot}' is already mounted."); - } - } - - if (!DiskLayoutUpgrade.TryRunAllUpgrades(enlistmentRoot)) - { - this.ReportErrorAndExit("Failed to upgrade repo disk layout. " + ConsoleHelper.GetScalarLogMessage(enlistmentRoot)); - } - - string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistmentRoot, error: out error)) - { - this.ReportErrorAndExit("Error: " + error); - } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - string errorMessage = null; - string mountExecutableLocation = null; - using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "ExecuteMount")) - { - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); - ScalarContext context = new ScalarContext(tracer, fileSystem, gitRepo, enlistment); - - if (!HooksInstaller.InstallHooks(context, out errorMessage)) - { - this.ReportErrorAndExit("Error installing hooks: " + errorMessage); - } - - CacheServerInfo cacheServer = this.ResolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); - - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.MountVerb), - EventLevel.Verbose, - Keywords.Any); - tracer.WriteStartEvent( - enlistment.EnlistmentRoot, - enlistment.RepoUrl, - cacheServer.Url, - new EventMetadata - { - { "Unattended", this.Unattended }, - { "IsElevated", ScalarPlatform.Instance.IsElevated() }, - { "NamedPipeName", enlistment.NamedPipeName }, - { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, - }); - - RetryConfig retryConfig = null; - ServerScalarConfig serverScalarConfig = this.DownloadedScalarConfig; - if (!this.SkipVersionCheck) - { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.Output.WriteLine(" WARNING: " + authErrorMessage); - this.Output.WriteLine(" Mount will proceed, but new files cannot be accessed until Scalar can authenticate."); - } - - if (serverScalarConfig == null) - { - if (retryConfig == null) - { - retryConfig = this.GetRetryConfig(tracer, enlistment); - } - - serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); - } - - this.ValidateClientVersions(tracer, enlistment, serverScalarConfig, showWarnings: true); - - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverScalarConfig); - this.Output.WriteLine("Configured cache server: " + cacheServer); - } - - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); - - if (!this.ShowStatusWhileRunning( - () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, - "Validating repo")) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.SkipVersionCheck) - { - string error; - if (!RepoMetadata.TryInitialize(tracer, enlistment.DotScalarRoot, out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - try - { - GitProcess git = new GitProcess(enlistment); - this.LogEnlistmentInfoAndSetConfigValues(tracer, git, enlistment); - } - finally - { - RepoMetadata.Shutdown(); - } - } - - if (!this.ShowStatusWhileRunning( - () => { return this.TryMount(tracer, enlistment, mountExecutableLocation, out errorMessage); }, - "Mounting")) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - - if (!this.Unattended && ScalarPlatform.Instance.UnderConstruction.SupportsScalarService) - { - tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); - - if (this.ShowStatusWhileRunning( - () => { return this.RegisterMount(enlistment, out errorMessage); }, - "Registering for automount")) - { - tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); - } - else - { - this.Output.WriteLine(" WARNING: " + errorMessage); - tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register for automount"); - } - } - } - } - - private bool PerformPreMountValidation(ITracer tracer, ScalarEnlistment enlistment, out string mountExecutableLocation, out string errorMessage) - { - errorMessage = string.Empty; - mountExecutableLocation = string.Empty; - - // We have to parse these parameters here to make sure they are valid before - // handing them to the background process which cannot tell the user when they are bad - EventLevel verbosity; - Keywords keywords; - this.ParseEnumArgs(out verbosity, out keywords); - - mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), ScalarPlatform.Instance.Constants.MountExecutableName); - if (!File.Exists(mountExecutableLocation)) - { - errorMessage = $"Could not find {ScalarPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall Scalar."; - return false; - } - - GitProcess git = new GitProcess(enlistment); - if (!git.IsValidRepo()) - { - errorMessage = "The .git folder is missing or has invalid contents"; - return false; - } - - if (!ScalarPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string error)) - { - errorMessage = $"FileSystem unsupported: {error}"; - return false; - } - - return true; - } - - private bool TryMount(ITracer tracer, ScalarEnlistment enlistment, string mountExecutableLocation, out string errorMessage) - { - if (!ScalarVerb.TrySetRequiredGitConfigSettings(enlistment)) - { - errorMessage = "Unable to configure git repo"; - return false; - } - - const string ParamPrefix = "--"; - - tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {enlistment.EnlistmentRoot}"); - - ScalarPlatform.Instance.StartBackgroundScalarProcess( - tracer, - mountExecutableLocation, - new[] - { - enlistment.EnlistmentRoot, - ParamPrefix + ScalarConstants.VerbParameters.Mount.Verbosity, - this.Verbosity, - ParamPrefix + ScalarConstants.VerbParameters.Mount.Keywords, - this.KeywordsCsv, - ParamPrefix + ScalarConstants.VerbParameters.Mount.StartedByService, - this.StartedByService.ToString(), - ParamPrefix + ScalarConstants.VerbParameters.Mount.StartedByVerb, - true.ToString() - }); - - tracer.RelatedInfo($"{nameof(this.TryMount)}: Waiting for repo to be mounted"); - return ScalarEnlistment.WaitUntilMounted(tracer, enlistment.EnlistmentRoot, this.Unattended, out errorMessage); - } - - private bool RegisterMount(ScalarEnlistment enlistment, out string errorMessage) - { - errorMessage = string.Empty; - - NamedPipeMessages.RegisterRepoRequest request = new NamedPipeMessages.RegisterRepoRequest(); - request.EnlistmentRoot = enlistment.EnlistmentRoot; - - request.OwnerSID = ScalarPlatform.Instance.GetCurrentUser(); - - using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) - { - if (!client.Connect()) - { - errorMessage = "Unable to register repo because Scalar.Service is not responding."; - return false; - } - - 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); - - if (!string.IsNullOrEmpty(message.ErrorMessage)) - { - errorMessage = message.ErrorMessage; - return false; - } - - 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; - } - } - } - - private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) - { - if (!Enum.TryParse(this.KeywordsCsv, out keywords)) - { - this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); - } - - if (!Enum.TryParse(this.Verbosity, out verbosity)) - { - this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); - } - } - } +using CommandLine; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.DiskLayoutUpgrades; +using System; +using System.IO; + +namespace Scalar.CommandLine +{ + [Verb(MountVerb.MountVerbName, HelpText = "Mount a Scalar virtual repo")] + public class MountVerb : ScalarVerb.ForExistingEnlistment + { + private const string MountVerbName = "mount"; + + [Option( + 'v', + ScalarConstants.VerbParameters.Mount.Verbosity, + Default = ScalarConstants.VerbParameters.Mount.DefaultVerbosity, + Required = false, + HelpText = "Sets the verbosity of console logging. Accepts: Verbose, Informational, Warning, Error")] + public string Verbosity { get; set; } + + [Option( + 'k', + ScalarConstants.VerbParameters.Mount.Keywords, + Default = ScalarConstants.VerbParameters.Mount.DefaultKeywords, + Required = false, + HelpText = "A CSV list of logging filter keywords. Accepts: Any, Network")] + public string KeywordsCsv { get; set; } + + public bool SkipMountedCheck { get; set; } + public bool SkipVersionCheck { get; set; } + public CacheServerInfo ResolvedCacheServer { get; set; } + public ServerScalarConfig DownloadedScalarConfig { get; set; } + + protected override string VerbName + { + get { return MountVerbName; } + } + + public override void InitializeDefaultParameterValues() + { + this.Verbosity = ScalarConstants.VerbParameters.Mount.DefaultVerbosity; + this.KeywordsCsv = ScalarConstants.VerbParameters.Mount.DefaultKeywords; + } + + protected override void PreCreateEnlistment() + { + string errorMessage; + string enlistmentRoot; + if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) + { + this.ReportErrorAndExit("Error: '{0}' is not a valid Scalar enlistment", this.EnlistmentRootPathParameter); + } + + if (!this.SkipMountedCheck) + { + if (this.IsExistingPipeListening(enlistmentRoot)) + { + this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.Success, error: $"The repo at '{enlistmentRoot}' is already mounted."); + } + } + + if (!DiskLayoutUpgrade.TryRunAllUpgrades(enlistmentRoot)) + { + this.ReportErrorAndExit("Failed to upgrade repo disk layout. " + ConsoleHelper.GetScalarLogMessage(enlistmentRoot)); + } + + string error; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistmentRoot, error: out error)) + { + this.ReportErrorAndExit("Error: " + error); + } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + string errorMessage = null; + string mountExecutableLocation = null; + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "ExecuteMount")) + { + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + GitRepo gitRepo = new GitRepo(tracer, enlistment, fileSystem); + ScalarContext context = new ScalarContext(tracer, fileSystem, gitRepo, enlistment); + + if (!HooksInstaller.InstallHooks(context, out errorMessage)) + { + this.ReportErrorAndExit("Error installing hooks: " + errorMessage); + } + + CacheServerInfo cacheServer = this.ResolvedCacheServer ?? CacheServerResolver.GetCacheServerFromConfig(enlistment); + + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.MountVerb), + EventLevel.Verbose, + Keywords.Any); + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + cacheServer.Url, + new EventMetadata + { + { "Unattended", this.Unattended }, + { "IsElevated", ScalarPlatform.Instance.IsElevated() }, + { "NamedPipeName", enlistment.NamedPipeName }, + { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, + }); + + RetryConfig retryConfig = null; + ServerScalarConfig serverScalarConfig = this.DownloadedScalarConfig; + if (!this.SkipVersionCheck) + { + string authErrorMessage; + if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) + { + this.Output.WriteLine(" WARNING: " + authErrorMessage); + this.Output.WriteLine(" Mount will proceed, but new files cannot be accessed until Scalar can authenticate."); + } + + if (serverScalarConfig == null) + { + if (retryConfig == null) + { + retryConfig = this.GetRetryConfig(tracer, enlistment); + } + + serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); + } + + this.ValidateClientVersions(tracer, enlistment, serverScalarConfig, showWarnings: true); + + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverScalarConfig); + this.Output.WriteLine("Configured cache server: " + cacheServer); + } + + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); + + if (!this.ShowStatusWhileRunning( + () => { return this.PerformPreMountValidation(tracer, enlistment, out mountExecutableLocation, out errorMessage); }, + "Validating repo")) + { + this.ReportErrorAndExit(tracer, errorMessage); + } + + if (!this.SkipVersionCheck) + { + string error; + if (!RepoMetadata.TryInitialize(tracer, enlistment.DotScalarRoot, out error)) + { + this.ReportErrorAndExit(tracer, error); + } + + try + { + GitProcess git = new GitProcess(enlistment); + this.LogEnlistmentInfoAndSetConfigValues(tracer, git, enlistment); + } + finally + { + RepoMetadata.Shutdown(); + } + } + + if (!this.ShowStatusWhileRunning( + () => { return this.TryMount(tracer, enlistment, mountExecutableLocation, out errorMessage); }, + "Mounting")) + { + this.ReportErrorAndExit(tracer, errorMessage); + } + + if (!this.Unattended && ScalarPlatform.Instance.UnderConstruction.SupportsScalarService) + { + tracer.RelatedInfo($"{nameof(this.Execute)}: Registering for automount"); + + if (this.ShowStatusWhileRunning( + () => { return this.RegisterMount(enlistment, out errorMessage); }, + "Registering for automount")) + { + tracer.RelatedInfo($"{nameof(this.Execute)}: Registered for automount"); + } + else + { + this.Output.WriteLine(" WARNING: " + errorMessage); + tracer.RelatedInfo($"{nameof(this.Execute)}: Failed to register for automount"); + } + } + } + } + + private bool PerformPreMountValidation(ITracer tracer, ScalarEnlistment enlistment, out string mountExecutableLocation, out string errorMessage) + { + errorMessage = string.Empty; + mountExecutableLocation = string.Empty; + + // We have to parse these parameters here to make sure they are valid before + // handing them to the background process which cannot tell the user when they are bad + EventLevel verbosity; + Keywords keywords; + this.ParseEnumArgs(out verbosity, out keywords); + + mountExecutableLocation = Path.Combine(ProcessHelper.GetCurrentProcessLocation(), ScalarPlatform.Instance.Constants.MountExecutableName); + if (!File.Exists(mountExecutableLocation)) + { + errorMessage = $"Could not find {ScalarPlatform.Instance.Constants.MountExecutableName}. You may need to reinstall Scalar."; + return false; + } + + GitProcess git = new GitProcess(enlistment); + if (!git.IsValidRepo()) + { + errorMessage = "The .git folder is missing or has invalid contents"; + return false; + } + + if (!ScalarPlatform.Instance.FileSystem.IsFileSystemSupported(enlistment.EnlistmentRoot, out string error)) + { + errorMessage = $"FileSystem unsupported: {error}"; + return false; + } + + return true; + } + + private bool TryMount(ITracer tracer, ScalarEnlistment enlistment, string mountExecutableLocation, out string errorMessage) + { + if (!ScalarVerb.TrySetRequiredGitConfigSettings(enlistment)) + { + errorMessage = "Unable to configure git repo"; + return false; + } + + const string ParamPrefix = "--"; + + tracer.RelatedInfo($"{nameof(this.TryMount)}: Launching background process('{mountExecutableLocation}') for {enlistment.EnlistmentRoot}"); + + ScalarPlatform.Instance.StartBackgroundScalarProcess( + tracer, + mountExecutableLocation, + new[] + { + enlistment.EnlistmentRoot, + ParamPrefix + ScalarConstants.VerbParameters.Mount.Verbosity, + this.Verbosity, + ParamPrefix + ScalarConstants.VerbParameters.Mount.Keywords, + this.KeywordsCsv, + ParamPrefix + ScalarConstants.VerbParameters.Mount.StartedByService, + this.StartedByService.ToString(), + ParamPrefix + ScalarConstants.VerbParameters.Mount.StartedByVerb, + true.ToString() + }); + + tracer.RelatedInfo($"{nameof(this.TryMount)}: Waiting for repo to be mounted"); + return ScalarEnlistment.WaitUntilMounted(tracer, enlistment.EnlistmentRoot, this.Unattended, out errorMessage); + } + + private bool RegisterMount(ScalarEnlistment enlistment, out string errorMessage) + { + errorMessage = string.Empty; + + NamedPipeMessages.RegisterRepoRequest request = new NamedPipeMessages.RegisterRepoRequest(); + request.EnlistmentRoot = enlistment.EnlistmentRoot; + + request.OwnerSID = ScalarPlatform.Instance.GetCurrentUser(); + + using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) + { + if (!client.Connect()) + { + errorMessage = "Unable to register repo because Scalar.Service is not responding."; + return false; + } + + 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); + + if (!string.IsNullOrEmpty(message.ErrorMessage)) + { + errorMessage = message.ErrorMessage; + return false; + } + + 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; + } + } + } + + private void ParseEnumArgs(out EventLevel verbosity, out Keywords keywords) + { + if (!Enum.TryParse(this.KeywordsCsv, out keywords)) + { + this.ReportErrorAndExit("Error: Invalid logging filter keywords: " + this.KeywordsCsv); + } + + if (!Enum.TryParse(this.Verbosity, out verbosity)) + { + this.ReportErrorAndExit("Error: Invalid logging verbosity: " + this.Verbosity); + } + } + } } diff --git a/Scalar/CommandLine/PrefetchVerb.cs b/Scalar/CommandLine/PrefetchVerb.cs index 203176b3b7..0562a82a40 100644 --- a/Scalar/CommandLine/PrefetchVerb.cs +++ b/Scalar/CommandLine/PrefetchVerb.cs @@ -1,461 +1,461 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.Maintenance; -using Scalar.Common.Prefetch; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.CommandLine -{ - [Verb(PrefetchVerb.PrefetchVerbName, HelpText = "Prefetch remote objects for the current head")] - public class PrefetchVerb : ScalarVerb.ForExistingEnlistment - { - private const string PrefetchVerbName = "prefetch"; - - private const int LockWaitTimeMs = 100; - private const int WaitingOnLockLogThreshold = 50; - private const int IoFailureRetryDelayMS = 50; - private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; - - private const int ChunkSize = 4000; - private static readonly int SearchThreadCount = Environment.ProcessorCount; - private static readonly int DownloadThreadCount = Environment.ProcessorCount; - private static readonly int IndexThreadCount = Environment.ProcessorCount; - - [Option( - "files", - Required = false, - Default = "", - HelpText = "A semicolon-delimited list of files to fetch. Simple prefix wildcards, e.g. *.txt, are supported.")] - public string Files { get; set; } - - [Option( - "folders", - Required = false, - Default = "", - HelpText = "A semicolon-delimited list of folders to fetch. Wildcards are not supported.")] - public string Folders { get; set; } - - [Option( - "folders-list", - Required = false, - Default = "", - HelpText = "A file containing line-delimited list of folders to fetch. Wildcards are not supported.")] - public string FoldersListFile { get; set; } - - [Option( - "stdin-files-list", - Required = false, - Default = false, - HelpText = "Specify this flag to load file list from stdin. Same format as when loading from file.")] +using CommandLine; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.Maintenance; +using Scalar.Common.Prefetch; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.CommandLine +{ + [Verb(PrefetchVerb.PrefetchVerbName, HelpText = "Prefetch remote objects for the current head")] + public class PrefetchVerb : ScalarVerb.ForExistingEnlistment + { + private const string PrefetchVerbName = "prefetch"; + + private const int LockWaitTimeMs = 100; + private const int WaitingOnLockLogThreshold = 50; + private const int IoFailureRetryDelayMS = 50; + private const string PrefetchCommitsAndTreesLock = "prefetch-commits-trees.lock"; + + private const int ChunkSize = 4000; + private static readonly int SearchThreadCount = Environment.ProcessorCount; + private static readonly int DownloadThreadCount = Environment.ProcessorCount; + private static readonly int IndexThreadCount = Environment.ProcessorCount; + + [Option( + "files", + Required = false, + Default = "", + HelpText = "A semicolon-delimited list of files to fetch. Simple prefix wildcards, e.g. *.txt, are supported.")] + public string Files { get; set; } + + [Option( + "folders", + Required = false, + Default = "", + HelpText = "A semicolon-delimited list of folders to fetch. Wildcards are not supported.")] + public string Folders { get; set; } + + [Option( + "folders-list", + Required = false, + Default = "", + HelpText = "A file containing line-delimited list of folders to fetch. Wildcards are not supported.")] + public string FoldersListFile { get; set; } + + [Option( + "stdin-files-list", + Required = false, + Default = false, + HelpText = "Specify this flag to load file list from stdin. Same format as when loading from file.")] public bool FilesFromStdIn { get; set; } [Option( "stdin-folders-list", Required = false, Default = false, - HelpText = "Specify this flag to load folder list from stdin. Same format as when loading from file.")] - public bool FoldersFromStdIn { get; set; } - - [Option( - "files-list", - Required = false, - Default = "", - HelpText = "A file containing line-delimited list of files to fetch. Wildcards are supported.")] - public string FilesListFile { get; set; } - - [Option( - "hydrate", - Required = false, - Default = false, - HelpText = "Specify this flag to also hydrate files in the working directory.")] - public bool HydrateFiles { get; set; } - - [Option( - 'c', - "commits", - Required = false, - Default = false, - HelpText = "Fetch the latest set of commit and tree packs. This option cannot be used with any of the file- or folder-related options.")] - public bool Commits { get; set; } - - [Option( - "verbose", - Required = false, - Default = false, - HelpText = "Show all outputs on the console in addition to writing them to a log file.")] - public bool Verbose { get; set; } - - public bool SkipVersionCheck { get; set; } - public CacheServerInfo ResolvedCacheServer { get; set; } - public ServerScalarConfig ServerScalarConfig { get; set; } - - protected override string VerbName - { - get { return PrefetchVerbName; } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "Prefetch")) - { - if (this.Verbose) - { - tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); - } - - string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); - - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Prefetch), - EventLevel.Informational, - Keywords.Any); - tracer.WriteStartEvent( - enlistment.EnlistmentRoot, - enlistment.RepoUrl, - cacheServerUrl); - - try - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Commits", this.Commits); - metadata.Add("Files", this.Files); - metadata.Add("Folders", this.Folders); - metadata.Add("FileListFile", this.FilesListFile); + HelpText = "Specify this flag to load folder list from stdin. Same format as when loading from file.")] + public bool FoldersFromStdIn { get; set; } + + [Option( + "files-list", + Required = false, + Default = "", + HelpText = "A file containing line-delimited list of files to fetch. Wildcards are supported.")] + public string FilesListFile { get; set; } + + [Option( + "hydrate", + Required = false, + Default = false, + HelpText = "Specify this flag to also hydrate files in the working directory.")] + public bool HydrateFiles { get; set; } + + [Option( + 'c', + "commits", + Required = false, + Default = false, + HelpText = "Fetch the latest set of commit and tree packs. This option cannot be used with any of the file- or folder-related options.")] + public bool Commits { get; set; } + + [Option( + "verbose", + Required = false, + Default = false, + HelpText = "Show all outputs on the console in addition to writing them to a log file.")] + public bool Verbose { get; set; } + + public bool SkipVersionCheck { get; set; } + public CacheServerInfo ResolvedCacheServer { get; set; } + public ServerScalarConfig ServerScalarConfig { get; set; } + + protected override string VerbName + { + get { return PrefetchVerbName; } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "Prefetch")) + { + if (this.Verbose) + { + tracer.AddDiagnosticConsoleEventListener(EventLevel.Informational, Keywords.Any); + } + + string cacheServerUrl = CacheServerResolver.GetUrlFromConfig(enlistment); + + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Prefetch), + EventLevel.Informational, + Keywords.Any); + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + cacheServerUrl); + + try + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Commits", this.Commits); + metadata.Add("Files", this.Files); + metadata.Add("Folders", this.Folders); + metadata.Add("FileListFile", this.FilesListFile); metadata.Add("FoldersListFile", this.FoldersListFile); metadata.Add("FilesFromStdIn", this.FilesFromStdIn); - metadata.Add("FoldersFromStdIn", this.FoldersFromStdIn); - metadata.Add("HydrateFiles", this.HydrateFiles); - tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); - - if (this.Commits) - { - if (!string.IsNullOrWhiteSpace(this.Files) || - !string.IsNullOrWhiteSpace(this.Folders) || - !string.IsNullOrWhiteSpace(this.FoldersListFile) || - !string.IsNullOrWhiteSpace(this.FilesListFile) || - this.FilesFromStdIn || - this.FoldersFromStdIn) - { - this.ReportErrorAndExit(tracer, "You cannot prefetch commits and blobs at the same time."); - } - - if (this.HydrateFiles) - { - this.ReportErrorAndExit(tracer, "You can only specify --hydrate with --files or --folders"); - } - - GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; - this.InitializeServerConnection( - tracer, - enlistment, - cacheServerUrl, - out objectRequestor, - out cacheServer); - this.PrefetchCommits(tracer, enlistment, objectRequestor, cacheServer); - } - else - { - string headCommitId; - List filesList; - List foldersList; - FileBasedDictionary lastPrefetchArgs; - + metadata.Add("FoldersFromStdIn", this.FoldersFromStdIn); + metadata.Add("HydrateFiles", this.HydrateFiles); + tracer.RelatedEvent(EventLevel.Informational, "PerformPrefetch", metadata); + + if (this.Commits) + { + if (!string.IsNullOrWhiteSpace(this.Files) || + !string.IsNullOrWhiteSpace(this.Folders) || + !string.IsNullOrWhiteSpace(this.FoldersListFile) || + !string.IsNullOrWhiteSpace(this.FilesListFile) || + this.FilesFromStdIn || + this.FoldersFromStdIn) + { + this.ReportErrorAndExit(tracer, "You cannot prefetch commits and blobs at the same time."); + } + + if (this.HydrateFiles) + { + this.ReportErrorAndExit(tracer, "You can only specify --hydrate with --files or --folders"); + } + + GitObjectsHttpRequestor objectRequestor; + CacheServerInfo cacheServer; + this.InitializeServerConnection( + tracer, + enlistment, + cacheServerUrl, + out objectRequestor, + out cacheServer); + this.PrefetchCommits(tracer, enlistment, objectRequestor, cacheServer); + } + else + { + string headCommitId; + List filesList; + List foldersList; + FileBasedDictionary lastPrefetchArgs; + this.LoadBlobPrefetchArgs(tracer, enlistment, out headCommitId, out filesList, out foldersList, out lastPrefetchArgs); - if (BlobPrefetcher.IsNoopPrefetch(tracer, lastPrefetchArgs, headCommitId, filesList, foldersList, this.HydrateFiles)) - { - Console.WriteLine("All requested files are already available. Nothing new to prefetch."); - } - else - { - GitObjectsHttpRequestor objectRequestor; - CacheServerInfo cacheServer; - this.InitializeServerConnection( - tracer, - enlistment, - cacheServerUrl, - out objectRequestor, - out cacheServer); - this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, cacheServer); - } - } - } - catch (VerbAbortedException) - { - throw; - } - catch (AggregateException aggregateException) - { - this.Output.WriteLine( - "Cannot prefetch {0}. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot), - enlistment.EnlistmentRoot); - foreach (Exception innerException in aggregateException.Flatten().InnerExceptions) - { - tracer.RelatedError( - new EventMetadata - { - { "Verb", typeof(PrefetchVerb).Name }, - { "Exception", innerException.ToString() } - }, - $"Unhandled {innerException.GetType().Name}: {innerException.Message}"); - } - - Environment.ExitCode = (int)ReturnCode.GenericError; - } - catch (Exception e) - { - this.Output.WriteLine( - "Cannot prefetch {0}. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot), - enlistment.EnlistmentRoot); - tracer.RelatedError( - new EventMetadata - { - { "Verb", typeof(PrefetchVerb).Name }, - { "Exception", e.ToString() } - }, - $"Unhandled {e.GetType().Name}: {e.Message}"); - - Environment.ExitCode = (int)ReturnCode.GenericError; - } - } - } - - private void InitializeServerConnection( - ITracer tracer, - ScalarEnlistment enlistment, - string cacheServerUrl, - out GitObjectsHttpRequestor objectRequestor, - out CacheServerInfo cacheServer) - { - RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); - - cacheServer = this.ResolvedCacheServer; - ServerScalarConfig serverScalarConfig = this.ServerScalarConfig; - if (!this.SkipVersionCheck) - { - string authErrorMessage; - if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) - { - this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); - } - - if (serverScalarConfig == null) - { - serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); - } - - if (cacheServer == null) - { - CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); - cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverScalarConfig); - } - - this.ValidateClientVersions(tracer, enlistment, serverScalarConfig, showWarnings: false); - - this.Output.WriteLine("Configured cache server: " + cacheServer); - } - - this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); - objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig); - } - - private void PrefetchCommits(ITracer tracer, ScalarEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) - { - bool success; - string error = string.Empty; - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - GitRepo repo = new GitRepo(tracer, enlistment, fileSystem); - ScalarContext context = new ScalarContext(tracer, fileSystem, repo, enlistment); - GitObjects gitObjects = new ScalarGitObjects(context, objectRequestor); - - if (this.Verbose) - { - success = new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error); - } - else - { - success = this.ShowStatusWhileRunning( - () => new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error), - "Fetching commits and trees " + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); - } - - if (!success) - { - this.ReportErrorAndExit(tracer, "Prefetching commits and trees failed: " + error); - } - } - - private void LoadBlobPrefetchArgs( - ITracer tracer, - ScalarEnlistment enlistment, - out string headCommitId, - out List filesList, - out List foldersList, - out FileBasedDictionary lastPrefetchArgs) - { + if (BlobPrefetcher.IsNoopPrefetch(tracer, lastPrefetchArgs, headCommitId, filesList, foldersList, this.HydrateFiles)) + { + Console.WriteLine("All requested files are already available. Nothing new to prefetch."); + } + else + { + GitObjectsHttpRequestor objectRequestor; + CacheServerInfo cacheServer; + this.InitializeServerConnection( + tracer, + enlistment, + cacheServerUrl, + out objectRequestor, + out cacheServer); + this.PrefetchBlobs(tracer, enlistment, headCommitId, filesList, foldersList, lastPrefetchArgs, objectRequestor, cacheServer); + } + } + } + catch (VerbAbortedException) + { + throw; + } + catch (AggregateException aggregateException) + { + this.Output.WriteLine( + "Cannot prefetch {0}. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot), + enlistment.EnlistmentRoot); + foreach (Exception innerException in aggregateException.Flatten().InnerExceptions) + { + tracer.RelatedError( + new EventMetadata + { + { "Verb", typeof(PrefetchVerb).Name }, + { "Exception", innerException.ToString() } + }, + $"Unhandled {innerException.GetType().Name}: {innerException.Message}"); + } + + Environment.ExitCode = (int)ReturnCode.GenericError; + } + catch (Exception e) + { + this.Output.WriteLine( + "Cannot prefetch {0}. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot), + enlistment.EnlistmentRoot); + tracer.RelatedError( + new EventMetadata + { + { "Verb", typeof(PrefetchVerb).Name }, + { "Exception", e.ToString() } + }, + $"Unhandled {e.GetType().Name}: {e.Message}"); + + Environment.ExitCode = (int)ReturnCode.GenericError; + } + } + } + + private void InitializeServerConnection( + ITracer tracer, + ScalarEnlistment enlistment, + string cacheServerUrl, + out GitObjectsHttpRequestor objectRequestor, + out CacheServerInfo cacheServer) + { + RetryConfig retryConfig = this.GetRetryConfig(tracer, enlistment, TimeSpan.FromMinutes(RetryConfig.FetchAndCloneTimeoutMinutes)); + + cacheServer = this.ResolvedCacheServer; + ServerScalarConfig serverScalarConfig = this.ServerScalarConfig; + if (!this.SkipVersionCheck) + { + string authErrorMessage; + if (!this.TryAuthenticate(tracer, enlistment, out authErrorMessage)) + { + this.ReportErrorAndExit(tracer, "Unable to prefetch because authentication failed: " + authErrorMessage); + } + + if (serverScalarConfig == null) + { + serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); + } + + if (cacheServer == null) + { + CacheServerResolver cacheServerResolver = new CacheServerResolver(tracer, enlistment); + cacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServerUrl, serverScalarConfig); + } + + this.ValidateClientVersions(tracer, enlistment, serverScalarConfig, showWarnings: false); + + this.Output.WriteLine("Configured cache server: " + cacheServer); + } + + this.InitializeLocalCacheAndObjectsPaths(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); + objectRequestor = new GitObjectsHttpRequestor(tracer, enlistment, cacheServer, retryConfig); + } + + private void PrefetchCommits(ITracer tracer, ScalarEnlistment enlistment, GitObjectsHttpRequestor objectRequestor, CacheServerInfo cacheServer) + { + bool success; + string error = string.Empty; + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + GitRepo repo = new GitRepo(tracer, enlistment, fileSystem); + ScalarContext context = new ScalarContext(tracer, fileSystem, repo, enlistment); + GitObjects gitObjects = new ScalarGitObjects(context, objectRequestor); + + if (this.Verbose) + { + success = new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error); + } + else + { + success = this.ShowStatusWhileRunning( + () => new PrefetchStep(context, gitObjects, requireCacheLock: false).TryPrefetchCommitsAndTrees(out error), + "Fetching commits and trees " + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); + } + + if (!success) + { + this.ReportErrorAndExit(tracer, "Prefetching commits and trees failed: " + error); + } + } + + private void LoadBlobPrefetchArgs( + ITracer tracer, + ScalarEnlistment enlistment, + out string headCommitId, + out List filesList, + out List foldersList, + out FileBasedDictionary lastPrefetchArgs) + { string error; - if (!FileBasedDictionary.TryCreate( - tracer, - Path.Combine(enlistment.DotScalarRoot, "LastBlobPrefetch.dat"), - new PhysicalFileSystem(), - out lastPrefetchArgs, - out error)) - { - tracer.RelatedWarning("Unable to load last prefetch args: " + error); - } - - filesList = new List(); - foldersList = new List(); - - if (!BlobPrefetcher.TryLoadFileList(enlistment, this.Files, this.FilesListFile, filesList, readListFromStdIn: this.FilesFromStdIn, error: out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - if (!BlobPrefetcher.TryLoadFolderList(enlistment, this.Folders, this.FoldersListFile, foldersList, readListFromStdIn: this.FoldersFromStdIn, error: out error)) - { - this.ReportErrorAndExit(tracer, error); - } - - GitProcess gitProcess = new GitProcess(enlistment); - GitProcess.Result result = gitProcess.RevParse(ScalarConstants.DotGit.HeadName); - if (result.ExitCodeIsFailure) - { - this.ReportErrorAndExit(tracer, result.Errors); - } - - headCommitId = result.Output.Trim(); - } - - private void PrefetchBlobs( - ITracer tracer, - ScalarEnlistment enlistment, - string headCommitId, - List filesList, - List foldersList, - FileBasedDictionary lastPrefetchArgs, - GitObjectsHttpRequestor objectRequestor, - CacheServerInfo cacheServer) - { - BlobPrefetcher blobPrefetcher = new BlobPrefetcher( - tracer, - enlistment, - objectRequestor, - filesList, - foldersList, - lastPrefetchArgs, - ChunkSize, - SearchThreadCount, - DownloadThreadCount, - IndexThreadCount); - - if (blobPrefetcher.FolderList.Count == 0 && - blobPrefetcher.FileList.Count == 0) - { - this.ReportErrorAndExit(tracer, "Did you mean to fetch all blobs? If so, specify `--files '*'` to confirm."); - } - - if (this.HydrateFiles) - { - if (!this.CheckIsMounted(verbose: true)) - { - this.ReportErrorAndExit("You can only specify --hydrate if the repo is mounted. Run 'scalar mount' and try again."); - } - } - - int matchedBlobCount = 0; - int downloadedBlobCount = 0; - int hydratedFileCount = 0; - - Func doPrefetch = - () => - { - try - { - blobPrefetcher.PrefetchWithStats( - headCommitId, - isBranch: false, - hydrateFilesAfterDownload: this.HydrateFiles, - matchedBlobCount: out matchedBlobCount, - downloadedBlobCount: out downloadedBlobCount, - hydratedFileCount: out hydratedFileCount); - return !blobPrefetcher.HasFailures; - } - catch (BlobPrefetcher.FetchException e) - { - tracer.RelatedError(e.Message); - return false; - } - }; - - if (this.Verbose) - { - doPrefetch(); - } - else - { - string message = - this.HydrateFiles - ? "Fetching blobs and hydrating files " - : "Fetching blobs "; - this.ShowStatusWhileRunning(doPrefetch, message + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); - } - - if (blobPrefetcher.HasFailures) - { - Environment.ExitCode = 1; - } - else - { - Console.WriteLine(); - Console.WriteLine("Stats:"); - Console.WriteLine(" Matched blobs: " + matchedBlobCount); - Console.WriteLine(" Already cached: " + (matchedBlobCount - downloadedBlobCount)); - Console.WriteLine(" Downloaded: " + downloadedBlobCount); - if (this.HydrateFiles) - { - Console.WriteLine(" Hydrated files: " + hydratedFileCount); - } - } - } - - private bool CheckIsMounted(bool verbose) - { - Func checkMount = () => this.Execute( - this.EnlistmentRootPathParameter, - verb => verb.Output = new StreamWriter(new MemoryStream())) == ReturnCode.Success; - - if (verbose) - { - return ConsoleHelper.ShowStatusWhileRunning( - checkMount, - "Checking that Scalar is mounted", - this.Output, - showSpinner: true, - scalarLogEnlistmentRoot: null); - } - else - { - return checkMount(); - } - } - - private string GetCacheServerDisplay(CacheServerInfo cacheServer, string repoUrl) - { - if (!cacheServer.IsNone(repoUrl)) - { - return "from cache server"; - } - - return "from origin (no cache server)"; - } - } -} + if (!FileBasedDictionary.TryCreate( + tracer, + Path.Combine(enlistment.DotScalarRoot, "LastBlobPrefetch.dat"), + new PhysicalFileSystem(), + out lastPrefetchArgs, + out error)) + { + tracer.RelatedWarning("Unable to load last prefetch args: " + error); + } + + filesList = new List(); + foldersList = new List(); + + if (!BlobPrefetcher.TryLoadFileList(enlistment, this.Files, this.FilesListFile, filesList, readListFromStdIn: this.FilesFromStdIn, error: out error)) + { + this.ReportErrorAndExit(tracer, error); + } + + if (!BlobPrefetcher.TryLoadFolderList(enlistment, this.Folders, this.FoldersListFile, foldersList, readListFromStdIn: this.FoldersFromStdIn, error: out error)) + { + this.ReportErrorAndExit(tracer, error); + } + + GitProcess gitProcess = new GitProcess(enlistment); + GitProcess.Result result = gitProcess.RevParse(ScalarConstants.DotGit.HeadName); + if (result.ExitCodeIsFailure) + { + this.ReportErrorAndExit(tracer, result.Errors); + } + + headCommitId = result.Output.Trim(); + } + + private void PrefetchBlobs( + ITracer tracer, + ScalarEnlistment enlistment, + string headCommitId, + List filesList, + List foldersList, + FileBasedDictionary lastPrefetchArgs, + GitObjectsHttpRequestor objectRequestor, + CacheServerInfo cacheServer) + { + BlobPrefetcher blobPrefetcher = new BlobPrefetcher( + tracer, + enlistment, + objectRequestor, + filesList, + foldersList, + lastPrefetchArgs, + ChunkSize, + SearchThreadCount, + DownloadThreadCount, + IndexThreadCount); + + if (blobPrefetcher.FolderList.Count == 0 && + blobPrefetcher.FileList.Count == 0) + { + this.ReportErrorAndExit(tracer, "Did you mean to fetch all blobs? If so, specify `--files '*'` to confirm."); + } + + if (this.HydrateFiles) + { + if (!this.CheckIsMounted(verbose: true)) + { + this.ReportErrorAndExit("You can only specify --hydrate if the repo is mounted. Run 'scalar mount' and try again."); + } + } + + int matchedBlobCount = 0; + int downloadedBlobCount = 0; + int hydratedFileCount = 0; + + Func doPrefetch = + () => + { + try + { + blobPrefetcher.PrefetchWithStats( + headCommitId, + isBranch: false, + hydrateFilesAfterDownload: this.HydrateFiles, + matchedBlobCount: out matchedBlobCount, + downloadedBlobCount: out downloadedBlobCount, + hydratedFileCount: out hydratedFileCount); + return !blobPrefetcher.HasFailures; + } + catch (BlobPrefetcher.FetchException e) + { + tracer.RelatedError(e.Message); + return false; + } + }; + + if (this.Verbose) + { + doPrefetch(); + } + else + { + string message = + this.HydrateFiles + ? "Fetching blobs and hydrating files " + : "Fetching blobs "; + this.ShowStatusWhileRunning(doPrefetch, message + this.GetCacheServerDisplay(cacheServer, enlistment.RepoUrl)); + } + + if (blobPrefetcher.HasFailures) + { + Environment.ExitCode = 1; + } + else + { + Console.WriteLine(); + Console.WriteLine("Stats:"); + Console.WriteLine(" Matched blobs: " + matchedBlobCount); + Console.WriteLine(" Already cached: " + (matchedBlobCount - downloadedBlobCount)); + Console.WriteLine(" Downloaded: " + downloadedBlobCount); + if (this.HydrateFiles) + { + Console.WriteLine(" Hydrated files: " + hydratedFileCount); + } + } + } + + private bool CheckIsMounted(bool verbose) + { + Func checkMount = () => this.Execute( + this.EnlistmentRootPathParameter, + verb => verb.Output = new StreamWriter(new MemoryStream())) == ReturnCode.Success; + + if (verbose) + { + return ConsoleHelper.ShowStatusWhileRunning( + checkMount, + "Checking that Scalar is mounted", + this.Output, + showSpinner: true, + scalarLogEnlistmentRoot: null); + } + else + { + return checkMount(); + } + } + + private string GetCacheServerDisplay(CacheServerInfo cacheServer, string repoUrl) + { + if (!cacheServer.IsNone(repoUrl)) + { + return "from cache server"; + } + + return "from origin (no cache server)"; + } + } +} diff --git a/Scalar/CommandLine/RepairVerb.cs b/Scalar/CommandLine/RepairVerb.cs index 86084eacba..e48ddcb77f 100644 --- a/Scalar/CommandLine/RepairVerb.cs +++ b/Scalar/CommandLine/RepairVerb.cs @@ -1,237 +1,237 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using Scalar.DiskLayoutUpgrades; -using Scalar.RepairJobs; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.CommandLine -{ - [Verb(RepairVerb.RepairVerbName, HelpText = "EXPERIMENTAL FEATURE - Repair issues that prevent a Scalar repo from mounting")] - public class RepairVerb : ScalarVerb - { - private const string RepairVerbName = "repair"; - - [Value( - 1, - Required = false, - Default = "", - MetaName = "Enlistment Root Path", - HelpText = "Full or relative path to the Scalar enlistment root")] - public override string EnlistmentRootPathParameter { get; set; } - - [Option( - "confirm", - Default = false, - Required = false, - HelpText = "Pass in this flag to actually do repair(s). Without it, only validation will be done.")] - public bool Confirmed { get; set; } - - protected override string VerbName - { - get { return RepairVerb.RepairVerbName; } - } - - public override void Execute() - { - this.ValidatePathParameter(this.EnlistmentRootPathParameter); - - if (!Directory.Exists(this.EnlistmentRootPathParameter)) - { - this.ReportErrorAndExit($"Path '{this.EnlistmentRootPathParameter}' does not exist"); - } - - string errorMessage; - string enlistmentRoot; - if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) - { - this.ReportErrorAndExit("'scalar repair' must be run within a Scalar enlistment"); - } - - ScalarEnlistment enlistment = null; - - try - { - enlistment = ScalarEnlistment.CreateFromDirectory( - this.EnlistmentRootPathParameter, - ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), - authentication: null, - createWithoutRepoURL: true); - } - catch (InvalidRepoException e) - { - this.ReportErrorAndExit($"Failed to initialize enlistment, error: {e.Message}"); - } - - if (!this.Confirmed) - { - this.Output.WriteLine( -@"WARNING: THIS IS AN EXPERIMENTAL FEATURE - -This command detects and repairs issues that prevent a Scalar repo from mounting. -A few such checks are currently implemented, and some of them can be repaired. -More repairs and more checks are coming soon. - -Without --confirm, it will non-invasively check if repairs are necessary. -To actually execute any necessary repair(s), run 'scalar repair --confirm' -"); - } - - string error; - if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.EnlistmentRoot, error: out error)) - { - this.ReportErrorAndExit(error); - } - - if (!ConsoleHelper.ShowStatusWhileRunning( - () => - { - // Don't use 'scalar status' here. The repo may be corrupt such that 'scalar status' cannot run normally, - // causing repair to continue when it shouldn't. - using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) - { - if (!pipeClient.Connect()) - { - return true; - } - } - - return false; - }, - "Checking that Scalar is not mounted", - this.Output, - showSpinner: true, - scalarLogEnlistmentRoot: null)) - { - this.ReportErrorAndExit("You can only run 'scalar repair' if Scalar is not mounted. Run 'scalar unmount' and try again."); - } - - this.Output.WriteLine(); - - using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "RepairVerb", enlistment.GetEnlistmentId(), mountId: null)) - { - tracer.AddLogFileEventListener( - ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Repair), - EventLevel.Verbose, - Keywords.Any); - tracer.WriteStartEvent( - enlistment.EnlistmentRoot, - enlistment.RepoUrl, - "N/A", - new EventMetadata - { - { "Confirmed", this.Confirmed }, - { "IsElevated", ScalarPlatform.Instance.IsElevated() }, - { "NamedPipename", enlistment.NamedPipeName }, - { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, - }); - - List jobs = new List(); - - // Repair databases - jobs.Add(new RepoMetadataDatabaseRepairJob(tracer, this.Output, enlistment)); - - // Repair .git folder files - jobs.Add(new GitHeadRepairJob(tracer, this.Output, enlistment)); - jobs.Add(new GitConfigRepairJob(tracer, this.Output, enlistment)); - - Dictionary> healthy = new Dictionary>(); - Dictionary> cantFix = new Dictionary>(); - Dictionary> fixable = new Dictionary>(); - - foreach (RepairJob job in jobs) - { - List messages = new List(); - switch (job.HasIssue(messages)) - { - case RepairJob.IssueType.None: - healthy[job] = messages; - break; - - case RepairJob.IssueType.CantFix: - cantFix[job] = messages; - break; - - case RepairJob.IssueType.Fixable: - fixable[job] = messages; - break; - } - } - - foreach (RepairJob job in healthy.Keys) - { - this.WriteMessage(tracer, string.Format("{0, -30}: Healthy", job.Name)); - this.WriteMessages(tracer, healthy[job]); - } - - if (healthy.Count > 0) - { - this.Output.WriteLine(); - } - - foreach (RepairJob job in cantFix.Keys) - { - this.WriteMessage(tracer, job.Name); - this.WriteMessages(tracer, cantFix[job]); - this.Indent(); - this.WriteMessage(tracer, "'scalar repair' does not currently support fixing this problem"); - this.Output.WriteLine(); - } - - foreach (RepairJob job in fixable.Keys) - { - this.WriteMessage(tracer, job.Name); - this.WriteMessages(tracer, fixable[job]); - this.Indent(); - - if (this.Confirmed) - { - List repairMessages = new List(); - switch (job.TryFixIssues(repairMessages)) - { - case RepairJob.FixResult.Success: - this.WriteMessage(tracer, "Repair succeeded"); - break; - case RepairJob.FixResult.ManualStepsRequired: - this.WriteMessage(tracer, "Repair succeeded, but requires some manual steps before remounting."); - break; - case RepairJob.FixResult.Failure: - this.WriteMessage(tracer, "Repair failed. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot)); - break; - } - - this.WriteMessages(tracer, repairMessages); - } - else - { - this.WriteMessage(tracer, "Run 'scalar repair --confirm' to attempt a repair"); - } - - this.Output.WriteLine(); - } - } - } - - private void WriteMessage(ITracer tracer, string message) - { - tracer.RelatedEvent(EventLevel.Informational, "RepairInfo", new EventMetadata { { TracingConstants.MessageKey.InfoMessage, message } }); - this.Output.WriteLine(message); - } - - private void WriteMessages(ITracer tracer, List messages) - { - foreach (string message in messages) - { - this.Indent(); - this.WriteMessage(tracer, message); - } - } - - private void Indent() - { - this.Output.Write(" "); - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using Scalar.DiskLayoutUpgrades; +using Scalar.RepairJobs; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.CommandLine +{ + [Verb(RepairVerb.RepairVerbName, HelpText = "EXPERIMENTAL FEATURE - Repair issues that prevent a Scalar repo from mounting")] + public class RepairVerb : ScalarVerb + { + private const string RepairVerbName = "repair"; + + [Value( + 1, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the Scalar enlistment root")] + public override string EnlistmentRootPathParameter { get; set; } + + [Option( + "confirm", + Default = false, + Required = false, + HelpText = "Pass in this flag to actually do repair(s). Without it, only validation will be done.")] + public bool Confirmed { get; set; } + + protected override string VerbName + { + get { return RepairVerb.RepairVerbName; } + } + + public override void Execute() + { + this.ValidatePathParameter(this.EnlistmentRootPathParameter); + + if (!Directory.Exists(this.EnlistmentRootPathParameter)) + { + this.ReportErrorAndExit($"Path '{this.EnlistmentRootPathParameter}' does not exist"); + } + + string errorMessage; + string enlistmentRoot; + if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out enlistmentRoot, out errorMessage)) + { + this.ReportErrorAndExit("'scalar repair' must be run within a Scalar enlistment"); + } + + ScalarEnlistment enlistment = null; + + try + { + enlistment = ScalarEnlistment.CreateFromDirectory( + this.EnlistmentRootPathParameter, + ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(), + authentication: null, + createWithoutRepoURL: true); + } + catch (InvalidRepoException e) + { + this.ReportErrorAndExit($"Failed to initialize enlistment, error: {e.Message}"); + } + + if (!this.Confirmed) + { + this.Output.WriteLine( +@"WARNING: THIS IS AN EXPERIMENTAL FEATURE + +This command detects and repairs issues that prevent a Scalar repo from mounting. +A few such checks are currently implemented, and some of them can be repaired. +More repairs and more checks are coming soon. + +Without --confirm, it will non-invasively check if repairs are necessary. +To actually execute any necessary repair(s), run 'scalar repair --confirm' +"); + } + + string error; + if (!DiskLayoutUpgrade.TryCheckDiskLayoutVersion(tracer: null, enlistmentRoot: enlistment.EnlistmentRoot, error: out error)) + { + this.ReportErrorAndExit(error); + } + + if (!ConsoleHelper.ShowStatusWhileRunning( + () => + { + // Don't use 'scalar status' here. The repo may be corrupt such that 'scalar status' cannot run normally, + // causing repair to continue when it shouldn't. + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + return true; + } + } + + return false; + }, + "Checking that Scalar is not mounted", + this.Output, + showSpinner: true, + scalarLogEnlistmentRoot: null)) + { + this.ReportErrorAndExit("You can only run 'scalar repair' if Scalar is not mounted. Run 'scalar unmount' and try again."); + } + + this.Output.WriteLine(); + + using (JsonTracer tracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "RepairVerb", enlistment.GetEnlistmentId(), mountId: null)) + { + tracer.AddLogFileEventListener( + ScalarEnlistment.GetNewScalarLogFileName(enlistment.ScalarLogsRoot, ScalarConstants.LogFileTypes.Repair), + EventLevel.Verbose, + Keywords.Any); + tracer.WriteStartEvent( + enlistment.EnlistmentRoot, + enlistment.RepoUrl, + "N/A", + new EventMetadata + { + { "Confirmed", this.Confirmed }, + { "IsElevated", ScalarPlatform.Instance.IsElevated() }, + { "NamedPipename", enlistment.NamedPipeName }, + { nameof(this.EnlistmentRootPathParameter), this.EnlistmentRootPathParameter }, + }); + + List jobs = new List(); + + // Repair databases + jobs.Add(new RepoMetadataDatabaseRepairJob(tracer, this.Output, enlistment)); + + // Repair .git folder files + jobs.Add(new GitHeadRepairJob(tracer, this.Output, enlistment)); + jobs.Add(new GitConfigRepairJob(tracer, this.Output, enlistment)); + + Dictionary> healthy = new Dictionary>(); + Dictionary> cantFix = new Dictionary>(); + Dictionary> fixable = new Dictionary>(); + + foreach (RepairJob job in jobs) + { + List messages = new List(); + switch (job.HasIssue(messages)) + { + case RepairJob.IssueType.None: + healthy[job] = messages; + break; + + case RepairJob.IssueType.CantFix: + cantFix[job] = messages; + break; + + case RepairJob.IssueType.Fixable: + fixable[job] = messages; + break; + } + } + + foreach (RepairJob job in healthy.Keys) + { + this.WriteMessage(tracer, string.Format("{0, -30}: Healthy", job.Name)); + this.WriteMessages(tracer, healthy[job]); + } + + if (healthy.Count > 0) + { + this.Output.WriteLine(); + } + + foreach (RepairJob job in cantFix.Keys) + { + this.WriteMessage(tracer, job.Name); + this.WriteMessages(tracer, cantFix[job]); + this.Indent(); + this.WriteMessage(tracer, "'scalar repair' does not currently support fixing this problem"); + this.Output.WriteLine(); + } + + foreach (RepairJob job in fixable.Keys) + { + this.WriteMessage(tracer, job.Name); + this.WriteMessages(tracer, fixable[job]); + this.Indent(); + + if (this.Confirmed) + { + List repairMessages = new List(); + switch (job.TryFixIssues(repairMessages)) + { + case RepairJob.FixResult.Success: + this.WriteMessage(tracer, "Repair succeeded"); + break; + case RepairJob.FixResult.ManualStepsRequired: + this.WriteMessage(tracer, "Repair succeeded, but requires some manual steps before remounting."); + break; + case RepairJob.FixResult.Failure: + this.WriteMessage(tracer, "Repair failed. " + ConsoleHelper.GetScalarLogMessage(enlistment.EnlistmentRoot)); + break; + } + + this.WriteMessages(tracer, repairMessages); + } + else + { + this.WriteMessage(tracer, "Run 'scalar repair --confirm' to attempt a repair"); + } + + this.Output.WriteLine(); + } + } + } + + private void WriteMessage(ITracer tracer, string message) + { + tracer.RelatedEvent(EventLevel.Informational, "RepairInfo", new EventMetadata { { TracingConstants.MessageKey.InfoMessage, message } }); + this.Output.WriteLine(message); + } + + private void WriteMessages(ITracer tracer, List messages) + { + foreach (string message in messages) + { + this.Indent(); + this.WriteMessage(tracer, message); + } + } + + private void Indent() + { + this.Output.Write(" "); + } + } +} diff --git a/Scalar/CommandLine/ScalarVerb.cs b/Scalar/CommandLine/ScalarVerb.cs index 36c0245805..8c08061122 100644 --- a/Scalar/CommandLine/ScalarVerb.cs +++ b/Scalar/CommandLine/ScalarVerb.cs @@ -1,1024 +1,1024 @@ -using CommandLine; -using Newtonsoft.Json; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Http; -using Scalar.Common.NamedPipes; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Security; - -namespace Scalar.CommandLine -{ - public abstract class ScalarVerb - { - protected const string StartServiceInstructions = "Run 'sc start Scalar.Service' from an elevated command prompt to ensure it is running."; - - private readonly bool validateOriginURL; - - public ScalarVerb(bool validateOrigin = true) - { - this.Output = Console.Out; - this.ReturnCode = ReturnCode.Success; - this.validateOriginURL = validateOrigin; - this.ServiceName = ScalarConstants.Service.ServiceName; - this.StartedByService = false; - this.Unattended = ScalarEnlistment.IsUnattended(tracer: null); - - this.InitializeDefaultParameterValues(); - } - - public abstract string EnlistmentRootPathParameter { get; set; } - - [Option( - ScalarConstants.VerbParameters.InternalUseOnly, - Required = false, - HelpText = "This parameter is reserved for internal use.")] - public string InternalParameters - { - set - { - if (!string.IsNullOrEmpty(value)) - { - try - { - InternalVerbParameters mountInternal = InternalVerbParameters.FromJson(value); - if (!string.IsNullOrEmpty(mountInternal.ServiceName)) - { - this.ServiceName = mountInternal.ServiceName; - } - - if (!string.IsNullOrEmpty(mountInternal.MaintenanceJob)) - { - this.MaintenanceJob = mountInternal.MaintenanceJob; - } - - if (!string.IsNullOrEmpty(mountInternal.PackfileMaintenanceBatchSize)) - { - this.PackfileMaintenanceBatchSize = mountInternal.PackfileMaintenanceBatchSize; - } - - this.StartedByService = mountInternal.StartedByService; - } - catch (JsonReaderException e) - { - this.ReportErrorAndExit("Failed to parse InternalParameters: {0}.\n {1}", value, e); - } - } - } - } - - public string ServiceName { get; set; } - - public string MaintenanceJob { get; set; } - - public string PackfileMaintenanceBatchSize { get; set; } - - public bool StartedByService { get; set; } - - public bool Unattended { get; private set; } - - public string ServicePipeName - { - get - { - return ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.ServiceName); - } - } - - public TextWriter Output { get; set; } - - public ReturnCode ReturnCode { get; private set; } - - protected abstract string VerbName { get; } - - public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment) - { - string expectedHooksPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Hooks.Root); - expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); - - // These settings are required for normal Scalar functionality. - // They will override any existing local configuration values. - // - // IMPORTANT! These must parallel the settings in ControlGitRepo:Initialize - // - Dictionary requiredSettings = new Dictionary - { - { "am.keepcr", "true" }, - { "checkout.optimizenewbranch", "true" }, - { "core.autocrlf", "false" }, - { "core.commitGraph", "true" }, - { "core.fscache", "true" }, - { "core.scalar", "true" }, - { "core.multiPackIndex", "true" }, - { "core.preloadIndex", "true" }, - { "core.safecrlf", "false" }, - { "core.untrackedCache", "false" }, - { "core.repositoryformatversion", "0" }, - { "core.filemode", ScalarPlatform.Instance.FileSystem.SupportsFileMode ? "true" : "false" }, - { GitConfigSetting.CoreVirtualizeObjectsName, "true" }, - { "core.bare", "false" }, - { "core.logallrefupdates", "true" }, - { "core.hookspath", expectedHooksPath }, - { GitConfigSetting.CredentialUseHttpPath, "true" }, - { "credential.validate", "false" }, - { "diff.autoRefreshIndex", "false" }, - { "gc.auto", "0" }, - { "gui.gcwarning", "false" }, - { "index.threads", "true" }, - { "index.version", "4" }, - { "merge.stat", "false" }, - { "merge.renames", "false" }, - { "pack.useBitmaps", "false" }, - { "pack.useSparse", "true" }, - { "receive.autogc", "false" }, - { "reset.quiet", "true" }, - }; - - if (!TrySetConfig(enlistment, requiredSettings, isRequired: true)) - { - return false; - } - - return true; - } - - public static bool TrySetOptionalGitConfigSettings(Enlistment enlistment) - { - // These settings are optional, because they impact performance but not functionality of Scalar. - // These settings should only be set by the clone or repair verbs, so that they do not - // overwrite the values set by the user in their local config. - Dictionary optionalSettings = new Dictionary - { - { "status.aheadbehind", "false" }, - }; - - if (!TrySetConfig(enlistment, optionalSettings, isRequired: false)) - { - return false; - } - - return true; - } - - public abstract void Execute(); - - public virtual void InitializeDefaultParameterValues() - { - } - - protected ReturnCode Execute( - string enlistmentRootPath, - Action configureVerb = null) - where TVerb : ScalarVerb, new() - { - TVerb verb = new TVerb(); - verb.EnlistmentRootPathParameter = enlistmentRootPath; - verb.ServiceName = this.ServiceName; - verb.Unattended = this.Unattended; - - if (configureVerb != null) - { - configureVerb(verb); - } - - try - { - verb.Execute(); - } - catch (VerbAbortedException) - { - } - - return verb.ReturnCode; - } - - protected ReturnCode Execute( - ScalarEnlistment enlistment, - Action configureVerb = null) - where TVerb : ScalarVerb.ForExistingEnlistment, new() - { - TVerb verb = new TVerb(); - verb.EnlistmentRootPathParameter = enlistment.EnlistmentRoot; - verb.ServiceName = this.ServiceName; - verb.Unattended = this.Unattended; - - if (configureVerb != null) - { - configureVerb(verb); - } - - try - { - verb.Execute(enlistment.Authentication); - } - catch (VerbAbortedException) - { - } - - return verb.ReturnCode; - } - - protected bool ShowStatusWhileRunning( - Func action, - string message, - string scalarLogEnlistmentRoot) - { - return ConsoleHelper.ShowStatusWhileRunning( - action, - message, - this.Output, - showSpinner: !this.Unattended && this.Output == Console.Out && !ScalarPlatform.Instance.IsConsoleOutputRedirectedToFile(), - scalarLogEnlistmentRoot: scalarLogEnlistmentRoot, - initialDelayMs: 0); - } - - protected bool ShowStatusWhileRunning( - Func action, - string message, - bool suppressGvfsLogMessage = false) - { - string scalarLogEnlistmentRoot = null; - if (!suppressGvfsLogMessage) - { - string errorMessage; - ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out scalarLogEnlistmentRoot, out errorMessage); - } - - return this.ShowStatusWhileRunning(action, message, scalarLogEnlistmentRoot); - } - - protected bool TryAuthenticate(ITracer tracer, ScalarEnlistment enlistment, out string authErrorMessage) - { - string authError = null; - - bool result = this.ShowStatusWhileRunning( - () => enlistment.Authentication.TryInitialize(tracer, enlistment, out authError), - "Authenticating", - enlistment.EnlistmentRoot); - - authErrorMessage = authError; - return result; - } - - protected void ReportErrorAndExit(ITracer tracer, ReturnCode exitCode, string error, params object[] args) - { - if (!string.IsNullOrEmpty(error)) - { - if (args == null || args.Length == 0) - { - this.Output.WriteLine(error); - if (tracer != null && exitCode != ReturnCode.Success) - { - tracer.RelatedError(error); - } - } - else - { - this.Output.WriteLine(error, args); - if (tracer != null && exitCode != ReturnCode.Success) - { - tracer.RelatedError(error, args); - } - } - } - - this.ReturnCode = exitCode; - throw new VerbAbortedException(this); - } - - protected void ReportErrorAndExit(string error, params object[] args) - { - this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.GenericError, error: error, args: args); - } - - protected void ReportErrorAndExit(ITracer tracer, string error, params object[] args) - { - this.ReportErrorAndExit(tracer, ReturnCode.GenericError, error, args); - } - - protected RetryConfig GetRetryConfig(ITracer tracer, ScalarEnlistment enlistment, TimeSpan? timeoutOverride = null) - { - RetryConfig retryConfig; - string error; - if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); - } - - if (timeoutOverride.HasValue) - { - retryConfig.Timeout = timeoutOverride.Value; - } - - return retryConfig; - } - - protected ServerScalarConfig QueryScalarConfig(ITracer tracer, ScalarEnlistment enlistment, RetryConfig retryConfig) - { - ServerScalarConfig serverScalarConfig = null; - string errorMessage = null; - if (!this.ShowStatusWhileRunning( - () => - { - using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) - { - const bool LogErrors = true; - return configRequestor.TryQueryScalarConfig(LogErrors, out serverScalarConfig, out _, out errorMessage); - } - }, - "Querying remote for config", - suppressGvfsLogMessage: true)) - { - this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + errorMessage); - } - - return serverScalarConfig; - } - - protected bool IsExistingPipeListening(string enlistmentRoot) - { - using (NamedPipeClient pipeClient = new NamedPipeClient(ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot))) - { - if (pipeClient.Connect(500)) - { - return true; - } - } - - return false; - } - - protected void ValidateClientVersions(ITracer tracer, ScalarEnlistment enlistment, ServerScalarConfig scalarConfig, bool showWarnings) - { - this.CheckGitVersion(tracer, enlistment, out string gitVersion); - enlistment.SetGitVersion(gitVersion); - - string errorMessage = null; - bool errorIsFatal = false; - if (!this.TryValidateScalarVersion(enlistment, tracer, scalarConfig, out errorMessage, out errorIsFatal)) - { - if (errorIsFatal) - { - this.ReportErrorAndExit(tracer, errorMessage); - } - else if (showWarnings) - { - this.Output.WriteLine(); - this.Output.WriteLine(errorMessage); - this.Output.WriteLine(); - } - } - } - - protected bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, ScalarEnlistment enlistment, out string errorMessage) - { - try - { - string alternatesFilePath = this.GetAlternatesPath(enlistment); - string tempFilePath = alternatesFilePath + ".tmp"; - fileSystem.WriteAllText(tempFilePath, enlistment.GitObjectsRoot); - fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath); - } - catch (SecurityException e) - { - errorMessage = e.Message; - return false; - } - catch (IOException e) - { - errorMessage = e.Message; - return false; - } - - errorMessage = null; - return true; - } - - protected void BlockEmptyCacheServerUrl(string userInput) - { - if (userInput == null) - { - return; - } - - if (string.IsNullOrWhiteSpace(userInput)) - { - this.ReportErrorAndExit( -@"You must specify a value for the cache server. -You can specify a URL, a name of a configured cache server, or the special names None or Default."); - } - } - - protected CacheServerInfo ResolveCacheServer( - ITracer tracer, - CacheServerInfo cacheServer, - CacheServerResolver cacheServerResolver, - ServerScalarConfig serverScalarConfig) - { - CacheServerInfo resolvedCacheServer = cacheServer; - - if (cacheServer.Url == null) - { - string cacheServerName = cacheServer.Name; - string error = null; - - if (!cacheServerResolver.TryResolveUrlFromRemote( - cacheServerName, - serverScalarConfig, - out resolvedCacheServer, - out error)) - { - this.ReportErrorAndExit(tracer, error); - } - } - else if (cacheServer.Name.Equals(CacheServerInfo.ReservedNames.UserDefined)) - { - resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverScalarConfig); - } - - this.Output.WriteLine("Using cache server: " + resolvedCacheServer); - return resolvedCacheServer; - } - - protected void ValidatePathParameter(string path) - { - if (!string.IsNullOrWhiteSpace(path)) - { - try - { - Path.GetFullPath(path); - } - catch (Exception e) - { - this.ReportErrorAndExit("Invalid path: '{0}' ({1})", path, e.Message); - } - } - } - - protected bool TryDownloadCommit( - string commitId, - ScalarEnlistment enlistment, - GitObjectsHttpRequestor objectRequestor, - ScalarGitObjects gitObjects, - GitRepo repo, - out string error, - bool checkLocalObjectCache = true) - { - if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId)) - { - if (!gitObjects.TryDownloadCommit(commitId)) - { - error = "Could not download commit " + commitId + " from: " + Uri.EscapeUriString(objectRequestor.CacheServer.ObjectsEndpointUrl); - return false; - } - } - - error = null; - return true; - } - - protected bool TryDownloadRootGitAttributes(ScalarEnlistment enlistment, ScalarGitObjects gitObjects, GitRepo repo, out string error) - { - List rootEntries = new List(); - GitProcess git = new GitProcess(enlistment); - GitProcess.Result result = git.LsTree( - ScalarConstants.DotGit.HeadName, - line => rootEntries.Add(DiffTreeResult.ParseFromLsTreeLine(line)), - recursive: false); - - if (result.ExitCodeIsFailure) - { - error = "Error returned from ls-tree to find " + ScalarConstants.SpecialGitFiles.GitAttributes + " file: " + result.Errors; - return false; - } - - DiffTreeResult gitAttributes = rootEntries.FirstOrDefault(entry => entry.TargetPath.Equals(ScalarConstants.SpecialGitFiles.GitAttributes)); - if (gitAttributes == null) - { - error = "This branch does not contain a " + ScalarConstants.SpecialGitFiles.GitAttributes + " file in the root folder. This file is required by Scalar clone"; - return false; - } - - if (!repo.ObjectExists(gitAttributes.TargetSha)) - { - if (gitObjects.TryDownloadAndSaveObject(gitAttributes.TargetSha, ScalarGitObjects.RequestSource.ScalarVerb) != GitObjects.DownloadAndSaveObjectResult.Success) - { - error = "Could not download " + ScalarConstants.SpecialGitFiles.GitAttributes + " file"; - return false; - } - } - - error = null; - return true; - } - - protected void LogEnlistmentInfoAndSetConfigValues(ITracer tracer, GitProcess git, ScalarEnlistment enlistment) - { - string mountId = CreateMountId(); - EventMetadata metadata = new EventMetadata(); - metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); - metadata.Add(nameof(mountId), mountId); - metadata.Add("Enlistment", enlistment); - metadata.Add("PhysicalDiskInfo", ScalarPlatform.Instance.GetPhysicalDiskInfo(enlistment.WorkingDirectoryRoot, sizeStatsOnly: false)); - tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry); - - GitProcess.Result configResult = git.SetInLocalConfig(ScalarConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true); - if (configResult.ExitCodeIsFailure) - { - string error = "Could not update config with enlistment id, error: " + configResult.Errors; - tracer.RelatedWarning(error); - } - - configResult = git.SetInLocalConfig(ScalarConstants.GitConfig.MountId, mountId, replaceAll: true); - if (configResult.ExitCodeIsFailure) - { - string error = "Could not update config with mount id, error: " + configResult.Errors; - tracer.RelatedWarning(error); - } - } - - private static string CreateMountId() - { - return Guid.NewGuid().ToString("N"); - } - - private static bool TrySetConfig(Enlistment enlistment, Dictionary configSettings, bool isRequired) - { - GitProcess git = new GitProcess(enlistment); - - Dictionary existingConfigSettings; - - // If the settings are required, then only check local config settings, because we don't want to depend on - // global settings that can then change independent of this repo. - if (!git.TryGetAllConfig(localOnly: isRequired, configSettings: out existingConfigSettings)) - { - return false; - } - - foreach (KeyValuePair setting in configSettings) - { - GitConfigSetting existingSetting; - if (setting.Value != null) - { - if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) || - (isRequired && !existingSetting.HasValue(setting.Value))) - { - GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value); - if (setConfigResult.ExitCodeIsFailure) - { - return false; - } - } - } - else - { - if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting)) - { - git.DeleteFromLocalConfig(setting.Key); - } - } - } - - return true; - } - - private string GetAlternatesPath(ScalarEnlistment enlistment) - { - return Path.Combine(enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Objects.Info.Alternates); - } - - private void CheckGitVersion(ITracer tracer, ScalarEnlistment enlistment, out string version) - { - GitVersion gitVersion = null; - if (string.IsNullOrEmpty(enlistment.GitBinPath) || !GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out string _)) - { - this.ReportErrorAndExit(tracer, "Error: Unable to retrieve the git version"); - } - - version = gitVersion.ToString(); - - if (gitVersion.Platform != ScalarConstants.SupportedGitVersion.Platform) - { - this.ReportErrorAndExit(tracer, "Error: Invalid version of git {0}. Must use scalar version.", version); - } - - if (ProcessHelper.IsDevelopmentVersion()) - { - if (gitVersion.IsLessThan(ScalarConstants.SupportedGitVersion)) - { - this.ReportErrorAndExit( - tracer, - "Error: Installed git version {0} is less than the supported version of {1}.", - gitVersion, - ScalarConstants.SupportedGitVersion); - } - else if (!gitVersion.IsEqualTo(ScalarConstants.SupportedGitVersion)) - { - this.Output.WriteLine($"Warning: Installed git version {gitVersion} does not match supported version of {ScalarConstants.SupportedGitVersion}."); - } - } - else - { - if (!gitVersion.IsEqualTo(ScalarConstants.SupportedGitVersion)) - { - this.ReportErrorAndExit( - tracer, - "Error: Installed git version {0} does not match supported version of {1}.", - gitVersion, - ScalarConstants.SupportedGitVersion); - } - } - } - - private bool TryValidateScalarVersion(ScalarEnlistment enlistment, ITracer tracer, ServerScalarConfig config, out string errorMessage, out bool errorIsFatal) - { - errorMessage = null; - errorIsFatal = false; - - using (ITracer activity = tracer.StartActivity("ValidateScalarVersion", EventLevel.Informational)) - { - Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion()); - - IEnumerable allowedGvfsClientVersions = - config != null - ? config.AllowedScalarClientVersions - : null; - - if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) - { - errorMessage = "WARNING: Unable to validate your Scalar version" + Environment.NewLine; - if (config == null) - { - errorMessage += "Could not query valid Scalar versions from: " + Uri.EscapeUriString(enlistment.RepoUrl); - } - else - { - errorMessage += "Server not configured to provide supported Scalar versions"; - } - - EventMetadata metadata = new EventMetadata(); - tracer.RelatedError(metadata, errorMessage, Keywords.Network); - - return false; - } - - foreach (ServerScalarConfig.VersionRange versionRange in config.AllowedScalarClientVersions) - { - if (currentVersion >= versionRange.Min && - (versionRange.Max == null || currentVersion <= versionRange.Max)) - { - activity.RelatedEvent( - EventLevel.Informational, - "ScalarVersionValidated", - new EventMetadata - { - { "SupportedVersionRange", versionRange }, - }); - - enlistment.SetScalarVersion(currentVersion.ToString()); - return true; - } - } - - activity.RelatedError("Scalar version {0} is not supported", currentVersion); - } - - errorMessage = "ERROR: Your Scalar version is no longer supported. Install the latest and try again."; - errorIsFatal = true; - return false; - } - - public abstract class ForExistingEnlistment : ScalarVerb - { - public ForExistingEnlistment(bool validateOrigin = true) : base(validateOrigin) - { - } - - [Value( - 0, - Required = false, - Default = "", - MetaName = "Enlistment Root Path", - HelpText = "Full or relative path to the Scalar enlistment root")] - public override string EnlistmentRootPathParameter { get; set; } - - public sealed override void Execute() - { - this.Execute(authentication: null); - } - - public void Execute(GitAuthentication authentication) - { - this.ValidatePathParameter(this.EnlistmentRootPathParameter); - - this.PreCreateEnlistment(); - ScalarEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter, authentication); - - this.Execute(enlistment); - } - - protected virtual void PreCreateEnlistment() - { - } - - protected abstract void Execute(ScalarEnlistment enlistment); - - protected void InitializeLocalCacheAndObjectsPaths( - ITracer tracer, - ScalarEnlistment enlistment, - RetryConfig retryConfig, - ServerScalarConfig serverScalarConfig, - CacheServerInfo cacheServer) - { - string error; - if (!RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot), out error)) - { - this.ReportErrorAndExit(tracer, "Failed to initialize repo metadata: " + error); - } - - this.InitializeCachePathsFromRepoMetadata(tracer, enlistment); - - // Note: Repos cloned with a version of Scalar that predates the local cache will not have a local cache configured - if (!string.IsNullOrWhiteSpace(enlistment.LocalCacheRoot)) - { - this.EnsureLocalCacheIsHealthy(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); - } - - RepoMetadata.Shutdown(); - } - - private void InitializeCachePathsFromRepoMetadata( - ITracer tracer, - ScalarEnlistment enlistment) - { - string error; - string gitObjectsRoot; - if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine git objects root from repo metadata: " + error); - } - - if (string.IsNullOrWhiteSpace(gitObjectsRoot)) - { - this.ReportErrorAndExit(tracer, "Invalid git objects root (empty or whitespace)"); - } - - string localCacheRoot; - if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine local cache path from repo metadata: " + error); - } - - // Note: localCacheRoot is allowed to be empty, this can occur when upgrading from disk layout version 11 to 12 - - string blobSizesRoot; - if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine blob sizes root from repo metadata: " + error); - } - - if (string.IsNullOrWhiteSpace(blobSizesRoot)) - { - this.ReportErrorAndExit(tracer, "Invalid blob sizes root (empty or whitespace)"); - } - - enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); - } - - private void EnsureLocalCacheIsHealthy( - ITracer tracer, - ScalarEnlistment enlistment, - RetryConfig retryConfig, - ServerScalarConfig serverScalarConfig, - CacheServerInfo cacheServer) - { - if (!Directory.Exists(enlistment.LocalCacheRoot)) - { - try - { - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {enlistment.LocalCacheRoot} missing, recreating it"); - Directory.CreateDirectory(enlistment.LocalCacheRoot); - } - catch (Exception e) - { - EventMetadata metadata = new EventMetadata(); - metadata.Add("Exception", e.ToString()); - metadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); - tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root"); - - this.ReportErrorAndExit(tracer, "Failed to create local cache: " + enlistment.LocalCacheRoot); - } - } - - // Validate that the GitObjectsRoot directory is on disk, and that the Scalar repo is configured to use it. - // If the directory is missing (and cannot be found in the mapping file) a new key for the repo will be added - // to the mapping file and used for BOTH the GitObjectsRoot and BlobSizesRoot - PhysicalFileSystem fileSystem = new PhysicalFileSystem(); - if (Directory.Exists(enlistment.GitObjectsRoot)) - { - bool gitObjectsRootInAlternates = false; - - string alternatesFilePath = this.GetAlternatesPath(enlistment); - if (File.Exists(alternatesFilePath)) - { - try - { - using (Stream stream = fileSystem.OpenFileStream( - alternatesFilePath, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - callFlushFileBuffers: false)) - { - using (StreamReader reader = new StreamReader(stream)) - { - while (!reader.EndOfStream) - { - string alternatesLine = reader.ReadLine(); - if (string.Equals(alternatesLine, enlistment.GitObjectsRoot, StringComparison.OrdinalIgnoreCase)) - { - gitObjectsRootInAlternates = true; - } - } - } - } - } - catch (Exception e) - { - EventMetadata exceptionMetadata = new EventMetadata(); - exceptionMetadata.Add("Exception", e.ToString()); - tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file"); - - this.ReportErrorAndExit(tracer, $"Failed to validate that alternates file includes git objects root: {e.Message}"); - } - } - else - { - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found"); - } - - if (!gitObjectsRootInAlternates) - { - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates"); - string error; - if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) - { - this.ReportErrorAndExit(tracer, $"Failed to update alternates file to include git objects root: {error}"); - } - } - } - else - { - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing, determining new root"); - - if (cacheServer == null) - { - cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); - } - - string error; - if (serverScalarConfig == null) - { - if (retryConfig == null) - { - if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) - { - this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); - } - } - - serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); - } - - string localCacheKey; - LocalCacheResolver localCacheResolver = new LocalCacheResolver(enlistment); - if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( - tracer, - serverScalarConfig, - cacheServer, - enlistment.LocalCacheRoot, - localCacheKey: out localCacheKey, - errorMessage: out error)) - { - this.ReportErrorAndExit(tracer, $"Previous git objects root ({enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}"); - } - - EventMetadata metadata = new EventMetadata(); - metadata.Add("localCacheRoot", enlistment.LocalCacheRoot); - metadata.Add("localCacheKey", localCacheKey); - metadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths"); - tracer.RelatedEvent(EventLevel.Informational, "ScalarVerb_EnsureLocalCacheIsHealthy_InitializePathsFromKey", metadata); - enlistment.InitializeCachePathsFromKey(enlistment.LocalCacheRoot, localCacheKey); - - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({enlistment.GitObjectsRoot}), GitPackRoot ({enlistment.GitPackRoot}), and BlobSizesRoot ({enlistment.BlobSizesRoot})"); - try - { - Directory.CreateDirectory(enlistment.GitObjectsRoot); - Directory.CreateDirectory(enlistment.GitPackRoot); - } - catch (Exception e) - { - EventMetadata exceptionMetadata = new EventMetadata(); - exceptionMetadata.Add("Exception", e.ToString()); - exceptionMetadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); - exceptionMetadata.Add("enlistment.GitObjectsRoot", enlistment.GitObjectsRoot); - exceptionMetadata.Add("enlistment.GitPackRoot", enlistment.GitPackRoot); - exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); - tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create objects, pack, and sizes folders"); - - this.ReportErrorAndExit(tracer, "Failed to create objects, pack, and sizes folders"); - } - - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file"); - if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) - { - this.ReportErrorAndExit(tracer, $"Failed to update alterates file with new objects path: {error}"); - } - - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({enlistment.GitObjectsRoot}) in repo metadata"); - RepoMetadata.Instance.SetGitObjectsRoot(enlistment.GitObjectsRoot); - - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({enlistment.BlobSizesRoot}) in repo metadata"); - RepoMetadata.Instance.SetBlobSizesRoot(enlistment.BlobSizesRoot); - } - - // Validate that the BlobSizesRoot folder is on disk. - // Note that if a user performed an action that resulted in the entire .scalarcache being deleted, the code above - // for validating GitObjectsRoot will have already taken care of generating a new key and setting a new enlistment.BlobSizesRoot path - if (!Directory.Exists(enlistment.BlobSizesRoot)) - { - tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({enlistment.BlobSizesRoot}) not found, re-creating"); - try - { - Directory.CreateDirectory(enlistment.BlobSizesRoot); - } - catch (Exception e) - { - EventMetadata exceptionMetadata = new EventMetadata(); - exceptionMetadata.Add("Exception", e.ToString()); - exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); - tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create blob sizes folder"); - - this.ReportErrorAndExit(tracer, "Failed to create blob sizes folder"); - } - } - } - - private ScalarEnlistment CreateEnlistment(string enlistmentRootPath, GitAuthentication authentication) - { - string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (string.IsNullOrWhiteSpace(gitBinPath)) - { - this.ReportErrorAndExit("Error: " + ScalarConstants.GitIsNotInstalledError); - } - - ScalarEnlistment enlistment = null; - try - { - enlistment = ScalarEnlistment.CreateFromDirectory( - enlistmentRootPath, - gitBinPath, - authentication, - createWithoutRepoURL: !this.validateOriginURL); - } - catch (InvalidRepoException e) - { - this.ReportErrorAndExit( - "Error: '{0}' is not a valid Scalar enlistment. {1}", - enlistmentRootPath, - e.Message); - } - - return enlistment; - } - } - - public abstract class ForNoEnlistment : ScalarVerb - { - public ForNoEnlistment(bool validateOrigin = true) : base(validateOrigin) - { - } - - public override string EnlistmentRootPathParameter - { - get { throw new InvalidOperationException(); } - set { throw new InvalidOperationException(); } - } - } - - public class VerbAbortedException : Exception - { - public VerbAbortedException(ScalarVerb verb) - { - this.Verb = verb; - } - - public ScalarVerb Verb { get; } - } - } -} +using CommandLine; +using Newtonsoft.Json; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Http; +using Scalar.Common.NamedPipes; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Security; + +namespace Scalar.CommandLine +{ + public abstract class ScalarVerb + { + protected const string StartServiceInstructions = "Run 'sc start Scalar.Service' from an elevated command prompt to ensure it is running."; + + private readonly bool validateOriginURL; + + public ScalarVerb(bool validateOrigin = true) + { + this.Output = Console.Out; + this.ReturnCode = ReturnCode.Success; + this.validateOriginURL = validateOrigin; + this.ServiceName = ScalarConstants.Service.ServiceName; + this.StartedByService = false; + this.Unattended = ScalarEnlistment.IsUnattended(tracer: null); + + this.InitializeDefaultParameterValues(); + } + + public abstract string EnlistmentRootPathParameter { get; set; } + + [Option( + ScalarConstants.VerbParameters.InternalUseOnly, + Required = false, + HelpText = "This parameter is reserved for internal use.")] + public string InternalParameters + { + set + { + if (!string.IsNullOrEmpty(value)) + { + try + { + InternalVerbParameters mountInternal = InternalVerbParameters.FromJson(value); + if (!string.IsNullOrEmpty(mountInternal.ServiceName)) + { + this.ServiceName = mountInternal.ServiceName; + } + + if (!string.IsNullOrEmpty(mountInternal.MaintenanceJob)) + { + this.MaintenanceJob = mountInternal.MaintenanceJob; + } + + if (!string.IsNullOrEmpty(mountInternal.PackfileMaintenanceBatchSize)) + { + this.PackfileMaintenanceBatchSize = mountInternal.PackfileMaintenanceBatchSize; + } + + this.StartedByService = mountInternal.StartedByService; + } + catch (JsonReaderException e) + { + this.ReportErrorAndExit("Failed to parse InternalParameters: {0}.\n {1}", value, e); + } + } + } + } + + public string ServiceName { get; set; } + + public string MaintenanceJob { get; set; } + + public string PackfileMaintenanceBatchSize { get; set; } + + public bool StartedByService { get; set; } + + public bool Unattended { get; private set; } + + public string ServicePipeName + { + get + { + return ScalarPlatform.Instance.GetScalarServiceNamedPipeName(this.ServiceName); + } + } + + public TextWriter Output { get; set; } + + public ReturnCode ReturnCode { get; private set; } + + protected abstract string VerbName { get; } + + public static bool TrySetRequiredGitConfigSettings(Enlistment enlistment) + { + string expectedHooksPath = Path.Combine(enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Hooks.Root); + expectedHooksPath = Paths.ConvertPathToGitFormat(expectedHooksPath); + + // These settings are required for normal Scalar functionality. + // They will override any existing local configuration values. + // + // IMPORTANT! These must parallel the settings in ControlGitRepo:Initialize + // + Dictionary requiredSettings = new Dictionary + { + { "am.keepcr", "true" }, + { "checkout.optimizenewbranch", "true" }, + { "core.autocrlf", "false" }, + { "core.commitGraph", "true" }, + { "core.fscache", "true" }, + { "core.scalar", "true" }, + { "core.multiPackIndex", "true" }, + { "core.preloadIndex", "true" }, + { "core.safecrlf", "false" }, + { "core.untrackedCache", "false" }, + { "core.repositoryformatversion", "0" }, + { "core.filemode", ScalarPlatform.Instance.FileSystem.SupportsFileMode ? "true" : "false" }, + { GitConfigSetting.CoreVirtualizeObjectsName, "true" }, + { "core.bare", "false" }, + { "core.logallrefupdates", "true" }, + { "core.hookspath", expectedHooksPath }, + { GitConfigSetting.CredentialUseHttpPath, "true" }, + { "credential.validate", "false" }, + { "diff.autoRefreshIndex", "false" }, + { "gc.auto", "0" }, + { "gui.gcwarning", "false" }, + { "index.threads", "true" }, + { "index.version", "4" }, + { "merge.stat", "false" }, + { "merge.renames", "false" }, + { "pack.useBitmaps", "false" }, + { "pack.useSparse", "true" }, + { "receive.autogc", "false" }, + { "reset.quiet", "true" }, + }; + + if (!TrySetConfig(enlistment, requiredSettings, isRequired: true)) + { + return false; + } + + return true; + } + + public static bool TrySetOptionalGitConfigSettings(Enlistment enlistment) + { + // These settings are optional, because they impact performance but not functionality of Scalar. + // These settings should only be set by the clone or repair verbs, so that they do not + // overwrite the values set by the user in their local config. + Dictionary optionalSettings = new Dictionary + { + { "status.aheadbehind", "false" }, + }; + + if (!TrySetConfig(enlistment, optionalSettings, isRequired: false)) + { + return false; + } + + return true; + } + + public abstract void Execute(); + + public virtual void InitializeDefaultParameterValues() + { + } + + protected ReturnCode Execute( + string enlistmentRootPath, + Action configureVerb = null) + where TVerb : ScalarVerb, new() + { + TVerb verb = new TVerb(); + verb.EnlistmentRootPathParameter = enlistmentRootPath; + verb.ServiceName = this.ServiceName; + verb.Unattended = this.Unattended; + + if (configureVerb != null) + { + configureVerb(verb); + } + + try + { + verb.Execute(); + } + catch (VerbAbortedException) + { + } + + return verb.ReturnCode; + } + + protected ReturnCode Execute( + ScalarEnlistment enlistment, + Action configureVerb = null) + where TVerb : ScalarVerb.ForExistingEnlistment, new() + { + TVerb verb = new TVerb(); + verb.EnlistmentRootPathParameter = enlistment.EnlistmentRoot; + verb.ServiceName = this.ServiceName; + verb.Unattended = this.Unattended; + + if (configureVerb != null) + { + configureVerb(verb); + } + + try + { + verb.Execute(enlistment.Authentication); + } + catch (VerbAbortedException) + { + } + + return verb.ReturnCode; + } + + protected bool ShowStatusWhileRunning( + Func action, + string message, + string scalarLogEnlistmentRoot) + { + return ConsoleHelper.ShowStatusWhileRunning( + action, + message, + this.Output, + showSpinner: !this.Unattended && this.Output == Console.Out && !ScalarPlatform.Instance.IsConsoleOutputRedirectedToFile(), + scalarLogEnlistmentRoot: scalarLogEnlistmentRoot, + initialDelayMs: 0); + } + + protected bool ShowStatusWhileRunning( + Func action, + string message, + bool suppressGvfsLogMessage = false) + { + string scalarLogEnlistmentRoot = null; + if (!suppressGvfsLogMessage) + { + string errorMessage; + ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out scalarLogEnlistmentRoot, out errorMessage); + } + + return this.ShowStatusWhileRunning(action, message, scalarLogEnlistmentRoot); + } + + protected bool TryAuthenticate(ITracer tracer, ScalarEnlistment enlistment, out string authErrorMessage) + { + string authError = null; + + bool result = this.ShowStatusWhileRunning( + () => enlistment.Authentication.TryInitialize(tracer, enlistment, out authError), + "Authenticating", + enlistment.EnlistmentRoot); + + authErrorMessage = authError; + return result; + } + + protected void ReportErrorAndExit(ITracer tracer, ReturnCode exitCode, string error, params object[] args) + { + if (!string.IsNullOrEmpty(error)) + { + if (args == null || args.Length == 0) + { + this.Output.WriteLine(error); + if (tracer != null && exitCode != ReturnCode.Success) + { + tracer.RelatedError(error); + } + } + else + { + this.Output.WriteLine(error, args); + if (tracer != null && exitCode != ReturnCode.Success) + { + tracer.RelatedError(error, args); + } + } + } + + this.ReturnCode = exitCode; + throw new VerbAbortedException(this); + } + + protected void ReportErrorAndExit(string error, params object[] args) + { + this.ReportErrorAndExit(tracer: null, exitCode: ReturnCode.GenericError, error: error, args: args); + } + + protected void ReportErrorAndExit(ITracer tracer, string error, params object[] args) + { + this.ReportErrorAndExit(tracer, ReturnCode.GenericError, error, args); + } + + protected RetryConfig GetRetryConfig(ITracer tracer, ScalarEnlistment enlistment, TimeSpan? timeoutOverride = null) + { + RetryConfig retryConfig; + string error; + if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); + } + + if (timeoutOverride.HasValue) + { + retryConfig.Timeout = timeoutOverride.Value; + } + + return retryConfig; + } + + protected ServerScalarConfig QueryScalarConfig(ITracer tracer, ScalarEnlistment enlistment, RetryConfig retryConfig) + { + ServerScalarConfig serverScalarConfig = null; + string errorMessage = null; + if (!this.ShowStatusWhileRunning( + () => + { + using (ConfigHttpRequestor configRequestor = new ConfigHttpRequestor(tracer, enlistment, retryConfig)) + { + const bool LogErrors = true; + return configRequestor.TryQueryScalarConfig(LogErrors, out serverScalarConfig, out _, out errorMessage); + } + }, + "Querying remote for config", + suppressGvfsLogMessage: true)) + { + this.ReportErrorAndExit(tracer, "Unable to query /gvfs/config" + Environment.NewLine + errorMessage); + } + + return serverScalarConfig; + } + + protected bool IsExistingPipeListening(string enlistmentRoot) + { + using (NamedPipeClient pipeClient = new NamedPipeClient(ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot))) + { + if (pipeClient.Connect(500)) + { + return true; + } + } + + return false; + } + + protected void ValidateClientVersions(ITracer tracer, ScalarEnlistment enlistment, ServerScalarConfig scalarConfig, bool showWarnings) + { + this.CheckGitVersion(tracer, enlistment, out string gitVersion); + enlistment.SetGitVersion(gitVersion); + + string errorMessage = null; + bool errorIsFatal = false; + if (!this.TryValidateScalarVersion(enlistment, tracer, scalarConfig, out errorMessage, out errorIsFatal)) + { + if (errorIsFatal) + { + this.ReportErrorAndExit(tracer, errorMessage); + } + else if (showWarnings) + { + this.Output.WriteLine(); + this.Output.WriteLine(errorMessage); + this.Output.WriteLine(); + } + } + } + + protected bool TryCreateAlternatesFile(PhysicalFileSystem fileSystem, ScalarEnlistment enlistment, out string errorMessage) + { + try + { + string alternatesFilePath = this.GetAlternatesPath(enlistment); + string tempFilePath = alternatesFilePath + ".tmp"; + fileSystem.WriteAllText(tempFilePath, enlistment.GitObjectsRoot); + fileSystem.MoveAndOverwriteFile(tempFilePath, alternatesFilePath); + } + catch (SecurityException e) + { + errorMessage = e.Message; + return false; + } + catch (IOException e) + { + errorMessage = e.Message; + return false; + } + + errorMessage = null; + return true; + } + + protected void BlockEmptyCacheServerUrl(string userInput) + { + if (userInput == null) + { + return; + } + + if (string.IsNullOrWhiteSpace(userInput)) + { + this.ReportErrorAndExit( +@"You must specify a value for the cache server. +You can specify a URL, a name of a configured cache server, or the special names None or Default."); + } + } + + protected CacheServerInfo ResolveCacheServer( + ITracer tracer, + CacheServerInfo cacheServer, + CacheServerResolver cacheServerResolver, + ServerScalarConfig serverScalarConfig) + { + CacheServerInfo resolvedCacheServer = cacheServer; + + if (cacheServer.Url == null) + { + string cacheServerName = cacheServer.Name; + string error = null; + + if (!cacheServerResolver.TryResolveUrlFromRemote( + cacheServerName, + serverScalarConfig, + out resolvedCacheServer, + out error)) + { + this.ReportErrorAndExit(tracer, error); + } + } + else if (cacheServer.Name.Equals(CacheServerInfo.ReservedNames.UserDefined)) + { + resolvedCacheServer = cacheServerResolver.ResolveNameFromRemote(cacheServer.Url, serverScalarConfig); + } + + this.Output.WriteLine("Using cache server: " + resolvedCacheServer); + return resolvedCacheServer; + } + + protected void ValidatePathParameter(string path) + { + if (!string.IsNullOrWhiteSpace(path)) + { + try + { + Path.GetFullPath(path); + } + catch (Exception e) + { + this.ReportErrorAndExit("Invalid path: '{0}' ({1})", path, e.Message); + } + } + } + + protected bool TryDownloadCommit( + string commitId, + ScalarEnlistment enlistment, + GitObjectsHttpRequestor objectRequestor, + ScalarGitObjects gitObjects, + GitRepo repo, + out string error, + bool checkLocalObjectCache = true) + { + if (!checkLocalObjectCache || !repo.CommitAndRootTreeExists(commitId)) + { + if (!gitObjects.TryDownloadCommit(commitId)) + { + error = "Could not download commit " + commitId + " from: " + Uri.EscapeUriString(objectRequestor.CacheServer.ObjectsEndpointUrl); + return false; + } + } + + error = null; + return true; + } + + protected bool TryDownloadRootGitAttributes(ScalarEnlistment enlistment, ScalarGitObjects gitObjects, GitRepo repo, out string error) + { + List rootEntries = new List(); + GitProcess git = new GitProcess(enlistment); + GitProcess.Result result = git.LsTree( + ScalarConstants.DotGit.HeadName, + line => rootEntries.Add(DiffTreeResult.ParseFromLsTreeLine(line)), + recursive: false); + + if (result.ExitCodeIsFailure) + { + error = "Error returned from ls-tree to find " + ScalarConstants.SpecialGitFiles.GitAttributes + " file: " + result.Errors; + return false; + } + + DiffTreeResult gitAttributes = rootEntries.FirstOrDefault(entry => entry.TargetPath.Equals(ScalarConstants.SpecialGitFiles.GitAttributes)); + if (gitAttributes == null) + { + error = "This branch does not contain a " + ScalarConstants.SpecialGitFiles.GitAttributes + " file in the root folder. This file is required by Scalar clone"; + return false; + } + + if (!repo.ObjectExists(gitAttributes.TargetSha)) + { + if (gitObjects.TryDownloadAndSaveObject(gitAttributes.TargetSha, ScalarGitObjects.RequestSource.ScalarVerb) != GitObjects.DownloadAndSaveObjectResult.Success) + { + error = "Could not download " + ScalarConstants.SpecialGitFiles.GitAttributes + " file"; + return false; + } + } + + error = null; + return true; + } + + protected void LogEnlistmentInfoAndSetConfigValues(ITracer tracer, GitProcess git, ScalarEnlistment enlistment) + { + string mountId = CreateMountId(); + EventMetadata metadata = new EventMetadata(); + metadata.Add(nameof(RepoMetadata.Instance.EnlistmentId), RepoMetadata.Instance.EnlistmentId); + metadata.Add(nameof(mountId), mountId); + metadata.Add("Enlistment", enlistment); + metadata.Add("PhysicalDiskInfo", ScalarPlatform.Instance.GetPhysicalDiskInfo(enlistment.WorkingDirectoryRoot, sizeStatsOnly: false)); + tracer.RelatedEvent(EventLevel.Informational, "EnlistmentInfo", metadata, Keywords.Telemetry); + + GitProcess.Result configResult = git.SetInLocalConfig(ScalarConstants.GitConfig.EnlistmentId, RepoMetadata.Instance.EnlistmentId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with enlistment id, error: " + configResult.Errors; + tracer.RelatedWarning(error); + } + + configResult = git.SetInLocalConfig(ScalarConstants.GitConfig.MountId, mountId, replaceAll: true); + if (configResult.ExitCodeIsFailure) + { + string error = "Could not update config with mount id, error: " + configResult.Errors; + tracer.RelatedWarning(error); + } + } + + private static string CreateMountId() + { + return Guid.NewGuid().ToString("N"); + } + + private static bool TrySetConfig(Enlistment enlistment, Dictionary configSettings, bool isRequired) + { + GitProcess git = new GitProcess(enlistment); + + Dictionary existingConfigSettings; + + // If the settings are required, then only check local config settings, because we don't want to depend on + // global settings that can then change independent of this repo. + if (!git.TryGetAllConfig(localOnly: isRequired, configSettings: out existingConfigSettings)) + { + return false; + } + + foreach (KeyValuePair setting in configSettings) + { + GitConfigSetting existingSetting; + if (setting.Value != null) + { + if (!existingConfigSettings.TryGetValue(setting.Key, out existingSetting) || + (isRequired && !existingSetting.HasValue(setting.Value))) + { + GitProcess.Result setConfigResult = git.SetInLocalConfig(setting.Key, setting.Value); + if (setConfigResult.ExitCodeIsFailure) + { + return false; + } + } + } + else + { + if (existingConfigSettings.TryGetValue(setting.Key, out existingSetting)) + { + git.DeleteFromLocalConfig(setting.Key); + } + } + } + + return true; + } + + private string GetAlternatesPath(ScalarEnlistment enlistment) + { + return Path.Combine(enlistment.WorkingDirectoryBackingRoot, ScalarConstants.DotGit.Objects.Info.Alternates); + } + + private void CheckGitVersion(ITracer tracer, ScalarEnlistment enlistment, out string version) + { + GitVersion gitVersion = null; + if (string.IsNullOrEmpty(enlistment.GitBinPath) || !GitProcess.TryGetVersion(enlistment.GitBinPath, out gitVersion, out string _)) + { + this.ReportErrorAndExit(tracer, "Error: Unable to retrieve the git version"); + } + + version = gitVersion.ToString(); + + if (gitVersion.Platform != ScalarConstants.SupportedGitVersion.Platform) + { + this.ReportErrorAndExit(tracer, "Error: Invalid version of git {0}. Must use scalar version.", version); + } + + if (ProcessHelper.IsDevelopmentVersion()) + { + if (gitVersion.IsLessThan(ScalarConstants.SupportedGitVersion)) + { + this.ReportErrorAndExit( + tracer, + "Error: Installed git version {0} is less than the supported version of {1}.", + gitVersion, + ScalarConstants.SupportedGitVersion); + } + else if (!gitVersion.IsEqualTo(ScalarConstants.SupportedGitVersion)) + { + this.Output.WriteLine($"Warning: Installed git version {gitVersion} does not match supported version of {ScalarConstants.SupportedGitVersion}."); + } + } + else + { + if (!gitVersion.IsEqualTo(ScalarConstants.SupportedGitVersion)) + { + this.ReportErrorAndExit( + tracer, + "Error: Installed git version {0} does not match supported version of {1}.", + gitVersion, + ScalarConstants.SupportedGitVersion); + } + } + } + + private bool TryValidateScalarVersion(ScalarEnlistment enlistment, ITracer tracer, ServerScalarConfig config, out string errorMessage, out bool errorIsFatal) + { + errorMessage = null; + errorIsFatal = false; + + using (ITracer activity = tracer.StartActivity("ValidateScalarVersion", EventLevel.Informational)) + { + Version currentVersion = new Version(ProcessHelper.GetCurrentProcessVersion()); + + IEnumerable allowedGvfsClientVersions = + config != null + ? config.AllowedScalarClientVersions + : null; + + if (allowedGvfsClientVersions == null || !allowedGvfsClientVersions.Any()) + { + errorMessage = "WARNING: Unable to validate your Scalar version" + Environment.NewLine; + if (config == null) + { + errorMessage += "Could not query valid Scalar versions from: " + Uri.EscapeUriString(enlistment.RepoUrl); + } + else + { + errorMessage += "Server not configured to provide supported Scalar versions"; + } + + EventMetadata metadata = new EventMetadata(); + tracer.RelatedError(metadata, errorMessage, Keywords.Network); + + return false; + } + + foreach (ServerScalarConfig.VersionRange versionRange in config.AllowedScalarClientVersions) + { + if (currentVersion >= versionRange.Min && + (versionRange.Max == null || currentVersion <= versionRange.Max)) + { + activity.RelatedEvent( + EventLevel.Informational, + "ScalarVersionValidated", + new EventMetadata + { + { "SupportedVersionRange", versionRange }, + }); + + enlistment.SetScalarVersion(currentVersion.ToString()); + return true; + } + } + + activity.RelatedError("Scalar version {0} is not supported", currentVersion); + } + + errorMessage = "ERROR: Your Scalar version is no longer supported. Install the latest and try again."; + errorIsFatal = true; + return false; + } + + public abstract class ForExistingEnlistment : ScalarVerb + { + public ForExistingEnlistment(bool validateOrigin = true) : base(validateOrigin) + { + } + + [Value( + 0, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the Scalar enlistment root")] + public override string EnlistmentRootPathParameter { get; set; } + + public sealed override void Execute() + { + this.Execute(authentication: null); + } + + public void Execute(GitAuthentication authentication) + { + this.ValidatePathParameter(this.EnlistmentRootPathParameter); + + this.PreCreateEnlistment(); + ScalarEnlistment enlistment = this.CreateEnlistment(this.EnlistmentRootPathParameter, authentication); + + this.Execute(enlistment); + } + + protected virtual void PreCreateEnlistment() + { + } + + protected abstract void Execute(ScalarEnlistment enlistment); + + protected void InitializeLocalCacheAndObjectsPaths( + ITracer tracer, + ScalarEnlistment enlistment, + RetryConfig retryConfig, + ServerScalarConfig serverScalarConfig, + CacheServerInfo cacheServer) + { + string error; + if (!RepoMetadata.TryInitialize(tracer, Path.Combine(enlistment.EnlistmentRoot, ScalarPlatform.Instance.Constants.DotScalarRoot), out error)) + { + this.ReportErrorAndExit(tracer, "Failed to initialize repo metadata: " + error); + } + + this.InitializeCachePathsFromRepoMetadata(tracer, enlistment); + + // Note: Repos cloned with a version of Scalar that predates the local cache will not have a local cache configured + if (!string.IsNullOrWhiteSpace(enlistment.LocalCacheRoot)) + { + this.EnsureLocalCacheIsHealthy(tracer, enlistment, retryConfig, serverScalarConfig, cacheServer); + } + + RepoMetadata.Shutdown(); + } + + private void InitializeCachePathsFromRepoMetadata( + ITracer tracer, + ScalarEnlistment enlistment) + { + string error; + string gitObjectsRoot; + if (!RepoMetadata.Instance.TryGetGitObjectsRoot(out gitObjectsRoot, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine git objects root from repo metadata: " + error); + } + + if (string.IsNullOrWhiteSpace(gitObjectsRoot)) + { + this.ReportErrorAndExit(tracer, "Invalid git objects root (empty or whitespace)"); + } + + string localCacheRoot; + if (!RepoMetadata.Instance.TryGetLocalCacheRoot(out localCacheRoot, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine local cache path from repo metadata: " + error); + } + + // Note: localCacheRoot is allowed to be empty, this can occur when upgrading from disk layout version 11 to 12 + + string blobSizesRoot; + if (!RepoMetadata.Instance.TryGetBlobSizesRoot(out blobSizesRoot, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine blob sizes root from repo metadata: " + error); + } + + if (string.IsNullOrWhiteSpace(blobSizesRoot)) + { + this.ReportErrorAndExit(tracer, "Invalid blob sizes root (empty or whitespace)"); + } + + enlistment.InitializeCachePaths(localCacheRoot, gitObjectsRoot, blobSizesRoot); + } + + private void EnsureLocalCacheIsHealthy( + ITracer tracer, + ScalarEnlistment enlistment, + RetryConfig retryConfig, + ServerScalarConfig serverScalarConfig, + CacheServerInfo cacheServer) + { + if (!Directory.Exists(enlistment.LocalCacheRoot)) + { + try + { + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Local cache root: {enlistment.LocalCacheRoot} missing, recreating it"); + Directory.CreateDirectory(enlistment.LocalCacheRoot); + } + catch (Exception e) + { + EventMetadata metadata = new EventMetadata(); + metadata.Add("Exception", e.ToString()); + metadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); + tracer.RelatedError(metadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to create local cache root"); + + this.ReportErrorAndExit(tracer, "Failed to create local cache: " + enlistment.LocalCacheRoot); + } + } + + // Validate that the GitObjectsRoot directory is on disk, and that the Scalar repo is configured to use it. + // If the directory is missing (and cannot be found in the mapping file) a new key for the repo will be added + // to the mapping file and used for BOTH the GitObjectsRoot and BlobSizesRoot + PhysicalFileSystem fileSystem = new PhysicalFileSystem(); + if (Directory.Exists(enlistment.GitObjectsRoot)) + { + bool gitObjectsRootInAlternates = false; + + string alternatesFilePath = this.GetAlternatesPath(enlistment); + if (File.Exists(alternatesFilePath)) + { + try + { + using (Stream stream = fileSystem.OpenFileStream( + alternatesFilePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + callFlushFileBuffers: false)) + { + using (StreamReader reader = new StreamReader(stream)) + { + while (!reader.EndOfStream) + { + string alternatesLine = reader.ReadLine(); + if (string.Equals(alternatesLine, enlistment.GitObjectsRoot, StringComparison.OrdinalIgnoreCase)) + { + gitObjectsRootInAlternates = true; + } + } + } + } + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + tracer.RelatedError(exceptionMetadata, $"{nameof(this.EnsureLocalCacheIsHealthy)}: Exception while trying to validate alternates file"); + + this.ReportErrorAndExit(tracer, $"Failed to validate that alternates file includes git objects root: {e.Message}"); + } + } + else + { + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Alternates file not found"); + } + + if (!gitObjectsRootInAlternates) + { + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing from alternates files, recreating alternates"); + string error; + if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) + { + this.ReportErrorAndExit(tracer, $"Failed to update alternates file to include git objects root: {error}"); + } + } + } + else + { + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: GitObjectsRoot ({enlistment.GitObjectsRoot}) missing, determining new root"); + + if (cacheServer == null) + { + cacheServer = CacheServerResolver.GetCacheServerFromConfig(enlistment); + } + + string error; + if (serverScalarConfig == null) + { + if (retryConfig == null) + { + if (!RetryConfig.TryLoadFromGitConfig(tracer, enlistment, out retryConfig, out error)) + { + this.ReportErrorAndExit(tracer, "Failed to determine Scalar timeout and max retries: " + error); + } + } + + serverScalarConfig = this.QueryScalarConfig(tracer, enlistment, retryConfig); + } + + string localCacheKey; + LocalCacheResolver localCacheResolver = new LocalCacheResolver(enlistment); + if (!localCacheResolver.TryGetLocalCacheKeyFromLocalConfigOrRemoteCacheServers( + tracer, + serverScalarConfig, + cacheServer, + enlistment.LocalCacheRoot, + localCacheKey: out localCacheKey, + errorMessage: out error)) + { + this.ReportErrorAndExit(tracer, $"Previous git objects root ({enlistment.GitObjectsRoot}) not found, and failed to determine new local cache key: {error}"); + } + + EventMetadata metadata = new EventMetadata(); + metadata.Add("localCacheRoot", enlistment.LocalCacheRoot); + metadata.Add("localCacheKey", localCacheKey); + metadata.Add(TracingConstants.MessageKey.InfoMessage, "Initializing and persisting updated paths"); + tracer.RelatedEvent(EventLevel.Informational, "ScalarVerb_EnsureLocalCacheIsHealthy_InitializePathsFromKey", metadata); + enlistment.InitializeCachePathsFromKey(enlistment.LocalCacheRoot, localCacheKey); + + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating GitObjectsRoot ({enlistment.GitObjectsRoot}), GitPackRoot ({enlistment.GitPackRoot}), and BlobSizesRoot ({enlistment.BlobSizesRoot})"); + try + { + Directory.CreateDirectory(enlistment.GitObjectsRoot); + Directory.CreateDirectory(enlistment.GitPackRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.LocalCacheRoot", enlistment.LocalCacheRoot); + exceptionMetadata.Add("enlistment.GitObjectsRoot", enlistment.GitObjectsRoot); + exceptionMetadata.Add("enlistment.GitPackRoot", enlistment.GitPackRoot); + exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); + tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create objects, pack, and sizes folders"); + + this.ReportErrorAndExit(tracer, "Failed to create objects, pack, and sizes folders"); + } + + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Creating new alternates file"); + if (!this.TryCreateAlternatesFile(fileSystem, enlistment, out error)) + { + this.ReportErrorAndExit(tracer, $"Failed to update alterates file with new objects path: {error}"); + } + + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving git objects root ({enlistment.GitObjectsRoot}) in repo metadata"); + RepoMetadata.Instance.SetGitObjectsRoot(enlistment.GitObjectsRoot); + + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: Saving blob sizes root ({enlistment.BlobSizesRoot}) in repo metadata"); + RepoMetadata.Instance.SetBlobSizesRoot(enlistment.BlobSizesRoot); + } + + // Validate that the BlobSizesRoot folder is on disk. + // Note that if a user performed an action that resulted in the entire .scalarcache being deleted, the code above + // for validating GitObjectsRoot will have already taken care of generating a new key and setting a new enlistment.BlobSizesRoot path + if (!Directory.Exists(enlistment.BlobSizesRoot)) + { + tracer.RelatedInfo($"{nameof(this.EnsureLocalCacheIsHealthy)}: BlobSizesRoot ({enlistment.BlobSizesRoot}) not found, re-creating"); + try + { + Directory.CreateDirectory(enlistment.BlobSizesRoot); + } + catch (Exception e) + { + EventMetadata exceptionMetadata = new EventMetadata(); + exceptionMetadata.Add("Exception", e.ToString()); + exceptionMetadata.Add("enlistment.BlobSizesRoot", enlistment.BlobSizesRoot); + tracer.RelatedError(exceptionMetadata, $"{nameof(this.InitializeLocalCacheAndObjectsPaths)}: Exception while trying to create blob sizes folder"); + + this.ReportErrorAndExit(tracer, "Failed to create blob sizes folder"); + } + } + } + + private ScalarEnlistment CreateEnlistment(string enlistmentRootPath, GitAuthentication authentication) + { + string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (string.IsNullOrWhiteSpace(gitBinPath)) + { + this.ReportErrorAndExit("Error: " + ScalarConstants.GitIsNotInstalledError); + } + + ScalarEnlistment enlistment = null; + try + { + enlistment = ScalarEnlistment.CreateFromDirectory( + enlistmentRootPath, + gitBinPath, + authentication, + createWithoutRepoURL: !this.validateOriginURL); + } + catch (InvalidRepoException e) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid Scalar enlistment. {1}", + enlistmentRootPath, + e.Message); + } + + return enlistment; + } + } + + public abstract class ForNoEnlistment : ScalarVerb + { + public ForNoEnlistment(bool validateOrigin = true) : base(validateOrigin) + { + } + + public override string EnlistmentRootPathParameter + { + get { throw new InvalidOperationException(); } + set { throw new InvalidOperationException(); } + } + } + + public class VerbAbortedException : Exception + { + public VerbAbortedException(ScalarVerb verb) + { + this.Verb = verb; + } + + public ScalarVerb Verb { get; } + } + } +} diff --git a/Scalar/CommandLine/ServiceVerb.cs b/Scalar/CommandLine/ServiceVerb.cs index ef6d944328..9f290d7622 100644 --- a/Scalar/CommandLine/ServiceVerb.cs +++ b/Scalar/CommandLine/ServiceVerb.cs @@ -1,202 +1,202 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.NamedPipes; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.CommandLine -{ - [Verb(ServiceVerbName, HelpText = "Runs commands for the Scalar service.")] - public class ServiceVerb : ScalarVerb.ForNoEnlistment - { - private const string ServiceVerbName = "service"; - - [Option( - "mount-all", - Default = false, - Required = false, - HelpText = "Mounts all repos")] - public bool MountAll { get; set; } - - [Option( - "unmount-all", - Default = false, - Required = false, - HelpText = "Unmounts all repos")] - public bool UnmountAll { get; set; } - - [Option( - "list-mounted", - Default = false, - Required = false, - HelpText = "Prints a list of all mounted repos")] - public bool List { get; set; } - - protected override string VerbName - { - get { return ServiceVerbName; } - } - - public override void Execute() - { - int optionCount = new[] { this.MountAll, this.UnmountAll, this.List }.Count(flag => flag); - if (optionCount == 0) - { - this.ReportErrorAndExit($"Error: You must specify an argument. Run 'scalar {ServiceVerbName} --help' for details."); - } - else if (optionCount > 1) - { - 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)) - { - this.ReportErrorAndExit("Error getting repo list: " + errorMessage); - } - - if (this.List) - { - foreach (string repoRoot in repoList) - { - if (this.IsRepoMounted(repoRoot)) - { - this.Output.WriteLine(repoRoot); - } - } - } - else if (this.MountAll) - { - List failedRepoRoots = new List(); - - foreach (string repoRoot in repoList) - { - if (!this.IsRepoMounted(repoRoot)) - { - this.Output.WriteLine("\r\nMounting repo at " + repoRoot); - ReturnCode result = this.Execute(repoRoot); - - if (result != ReturnCode.Success) - { - failedRepoRoots.Add(repoRoot); - } - } - } - - if (failedRepoRoots.Count() > 0) - { - string errorString = $"The following repos failed to mount:{Environment.NewLine}{string.Join("\r\n", failedRepoRoots.ToArray())}"; - Console.Error.WriteLine(errorString); - this.ReportErrorAndExit(Environment.NewLine + errorString); - } - } - else if (this.UnmountAll) - { - List failedRepoRoots = new List(); - - foreach (string repoRoot in repoList) - { - if (this.IsRepoMounted(repoRoot)) - { - this.Output.WriteLine("\r\nUnmounting repo at " + repoRoot); - ReturnCode result = this.Execute( - repoRoot, - verb => - { - verb.SkipUnregister = true; - verb.SkipLock = true; - }); - - if (result != ReturnCode.Success) - { - failedRepoRoots.Add(repoRoot); - } - } - } - - if (failedRepoRoots.Count() > 0) - { - string errorString = $"The following repos failed to unmount:{Environment.NewLine}{string.Join(Environment.NewLine, failedRepoRoots.ToArray())}"; - Console.Error.WriteLine(errorString); - this.ReportErrorAndExit(Environment.NewLine + errorString); - } - } - } - - private bool TryGetRepoList(out List repoList, out string errorMessage) - { - repoList = null; - errorMessage = string.Empty; - - NamedPipeMessages.GetActiveRepoListRequest request = new NamedPipeMessages.GetActiveRepoListRequest(); - - using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) - { - 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(); - } - - return false; - } - } - - private bool IsRepoMounted(string repoRoot) - { - // Hide the output of status - StringWriter statusOutput = new StringWriter(); - ReturnCode result = this.Execute( - repoRoot, - verb => - { - verb.Output = statusOutput; - }); - - if (result == ReturnCode.Success) - { - return true; - } - - return false; - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.NamedPipes; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.CommandLine +{ + [Verb(ServiceVerbName, HelpText = "Runs commands for the Scalar service.")] + public class ServiceVerb : ScalarVerb.ForNoEnlistment + { + private const string ServiceVerbName = "service"; + + [Option( + "mount-all", + Default = false, + Required = false, + HelpText = "Mounts all repos")] + public bool MountAll { get; set; } + + [Option( + "unmount-all", + Default = false, + Required = false, + HelpText = "Unmounts all repos")] + public bool UnmountAll { get; set; } + + [Option( + "list-mounted", + Default = false, + Required = false, + HelpText = "Prints a list of all mounted repos")] + public bool List { get; set; } + + protected override string VerbName + { + get { return ServiceVerbName; } + } + + public override void Execute() + { + int optionCount = new[] { this.MountAll, this.UnmountAll, this.List }.Count(flag => flag); + if (optionCount == 0) + { + this.ReportErrorAndExit($"Error: You must specify an argument. Run 'scalar {ServiceVerbName} --help' for details."); + } + else if (optionCount > 1) + { + 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)) + { + this.ReportErrorAndExit("Error getting repo list: " + errorMessage); + } + + if (this.List) + { + foreach (string repoRoot in repoList) + { + if (this.IsRepoMounted(repoRoot)) + { + this.Output.WriteLine(repoRoot); + } + } + } + else if (this.MountAll) + { + List failedRepoRoots = new List(); + + foreach (string repoRoot in repoList) + { + if (!this.IsRepoMounted(repoRoot)) + { + this.Output.WriteLine("\r\nMounting repo at " + repoRoot); + ReturnCode result = this.Execute(repoRoot); + + if (result != ReturnCode.Success) + { + failedRepoRoots.Add(repoRoot); + } + } + } + + if (failedRepoRoots.Count() > 0) + { + string errorString = $"The following repos failed to mount:{Environment.NewLine}{string.Join("\r\n", failedRepoRoots.ToArray())}"; + Console.Error.WriteLine(errorString); + this.ReportErrorAndExit(Environment.NewLine + errorString); + } + } + else if (this.UnmountAll) + { + List failedRepoRoots = new List(); + + foreach (string repoRoot in repoList) + { + if (this.IsRepoMounted(repoRoot)) + { + this.Output.WriteLine("\r\nUnmounting repo at " + repoRoot); + ReturnCode result = this.Execute( + repoRoot, + verb => + { + verb.SkipUnregister = true; + verb.SkipLock = true; + }); + + if (result != ReturnCode.Success) + { + failedRepoRoots.Add(repoRoot); + } + } + } + + if (failedRepoRoots.Count() > 0) + { + string errorString = $"The following repos failed to unmount:{Environment.NewLine}{string.Join(Environment.NewLine, failedRepoRoots.ToArray())}"; + Console.Error.WriteLine(errorString); + this.ReportErrorAndExit(Environment.NewLine + errorString); + } + } + } + + private bool TryGetRepoList(out List repoList, out string errorMessage) + { + repoList = null; + errorMessage = string.Empty; + + NamedPipeMessages.GetActiveRepoListRequest request = new NamedPipeMessages.GetActiveRepoListRequest(); + + using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) + { + 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(); + } + + return false; + } + } + + private bool IsRepoMounted(string repoRoot) + { + // Hide the output of status + StringWriter statusOutput = new StringWriter(); + ReturnCode result = this.Execute( + repoRoot, + verb => + { + verb.Output = statusOutput; + }); + + if (result == ReturnCode.Success) + { + return true; + } + + return false; + } + } +} diff --git a/Scalar/CommandLine/StatusVerb.cs b/Scalar/CommandLine/StatusVerb.cs index 4b5d890493..6638167e3c 100644 --- a/Scalar/CommandLine/StatusVerb.cs +++ b/Scalar/CommandLine/StatusVerb.cs @@ -1,47 +1,47 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.NamedPipes; - -namespace Scalar.CommandLine -{ - [Verb(StatusVerb.StatusVerbName, HelpText = "Get the status of the Scalar virtual repo")] - public class StatusVerb : ScalarVerb.ForExistingEnlistment - { - private const string StatusVerbName = "status"; - - protected override string VerbName - { - get { return StatusVerbName; } - } - - protected override void Execute(ScalarEnlistment enlistment) - { - using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) - { - if (!pipeClient.Connect()) - { - this.ReportErrorAndExit("Unable to connect to Scalar. Try running 'scalar mount'"); - } - - try - { - pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); - NamedPipeMessages.GetStatus.Response getStatusResponse = - NamedPipeMessages.GetStatus.Response.FromJson(pipeClient.ReadRawResponse()); - - this.Output.WriteLine("Enlistment root: " + getStatusResponse.EnlistmentRoot); - this.Output.WriteLine("Repo URL: " + getStatusResponse.RepoUrl); - this.Output.WriteLine("Cache Server: " + getStatusResponse.CacheServer); - this.Output.WriteLine("Local Cache: " + getStatusResponse.LocalCacheRoot); - this.Output.WriteLine("Mount status: " + getStatusResponse.MountStatus); - this.Output.WriteLine("Background operations: " + getStatusResponse.BackgroundOperationCount); - this.Output.WriteLine("Disk layout version: " + getStatusResponse.DiskLayoutVersion); - } - catch (BrokenPipeException e) - { - this.ReportErrorAndExit("Unable to communicate with Scalar: " + e.ToString()); - } - } - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.NamedPipes; + +namespace Scalar.CommandLine +{ + [Verb(StatusVerb.StatusVerbName, HelpText = "Get the status of the Scalar virtual repo")] + public class StatusVerb : ScalarVerb.ForExistingEnlistment + { + private const string StatusVerbName = "status"; + + protected override string VerbName + { + get { return StatusVerbName; } + } + + protected override void Execute(ScalarEnlistment enlistment) + { + using (NamedPipeClient pipeClient = new NamedPipeClient(enlistment.NamedPipeName)) + { + if (!pipeClient.Connect()) + { + this.ReportErrorAndExit("Unable to connect to Scalar. Try running 'scalar mount'"); + } + + try + { + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(pipeClient.ReadRawResponse()); + + this.Output.WriteLine("Enlistment root: " + getStatusResponse.EnlistmentRoot); + this.Output.WriteLine("Repo URL: " + getStatusResponse.RepoUrl); + this.Output.WriteLine("Cache Server: " + getStatusResponse.CacheServer); + this.Output.WriteLine("Local Cache: " + getStatusResponse.LocalCacheRoot); + this.Output.WriteLine("Mount status: " + getStatusResponse.MountStatus); + this.Output.WriteLine("Background operations: " + getStatusResponse.BackgroundOperationCount); + this.Output.WriteLine("Disk layout version: " + getStatusResponse.DiskLayoutVersion); + } + catch (BrokenPipeException e) + { + this.ReportErrorAndExit("Unable to communicate with Scalar: " + e.ToString()); + } + } + } + } +} diff --git a/Scalar/CommandLine/UnmountVerb.cs b/Scalar/CommandLine/UnmountVerb.cs index 48731cfd60..3236de43f6 100644 --- a/Scalar/CommandLine/UnmountVerb.cs +++ b/Scalar/CommandLine/UnmountVerb.cs @@ -1,194 +1,194 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.NamedPipes; - -namespace Scalar.CommandLine -{ - [Verb(UnmountVerb.UnmountVerbName, HelpText = "Unmount a Scalar virtual repo")] - public class UnmountVerb : ScalarVerb - { - private const string UnmountVerbName = "unmount"; - - [Value( - 0, - Required = false, - Default = "", - MetaName = "Enlistment Root Path", - HelpText = "Full or relative path to the Scalar enlistment root")] - public override string EnlistmentRootPathParameter { get; set; } - - [Option( - ScalarConstants.VerbParameters.Unmount.SkipLock, - Default = false, - Required = false, - HelpText = "Force unmount even if the lock is not available.")] - public bool SkipLock { get; set; } - - public bool SkipUnregister { get; set; } - - protected override string VerbName - { - get { return UnmountVerbName; } - } - - public override void Execute() - { - this.ValidatePathParameter(this.EnlistmentRootPathParameter); - - string errorMessage; - string root; - if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out root, out errorMessage)) - { - this.ReportErrorAndExit( - "Error: '{0}' is not a valid Scalar enlistment", - this.EnlistmentRootPathParameter); - } - - if (!this.ShowStatusWhileRunning( - () => { return this.Unmount(root, out errorMessage); }, - "Unmounting")) - { - this.ReportErrorAndExit(errorMessage); - } - - if (!this.Unattended && !this.SkipUnregister && ScalarPlatform.Instance.UnderConstruction.SupportsScalarService) - { - if (!this.ShowStatusWhileRunning( - () => { return this.UnregisterRepo(root, out errorMessage); }, - "Unregistering automount")) - { - this.Output.WriteLine(" WARNING: " + errorMessage); - } - } - } - - private bool Unmount(string enlistmentRoot, out string errorMessage) - { - errorMessage = string.Empty; - - string pipeName = ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot); - string rawGetStatusResponse = string.Empty; - - try - { - using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) - { - if (!pipeClient.Connect()) - { - errorMessage = "Unable to connect to Scalar.Mount"; - return false; - } - - pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); - rawGetStatusResponse = pipeClient.ReadRawResponse(); - NamedPipeMessages.GetStatus.Response getStatusResponse = - NamedPipeMessages.GetStatus.Response.FromJson(rawGetStatusResponse); - - switch (getStatusResponse.MountStatus) - { - case NamedPipeMessages.GetStatus.Mounting: - errorMessage = "Still mounting, please try again later"; - return false; - - case NamedPipeMessages.GetStatus.Unmounting: - errorMessage = "Already unmounting, please wait"; - return false; - - case NamedPipeMessages.GetStatus.Ready: - break; - - case NamedPipeMessages.GetStatus.MountFailed: - break; - - default: - errorMessage = "Unrecognized response to GetStatus: " + rawGetStatusResponse; - return false; - } - - pipeClient.SendRequest(NamedPipeMessages.Unmount.Request); - string unmountResponse = pipeClient.ReadRawResponse(); - - switch (unmountResponse) - { - case NamedPipeMessages.Unmount.Acknowledged: - string finalResponse = pipeClient.ReadRawResponse(); - if (finalResponse == NamedPipeMessages.Unmount.Completed) - { - errorMessage = string.Empty; - return true; - } - else - { - errorMessage = "Unrecognized final response to unmount: " + finalResponse; - return false; - } - - case NamedPipeMessages.Unmount.NotMounted: - errorMessage = "Unable to unmount, repo was not mounted"; - return false; - - case NamedPipeMessages.Unmount.MountFailed: - errorMessage = "Unable to unmount, previous mount attempt failed"; - return false; - - default: - errorMessage = "Unrecognized response to unmount: " + unmountResponse; - return false; - } - } - } - catch (BrokenPipeException e) - { - errorMessage = "Unable to communicate with Scalar: " + e.ToString(); - return false; - } - } - - private bool UnregisterRepo(string rootPath, out string errorMessage) - { - errorMessage = string.Empty; - NamedPipeMessages.UnregisterRepoRequest request = new NamedPipeMessages.UnregisterRepoRequest(); - request.EnlistmentRoot = rootPath; - - using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) - { - if (!client.Connect()) - { - errorMessage = "Unable to unregister repo because Scalar.Service is not responding. " + ScalarVerb.StartServiceInstructions; - return false; - } - - try - { - client.SendRequest(request.ToMessage()); - NamedPipeMessages.Message response = client.ReadResponse(); - if (response.Header == NamedPipeMessages.UnregisterRepoRequest.Response.Header) - { - NamedPipeMessages.UnregisterRepoRequest.Response message = NamedPipeMessages.UnregisterRepoRequest.Response.FromMessage(response); - - if (message.State != NamedPipeMessages.CompletionState.Success) - { - errorMessage = message.ErrorMessage; - return false; - } - else - { - errorMessage = string.Empty; - 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; - } - } - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.NamedPipes; + +namespace Scalar.CommandLine +{ + [Verb(UnmountVerb.UnmountVerbName, HelpText = "Unmount a Scalar virtual repo")] + public class UnmountVerb : ScalarVerb + { + private const string UnmountVerbName = "unmount"; + + [Value( + 0, + Required = false, + Default = "", + MetaName = "Enlistment Root Path", + HelpText = "Full or relative path to the Scalar enlistment root")] + public override string EnlistmentRootPathParameter { get; set; } + + [Option( + ScalarConstants.VerbParameters.Unmount.SkipLock, + Default = false, + Required = false, + HelpText = "Force unmount even if the lock is not available.")] + public bool SkipLock { get; set; } + + public bool SkipUnregister { get; set; } + + protected override string VerbName + { + get { return UnmountVerbName; } + } + + public override void Execute() + { + this.ValidatePathParameter(this.EnlistmentRootPathParameter); + + string errorMessage; + string root; + if (!ScalarPlatform.Instance.TryGetScalarEnlistmentRoot(this.EnlistmentRootPathParameter, out root, out errorMessage)) + { + this.ReportErrorAndExit( + "Error: '{0}' is not a valid Scalar enlistment", + this.EnlistmentRootPathParameter); + } + + if (!this.ShowStatusWhileRunning( + () => { return this.Unmount(root, out errorMessage); }, + "Unmounting")) + { + this.ReportErrorAndExit(errorMessage); + } + + if (!this.Unattended && !this.SkipUnregister && ScalarPlatform.Instance.UnderConstruction.SupportsScalarService) + { + if (!this.ShowStatusWhileRunning( + () => { return this.UnregisterRepo(root, out errorMessage); }, + "Unregistering automount")) + { + this.Output.WriteLine(" WARNING: " + errorMessage); + } + } + } + + private bool Unmount(string enlistmentRoot, out string errorMessage) + { + errorMessage = string.Empty; + + string pipeName = ScalarPlatform.Instance.GetNamedPipeName(enlistmentRoot); + string rawGetStatusResponse = string.Empty; + + try + { + using (NamedPipeClient pipeClient = new NamedPipeClient(pipeName)) + { + if (!pipeClient.Connect()) + { + errorMessage = "Unable to connect to Scalar.Mount"; + return false; + } + + pipeClient.SendRequest(NamedPipeMessages.GetStatus.Request); + rawGetStatusResponse = pipeClient.ReadRawResponse(); + NamedPipeMessages.GetStatus.Response getStatusResponse = + NamedPipeMessages.GetStatus.Response.FromJson(rawGetStatusResponse); + + switch (getStatusResponse.MountStatus) + { + case NamedPipeMessages.GetStatus.Mounting: + errorMessage = "Still mounting, please try again later"; + return false; + + case NamedPipeMessages.GetStatus.Unmounting: + errorMessage = "Already unmounting, please wait"; + return false; + + case NamedPipeMessages.GetStatus.Ready: + break; + + case NamedPipeMessages.GetStatus.MountFailed: + break; + + default: + errorMessage = "Unrecognized response to GetStatus: " + rawGetStatusResponse; + return false; + } + + pipeClient.SendRequest(NamedPipeMessages.Unmount.Request); + string unmountResponse = pipeClient.ReadRawResponse(); + + switch (unmountResponse) + { + case NamedPipeMessages.Unmount.Acknowledged: + string finalResponse = pipeClient.ReadRawResponse(); + if (finalResponse == NamedPipeMessages.Unmount.Completed) + { + errorMessage = string.Empty; + return true; + } + else + { + errorMessage = "Unrecognized final response to unmount: " + finalResponse; + return false; + } + + case NamedPipeMessages.Unmount.NotMounted: + errorMessage = "Unable to unmount, repo was not mounted"; + return false; + + case NamedPipeMessages.Unmount.MountFailed: + errorMessage = "Unable to unmount, previous mount attempt failed"; + return false; + + default: + errorMessage = "Unrecognized response to unmount: " + unmountResponse; + return false; + } + } + } + catch (BrokenPipeException e) + { + errorMessage = "Unable to communicate with Scalar: " + e.ToString(); + return false; + } + } + + private bool UnregisterRepo(string rootPath, out string errorMessage) + { + errorMessage = string.Empty; + NamedPipeMessages.UnregisterRepoRequest request = new NamedPipeMessages.UnregisterRepoRequest(); + request.EnlistmentRoot = rootPath; + + using (NamedPipeClient client = new NamedPipeClient(this.ServicePipeName)) + { + if (!client.Connect()) + { + errorMessage = "Unable to unregister repo because Scalar.Service is not responding. " + ScalarVerb.StartServiceInstructions; + return false; + } + + try + { + client.SendRequest(request.ToMessage()); + NamedPipeMessages.Message response = client.ReadResponse(); + if (response.Header == NamedPipeMessages.UnregisterRepoRequest.Response.Header) + { + NamedPipeMessages.UnregisterRepoRequest.Response message = NamedPipeMessages.UnregisterRepoRequest.Response.FromMessage(response); + + if (message.State != NamedPipeMessages.CompletionState.Success) + { + errorMessage = message.ErrorMessage; + return false; + } + else + { + errorMessage = string.Empty; + 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; + } + } + } + } +} diff --git a/Scalar/CommandLine/UpgradeVerb.cs b/Scalar/CommandLine/UpgradeVerb.cs index 9127e98381..d25156431f 100644 --- a/Scalar/CommandLine/UpgradeVerb.cs +++ b/Scalar/CommandLine/UpgradeVerb.cs @@ -1,434 +1,434 @@ -using CommandLine; -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using Scalar.Upgrader; -using System; -using System.Diagnostics; -using System.IO; - -namespace Scalar.CommandLine -{ - [Verb(UpgradeVerbName, HelpText = "Checks for new Scalar release, downloads and installs it when available.")] - public class UpgradeVerb : ScalarVerb.ForNoEnlistment - { - private const string UpgradeVerbName = "upgrade"; - private const string DryRunOption = "--dry-run"; - private const string NoVerifyOption = "--no-verify"; - private const string ConfirmOption = "--confirm"; - - private ITracer tracer; - private PhysicalFileSystem fileSystem; - private ProductUpgrader upgrader; - private InstallerPreRunChecker prerunChecker; - private ProcessLauncher processLauncher; - - private ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; - - public UpgradeVerb( - ProductUpgrader upgrader, - ITracer tracer, - PhysicalFileSystem fileSystem, - InstallerPreRunChecker prerunChecker, - ProcessLauncher processWrapper, - TextWriter output) - { - this.upgrader = upgrader; - this.tracer = tracer; - this.fileSystem = fileSystem; - this.prerunChecker = prerunChecker; - this.processLauncher = processWrapper; - this.Output = output; - this.productUpgraderPlatformStrategy = ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer); - } - - public UpgradeVerb() - { - this.fileSystem = new PhysicalFileSystem(); - this.processLauncher = new ProcessLauncher(); - this.Output = Console.Out; - } - - [Option( - "confirm", - Default = false, - Required = false, - HelpText = "Pass in this flag to actually install the newest release")] - public bool Confirmed { get; set; } - - [Option( - "dry-run", - Default = false, - Required = false, - HelpText = "Display progress and errors, but don't install Scalar")] - public bool DryRun { get; set; } - - [Option( - "no-verify", - Default = false, - Required = false, - HelpText = "This parameter is reserved for internal use.")] - public bool NoVerify { get; set; } - - protected override string VerbName - { - get { return UpgradeVerbName; } - } - - public override void Execute() - { - string error; - if (!this.TryInitializeUpgrader(out error) || !this.TryRunProductUpgrade()) - { - this.ReportErrorAndExit(this.tracer, ReturnCode.GenericError, error); - } - } - - private bool TryInitializeUpgrader(out string error) - { - if (this.DryRun && this.Confirmed) - { - error = $"{DryRunOption} and {ConfirmOption} arguments are not compatible."; - return false; - } - - if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarUpgrade) - { - error = null; - if (this.upgrader == null) - { - this.productUpgraderPlatformStrategy = ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(this.fileSystem, tracer: null); - if (!this.productUpgraderPlatformStrategy.TryPrepareLogDirectory(out error)) - { - return false; - } - - JsonTracer jsonTracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "UpgradeVerb"); - string logFilePath = ScalarEnlistment.GetNewScalarLogFileName( - ProductUpgraderInfo.GetLogDirectoryPath(), - ScalarConstants.LogFileTypes.UpgradeVerb); - jsonTracer.AddLogFileEventListener(logFilePath, EventLevel.Informational, Keywords.Any); - - this.tracer = jsonTracer; - this.prerunChecker = new InstallerPreRunChecker(this.tracer, this.Confirmed ? ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm : ScalarConstants.UpgradeVerbMessages.ScalarUpgrade); - - string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); - if (string.IsNullOrEmpty(gitBinPath)) - { - error = $"nameof(this.TryInitializeUpgrader): Unable to locate git installation. Ensure git is installed and try again."; - return false; - } - - ICredentialStore credentialStore = new GitProcess(gitBinPath, workingDirectoryRoot: null); - - ProductUpgrader upgrader; - if (ProductUpgrader.TryCreateUpgrader(this.tracer, this.fileSystem, new LocalScalarConfig(), credentialStore, this.DryRun, this.NoVerify, out upgrader, out error)) - { - this.upgrader = upgrader; - } - else - { - error = $"ERROR: {error}"; - } - } - - return this.upgrader != null; - } - else - { - error = $"ERROR: {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is not supported on this operating system."; - return false; - } - } - - private bool TryRunProductUpgrade() - { - string errorOutputFormat = Environment.NewLine + "ERROR: {0}"; - string message = null; - string cannotInstallReason = null; - Version newestVersion = null; - - bool isInstallable = this.TryCheckUpgradeInstallable(out cannotInstallReason); - if (this.ShouldRunUpgraderTool() && !isInstallable) - { - this.ReportInfoToConsole($"Cannot upgrade Scalar on this machine."); - this.Output.WriteLine(errorOutputFormat, cannotInstallReason); - return false; - } - - if (!this.upgrader.UpgradeAllowed(out message)) - { - ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( - this.tracer, - this.fileSystem); - productUpgraderInfo.DeleteAllInstallerDownloads(); - productUpgraderInfo.RecordHighestAvailableVersion(highestAvailableVersion: null); - this.ReportInfoToConsole(message); - return true; - } - - if (!this.TryRunUpgradeChecks(out newestVersion, out message)) - { - this.Output.WriteLine(errorOutputFormat, message); - this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: Upgrade checks failed. {message}"); - return false; - } - - if (newestVersion == null) - { - // Make sure there a no asset installers remaining in the Downloads directory. This can happen if user - // upgraded by manually downloading and running asset installers. - ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( - this.tracer, - this.fileSystem); - productUpgraderInfo.DeleteAllInstallerDownloads(); - this.ReportInfoToConsole(message); - return true; - } - - if (this.ShouldRunUpgraderTool()) - { - this.ReportInfoToConsole(message); - - if (!isInstallable) - { - this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: {message}"); - this.Output.WriteLine(errorOutputFormat, message); - return false; - } - - if (!this.TryRunInstaller(out message)) - { - this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: Could not launch upgrade tool. {message}"); - this.Output.WriteLine(errorOutputFormat, "Could not launch upgrade tool. " + message); - return false; - } - } - else - { - string advisoryMessage = string.Join( - Environment.NewLine, - ScalarConstants.UpgradeVerbMessages.UnmountRepoWarning, - ScalarConstants.UpgradeVerbMessages.UpgradeInstallAdvice); - this.ReportInfoToConsole(message + Environment.NewLine + Environment.NewLine + advisoryMessage + Environment.NewLine); - } - - return true; - } - - private bool TryRunUpgradeChecks( - out Version latestVersion, - out string error) - { - bool upgradeCheckSuccess = false; - string errorMessage = null; - Version version = null; - - this.ShowStatusWhileRunning( - () => - { - upgradeCheckSuccess = this.TryCheckUpgradeAvailable(out version, out errorMessage); - return upgradeCheckSuccess; - }, - "Checking for Scalar upgrades", - suppressGvfsLogMessage: true); - - latestVersion = version; - error = errorMessage; - - return upgradeCheckSuccess; - } - - private bool TryRunInstaller(out string consoleError) - { - string upgraderPath = null; - string errorMessage = null; - bool supportsInlineUpgrade = ScalarPlatform.Instance.Constants.SupportsUpgradeWhileRunning; - - this.ReportInfoToConsole("Launching upgrade tool..."); - - if (!this.TryCopyUpgradeTool(out upgraderPath, out consoleError)) - { - return false; - } - - if (!this.TryLaunchUpgradeTool( - upgraderPath, - runUpgradeInline: supportsInlineUpgrade, - consoleError: out errorMessage)) - { - return false; - } - - if (supportsInlineUpgrade) - { - this.processLauncher.WaitForExit(); - this.ReportInfoToConsole($"{Environment.NewLine}Upgrade completed."); - } - else - { - this.ReportInfoToConsole($"{Environment.NewLine}Installer launched in a new window. Do not run any git or scalar commands until the installer has completed."); - } - - consoleError = null; - return true; - } - - private bool TryCopyUpgradeTool(out string upgraderExePath, out string consoleError) - { - upgraderExePath = null; - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCopyUpgradeTool), EventLevel.Informational)) - { - if (!this.upgrader.TrySetupUpgradeApplicationDirectory(out upgraderExePath, out consoleError)) - { - return false; - } - - activity.RelatedInfo($"Successfully Copied upgrade tool to {upgraderExePath}"); - } - - return true; - } - - private bool TryLaunchUpgradeTool(string path, bool runUpgradeInline, out string consoleError) - { - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryLaunchUpgradeTool), EventLevel.Informational)) - { - Exception exception; - string args = string.Empty + (this.DryRun ? $" {DryRunOption}" : string.Empty) + (this.NoVerify ? $" {NoVerifyOption}" : string.Empty); - - // If the upgrade application is being run "inline" with the current process, then do not run the installer via the - // shell - we want the upgrade process to inherit the current terminal's stdin / stdout / sterr - if (!this.processLauncher.TryStart(path, args, !runUpgradeInline, out exception)) - { - if (exception != null) - { - consoleError = exception.Message; - this.tracer.RelatedError($"Error launching upgrade tool. {exception.ToString()}"); - } - else - { - consoleError = "Error launching upgrade tool"; - } - - return false; - } - - activity.RelatedInfo("Successfully launched upgrade tool."); - } - - consoleError = null; - return true; - } - - private bool TryCheckUpgradeAvailable( - out Version latestVersion, - out string error) - { - latestVersion = null; - error = null; - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckUpgradeAvailable), EventLevel.Informational)) - { - bool checkSucceeded = false; - Version version = null; - - checkSucceeded = this.upgrader.TryQueryNewestVersion(out version, out error); - if (!checkSucceeded) - { - return false; - } - - string currentVersion = ProcessHelper.GetCurrentProcessVersion(); - latestVersion = version; - - string message = latestVersion == null ? - $"Successfully checked for Scalar upgrades. Local version ({currentVersion}) is up-to-date." : - $"Successfully checked for Scalar upgrades. A new version is available: {latestVersion}, local version is: {currentVersion}."; - - activity.RelatedInfo(message); - } - - return true; - } - - private bool TryCheckUpgradeInstallable(out string consoleError) - { - consoleError = null; - - using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckUpgradeInstallable), EventLevel.Informational)) - { - if (!this.prerunChecker.TryRunPreUpgradeChecks( - out consoleError)) - { - return false; - } - - activity.RelatedInfo("Upgrade is installable."); - } - - return true; - } - - private bool ShouldRunUpgraderTool() - { - return this.Confirmed || this.DryRun; - } - - private void ReportInfoToConsole(string message, params object[] args) - { - this.Output.WriteLine(message, args); - } - - public class ProcessLauncher - { - public ProcessLauncher() - { - this.Process = new Process(); - } - - public Process Process { get; private set; } - - public virtual bool HasExited - { - get { return this.Process.HasExited; } - } - - public virtual int ExitCode - { - get { return this.Process.ExitCode; } - } - - public virtual void WaitForExit() - { - this.Process.WaitForExit(); - } - - public virtual bool TryStart(string path, string args, bool useShellExecute, out Exception exception) - { - this.Process.StartInfo = new ProcessStartInfo(path) - { - UseShellExecute = useShellExecute, - WorkingDirectory = Environment.SystemDirectory, - WindowStyle = ProcessWindowStyle.Normal, - Arguments = args - }; - - exception = null; - - try - { - return this.Process.Start(); - } - catch (Exception ex) - { - exception = ex; - } - - return false; - } - } - } -} +using CommandLine; +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using Scalar.Upgrader; +using System; +using System.Diagnostics; +using System.IO; + +namespace Scalar.CommandLine +{ + [Verb(UpgradeVerbName, HelpText = "Checks for new Scalar release, downloads and installs it when available.")] + public class UpgradeVerb : ScalarVerb.ForNoEnlistment + { + private const string UpgradeVerbName = "upgrade"; + private const string DryRunOption = "--dry-run"; + private const string NoVerifyOption = "--no-verify"; + private const string ConfirmOption = "--confirm"; + + private ITracer tracer; + private PhysicalFileSystem fileSystem; + private ProductUpgrader upgrader; + private InstallerPreRunChecker prerunChecker; + private ProcessLauncher processLauncher; + + private ProductUpgraderPlatformStrategy productUpgraderPlatformStrategy; + + public UpgradeVerb( + ProductUpgrader upgrader, + ITracer tracer, + PhysicalFileSystem fileSystem, + InstallerPreRunChecker prerunChecker, + ProcessLauncher processWrapper, + TextWriter output) + { + this.upgrader = upgrader; + this.tracer = tracer; + this.fileSystem = fileSystem; + this.prerunChecker = prerunChecker; + this.processLauncher = processWrapper; + this.Output = output; + this.productUpgraderPlatformStrategy = ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(fileSystem, tracer); + } + + public UpgradeVerb() + { + this.fileSystem = new PhysicalFileSystem(); + this.processLauncher = new ProcessLauncher(); + this.Output = Console.Out; + } + + [Option( + "confirm", + Default = false, + Required = false, + HelpText = "Pass in this flag to actually install the newest release")] + public bool Confirmed { get; set; } + + [Option( + "dry-run", + Default = false, + Required = false, + HelpText = "Display progress and errors, but don't install Scalar")] + public bool DryRun { get; set; } + + [Option( + "no-verify", + Default = false, + Required = false, + HelpText = "This parameter is reserved for internal use.")] + public bool NoVerify { get; set; } + + protected override string VerbName + { + get { return UpgradeVerbName; } + } + + public override void Execute() + { + string error; + if (!this.TryInitializeUpgrader(out error) || !this.TryRunProductUpgrade()) + { + this.ReportErrorAndExit(this.tracer, ReturnCode.GenericError, error); + } + } + + private bool TryInitializeUpgrader(out string error) + { + if (this.DryRun && this.Confirmed) + { + error = $"{DryRunOption} and {ConfirmOption} arguments are not compatible."; + return false; + } + + if (ScalarPlatform.Instance.UnderConstruction.SupportsScalarUpgrade) + { + error = null; + if (this.upgrader == null) + { + this.productUpgraderPlatformStrategy = ScalarPlatform.Instance.CreateProductUpgraderPlatformInteractions(this.fileSystem, tracer: null); + if (!this.productUpgraderPlatformStrategy.TryPrepareLogDirectory(out error)) + { + return false; + } + + JsonTracer jsonTracer = new JsonTracer(ScalarConstants.ScalarEtwProviderName, "UpgradeVerb"); + string logFilePath = ScalarEnlistment.GetNewScalarLogFileName( + ProductUpgraderInfo.GetLogDirectoryPath(), + ScalarConstants.LogFileTypes.UpgradeVerb); + jsonTracer.AddLogFileEventListener(logFilePath, EventLevel.Informational, Keywords.Any); + + this.tracer = jsonTracer; + this.prerunChecker = new InstallerPreRunChecker(this.tracer, this.Confirmed ? ScalarConstants.UpgradeVerbMessages.ScalarUpgradeConfirm : ScalarConstants.UpgradeVerbMessages.ScalarUpgrade); + + string gitBinPath = ScalarPlatform.Instance.GitInstallation.GetInstalledGitBinPath(); + if (string.IsNullOrEmpty(gitBinPath)) + { + error = $"nameof(this.TryInitializeUpgrader): Unable to locate git installation. Ensure git is installed and try again."; + return false; + } + + ICredentialStore credentialStore = new GitProcess(gitBinPath, workingDirectoryRoot: null); + + ProductUpgrader upgrader; + if (ProductUpgrader.TryCreateUpgrader(this.tracer, this.fileSystem, new LocalScalarConfig(), credentialStore, this.DryRun, this.NoVerify, out upgrader, out error)) + { + this.upgrader = upgrader; + } + else + { + error = $"ERROR: {error}"; + } + } + + return this.upgrader != null; + } + else + { + error = $"ERROR: {ScalarConstants.UpgradeVerbMessages.ScalarUpgrade} is not supported on this operating system."; + return false; + } + } + + private bool TryRunProductUpgrade() + { + string errorOutputFormat = Environment.NewLine + "ERROR: {0}"; + string message = null; + string cannotInstallReason = null; + Version newestVersion = null; + + bool isInstallable = this.TryCheckUpgradeInstallable(out cannotInstallReason); + if (this.ShouldRunUpgraderTool() && !isInstallable) + { + this.ReportInfoToConsole($"Cannot upgrade Scalar on this machine."); + this.Output.WriteLine(errorOutputFormat, cannotInstallReason); + return false; + } + + if (!this.upgrader.UpgradeAllowed(out message)) + { + ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( + this.tracer, + this.fileSystem); + productUpgraderInfo.DeleteAllInstallerDownloads(); + productUpgraderInfo.RecordHighestAvailableVersion(highestAvailableVersion: null); + this.ReportInfoToConsole(message); + return true; + } + + if (!this.TryRunUpgradeChecks(out newestVersion, out message)) + { + this.Output.WriteLine(errorOutputFormat, message); + this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: Upgrade checks failed. {message}"); + return false; + } + + if (newestVersion == null) + { + // Make sure there a no asset installers remaining in the Downloads directory. This can happen if user + // upgraded by manually downloading and running asset installers. + ProductUpgraderInfo productUpgraderInfo = new ProductUpgraderInfo( + this.tracer, + this.fileSystem); + productUpgraderInfo.DeleteAllInstallerDownloads(); + this.ReportInfoToConsole(message); + return true; + } + + if (this.ShouldRunUpgraderTool()) + { + this.ReportInfoToConsole(message); + + if (!isInstallable) + { + this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: {message}"); + this.Output.WriteLine(errorOutputFormat, message); + return false; + } + + if (!this.TryRunInstaller(out message)) + { + this.tracer.RelatedError($"{nameof(this.TryRunProductUpgrade)}: Could not launch upgrade tool. {message}"); + this.Output.WriteLine(errorOutputFormat, "Could not launch upgrade tool. " + message); + return false; + } + } + else + { + string advisoryMessage = string.Join( + Environment.NewLine, + ScalarConstants.UpgradeVerbMessages.UnmountRepoWarning, + ScalarConstants.UpgradeVerbMessages.UpgradeInstallAdvice); + this.ReportInfoToConsole(message + Environment.NewLine + Environment.NewLine + advisoryMessage + Environment.NewLine); + } + + return true; + } + + private bool TryRunUpgradeChecks( + out Version latestVersion, + out string error) + { + bool upgradeCheckSuccess = false; + string errorMessage = null; + Version version = null; + + this.ShowStatusWhileRunning( + () => + { + upgradeCheckSuccess = this.TryCheckUpgradeAvailable(out version, out errorMessage); + return upgradeCheckSuccess; + }, + "Checking for Scalar upgrades", + suppressGvfsLogMessage: true); + + latestVersion = version; + error = errorMessage; + + return upgradeCheckSuccess; + } + + private bool TryRunInstaller(out string consoleError) + { + string upgraderPath = null; + string errorMessage = null; + bool supportsInlineUpgrade = ScalarPlatform.Instance.Constants.SupportsUpgradeWhileRunning; + + this.ReportInfoToConsole("Launching upgrade tool..."); + + if (!this.TryCopyUpgradeTool(out upgraderPath, out consoleError)) + { + return false; + } + + if (!this.TryLaunchUpgradeTool( + upgraderPath, + runUpgradeInline: supportsInlineUpgrade, + consoleError: out errorMessage)) + { + return false; + } + + if (supportsInlineUpgrade) + { + this.processLauncher.WaitForExit(); + this.ReportInfoToConsole($"{Environment.NewLine}Upgrade completed."); + } + else + { + this.ReportInfoToConsole($"{Environment.NewLine}Installer launched in a new window. Do not run any git or scalar commands until the installer has completed."); + } + + consoleError = null; + return true; + } + + private bool TryCopyUpgradeTool(out string upgraderExePath, out string consoleError) + { + upgraderExePath = null; + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCopyUpgradeTool), EventLevel.Informational)) + { + if (!this.upgrader.TrySetupUpgradeApplicationDirectory(out upgraderExePath, out consoleError)) + { + return false; + } + + activity.RelatedInfo($"Successfully Copied upgrade tool to {upgraderExePath}"); + } + + return true; + } + + private bool TryLaunchUpgradeTool(string path, bool runUpgradeInline, out string consoleError) + { + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryLaunchUpgradeTool), EventLevel.Informational)) + { + Exception exception; + string args = string.Empty + (this.DryRun ? $" {DryRunOption}" : string.Empty) + (this.NoVerify ? $" {NoVerifyOption}" : string.Empty); + + // If the upgrade application is being run "inline" with the current process, then do not run the installer via the + // shell - we want the upgrade process to inherit the current terminal's stdin / stdout / sterr + if (!this.processLauncher.TryStart(path, args, !runUpgradeInline, out exception)) + { + if (exception != null) + { + consoleError = exception.Message; + this.tracer.RelatedError($"Error launching upgrade tool. {exception.ToString()}"); + } + else + { + consoleError = "Error launching upgrade tool"; + } + + return false; + } + + activity.RelatedInfo("Successfully launched upgrade tool."); + } + + consoleError = null; + return true; + } + + private bool TryCheckUpgradeAvailable( + out Version latestVersion, + out string error) + { + latestVersion = null; + error = null; + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckUpgradeAvailable), EventLevel.Informational)) + { + bool checkSucceeded = false; + Version version = null; + + checkSucceeded = this.upgrader.TryQueryNewestVersion(out version, out error); + if (!checkSucceeded) + { + return false; + } + + string currentVersion = ProcessHelper.GetCurrentProcessVersion(); + latestVersion = version; + + string message = latestVersion == null ? + $"Successfully checked for Scalar upgrades. Local version ({currentVersion}) is up-to-date." : + $"Successfully checked for Scalar upgrades. A new version is available: {latestVersion}, local version is: {currentVersion}."; + + activity.RelatedInfo(message); + } + + return true; + } + + private bool TryCheckUpgradeInstallable(out string consoleError) + { + consoleError = null; + + using (ITracer activity = this.tracer.StartActivity(nameof(this.TryCheckUpgradeInstallable), EventLevel.Informational)) + { + if (!this.prerunChecker.TryRunPreUpgradeChecks( + out consoleError)) + { + return false; + } + + activity.RelatedInfo("Upgrade is installable."); + } + + return true; + } + + private bool ShouldRunUpgraderTool() + { + return this.Confirmed || this.DryRun; + } + + private void ReportInfoToConsole(string message, params object[] args) + { + this.Output.WriteLine(message, args); + } + + public class ProcessLauncher + { + public ProcessLauncher() + { + this.Process = new Process(); + } + + public Process Process { get; private set; } + + public virtual bool HasExited + { + get { return this.Process.HasExited; } + } + + public virtual int ExitCode + { + get { return this.Process.ExitCode; } + } + + public virtual void WaitForExit() + { + this.Process.WaitForExit(); + } + + public virtual bool TryStart(string path, string args, bool useShellExecute, out Exception exception) + { + this.Process.StartInfo = new ProcessStartInfo(path) + { + UseShellExecute = useShellExecute, + WorkingDirectory = Environment.SystemDirectory, + WindowStyle = ProcessWindowStyle.Normal, + Arguments = args + }; + + exception = null; + + try + { + return this.Process.Start(); + } + catch (Exception ex) + { + exception = ex; + } + + return false; + } + } + } +} diff --git a/Scalar/Program.cs b/Scalar/Program.cs index a09782523a..f199197a91 100644 --- a/Scalar/Program.cs +++ b/Scalar/Program.cs @@ -1,104 +1,104 @@ -using CommandLine; -using Scalar.CommandLine; -using Scalar.Common; -using Scalar.PlatformLoader; -using System; -using System.IO; -using System.Linq; - -namespace Scalar -{ - public class Program - { - public static void Main(string[] args) - { - ScalarPlatformLoader.Initialize(); - - Type[] verbTypes = new Type[] - { - typeof(CacheServerVerb), - typeof(CloneVerb), - typeof(ConfigVerb), - typeof(DehydrateVerb), - typeof(DiagnoseVerb), - typeof(LogVerb), - typeof(MountVerb), - typeof(PrefetchVerb), - typeof(RepairVerb), - typeof(ServiceVerb), - typeof(StatusVerb), - typeof(UnmountVerb), - typeof(UpgradeVerb), - }; - - int consoleWidth = 80; - - // Running in a headless environment can result in a Console with a - // WindowWidth of 0, which causes issues with CommandLineParser - try - { - if (Console.WindowWidth > 0) - { - consoleWidth = Console.WindowWidth; - } - } - catch (IOException) - { - } - - try - { - new Parser( - settings => - { - settings.CaseSensitive = false; - settings.EnableDashDash = true; - settings.IgnoreUnknownArguments = false; - settings.HelpWriter = Console.Error; - settings.MaximumDisplayWidth = consoleWidth; - }) - .ParseArguments(args, verbTypes) - .WithNotParsed( - errors => - { - if (errors.Any(error => error is TokenError)) - { - Environment.Exit((int)ReturnCode.ParsingError); - } - }) - .WithParsed( - clone => - { - // We handle the clone verb differently, because clone cares if the enlistment path - // was not specified vs if it was specified to be the current directory - clone.Execute(); - Environment.Exit((int)ReturnCode.Success); - }) - .WithParsed( - verb => - { - verb.Execute(); - Environment.Exit((int)ReturnCode.Success); - }) - .WithParsed( - verb => - { - // For all other verbs, they don't care if the enlistment root is explicitly - // specified or implied to be the current directory - if (string.IsNullOrEmpty(verb.EnlistmentRootPathParameter)) - { - verb.EnlistmentRootPathParameter = Environment.CurrentDirectory; - } - - verb.Execute(); - Environment.Exit((int)ReturnCode.Success); - }); - } - catch (ScalarVerb.VerbAbortedException e) - { - // Calling Environment.Exit() is required, to force all background threads to exit as well - Environment.Exit((int)e.Verb.ReturnCode); - } - } - } -} +using CommandLine; +using Scalar.CommandLine; +using Scalar.Common; +using Scalar.PlatformLoader; +using System; +using System.IO; +using System.Linq; + +namespace Scalar +{ + public class Program + { + public static void Main(string[] args) + { + ScalarPlatformLoader.Initialize(); + + Type[] verbTypes = new Type[] + { + typeof(CacheServerVerb), + typeof(CloneVerb), + typeof(ConfigVerb), + typeof(DehydrateVerb), + typeof(DiagnoseVerb), + typeof(LogVerb), + typeof(MountVerb), + typeof(PrefetchVerb), + typeof(RepairVerb), + typeof(ServiceVerb), + typeof(StatusVerb), + typeof(UnmountVerb), + typeof(UpgradeVerb), + }; + + int consoleWidth = 80; + + // Running in a headless environment can result in a Console with a + // WindowWidth of 0, which causes issues with CommandLineParser + try + { + if (Console.WindowWidth > 0) + { + consoleWidth = Console.WindowWidth; + } + } + catch (IOException) + { + } + + try + { + new Parser( + settings => + { + settings.CaseSensitive = false; + settings.EnableDashDash = true; + settings.IgnoreUnknownArguments = false; + settings.HelpWriter = Console.Error; + settings.MaximumDisplayWidth = consoleWidth; + }) + .ParseArguments(args, verbTypes) + .WithNotParsed( + errors => + { + if (errors.Any(error => error is TokenError)) + { + Environment.Exit((int)ReturnCode.ParsingError); + } + }) + .WithParsed( + clone => + { + // We handle the clone verb differently, because clone cares if the enlistment path + // was not specified vs if it was specified to be the current directory + clone.Execute(); + Environment.Exit((int)ReturnCode.Success); + }) + .WithParsed( + verb => + { + verb.Execute(); + Environment.Exit((int)ReturnCode.Success); + }) + .WithParsed( + verb => + { + // For all other verbs, they don't care if the enlistment root is explicitly + // specified or implied to be the current directory + if (string.IsNullOrEmpty(verb.EnlistmentRootPathParameter)) + { + verb.EnlistmentRootPathParameter = Environment.CurrentDirectory; + } + + verb.Execute(); + Environment.Exit((int)ReturnCode.Success); + }); + } + catch (ScalarVerb.VerbAbortedException e) + { + // Calling Environment.Exit() is required, to force all background threads to exit as well + Environment.Exit((int)e.Verb.ReturnCode); + } + } + } +} diff --git a/Scalar/Properties/AssemblyInfo.cs b/Scalar/Properties/AssemblyInfo.cs index e2dbde585a..f535b6418b 100644 --- a/Scalar/Properties/AssemblyInfo.cs +++ b/Scalar/Properties/AssemblyInfo.cs @@ -1,23 +1,23 @@ -using System.Reflection; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; - -// General Information about an assembly is controlled through the following -// set of attributes. Change these attribute values to modify the information -// associated with an assembly. -[assembly: AssemblyTitle("Scalar")] -[assembly: AssemblyDescription("")] -[assembly: AssemblyConfiguration("")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Scalar")] -[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] -[assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] - -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from -// COM, set the ComVisible attribute to true on that type. -[assembly: ComVisible(false)] - -// The following GUID is for the ID of the typelib if this project is exposed to COM +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Scalar")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Scalar")] +[assembly: AssemblyCopyright("Copyright © Microsoft 2019")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM [assembly: Guid("32220664-594c-4425-b9a0-88e0be2f3d2a")] diff --git a/Scalar/RepairJobs/GitConfigRepairJob.cs b/Scalar/RepairJobs/GitConfigRepairJob.cs index 6810e53361..5b9bc79e4f 100644 --- a/Scalar/RepairJobs/GitConfigRepairJob.cs +++ b/Scalar/RepairJobs/GitConfigRepairJob.cs @@ -1,123 +1,123 @@ -using Scalar.CommandLine; -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.RepairJobs -{ - public class GitConfigRepairJob : RepairJob - { - public GitConfigRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) - : base(tracer, output, enlistment) - { - } - - public override string Name - { - get { return ".git\\config"; } - } - - public override IssueType HasIssue(List messages) - { - GitProcess git = new GitProcess(this.Enlistment); - GitProcess.ConfigResult originResult = git.GetOriginUrl(); - string error; - string originUrl; - if (!originResult.TryParseAsString(out originUrl, out error)) - { - if (error.Contains("--local")) - { - // example error: '--local can only be used inside a git repository' - // Corrupting the git config does not cause git to not recognize the current folder as "not a git repository". - // This is a symptom of deeper issues such as missing HEAD file or refs folders. - messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'scalar repair --confirm' then 'scalar repair' again."); - return IssueType.CantFix; - } - - messages.Add("Could not read origin url: " + error); - return IssueType.Fixable; - } - - if (originUrl == null) - { - messages.Add("Remote 'origin' is not configured for this repo. You can fix this by running 'git remote add origin '"); - return IssueType.CantFix; - } - - // We've validated the repo URL, so now make sure we can authenticate - try - { - ScalarEnlistment enlistment = ScalarEnlistment.CreateFromDirectory( - this.Enlistment.EnlistmentRoot, - this.Enlistment.GitBinPath, - authentication: null); - - string authError; - if (!enlistment.Authentication.TryInitialize(this.Tracer, enlistment, out authError)) - { - messages.Add("Authentication failed. Run 'scalar log' for more info."); - messages.Add(".git\\config is valid and remote 'origin' is set, but may have a typo:"); - messages.Add(originUrl.Trim()); - return IssueType.CantFix; - } - } - catch (InvalidRepoException) - { - messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'scalar repair --confirm' then 'scalar repair' again."); - return IssueType.CantFix; - } - - return IssueType.None; - } - - public override FixResult TryFixIssues(List messages) - { - string configPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Config); - string configBackupPath; - if (!this.TryRenameToBackupFile(configPath, out configBackupPath, messages)) - { - return FixResult.Failure; - } - - File.WriteAllText(configPath, string.Empty); - this.Tracer.RelatedInfo("Created empty file: " + configPath); - - if (!ScalarVerb.TrySetRequiredGitConfigSettings(this.Enlistment) || - !ScalarVerb.TrySetOptionalGitConfigSettings(this.Enlistment)) - { - messages.Add("Unable to create default .git\\config."); - this.RestoreFromBackupFile(configBackupPath, configPath, messages); - - return FixResult.Failure; - } - - // Don't output the validation output unless it turns out we couldn't fix the problem - List validationMessages = new List(); - - // HasIssue should return CantFix because we can't set the repo url ourselves, - // but getting Fixable means that we still failed - if (this.HasIssue(validationMessages) == IssueType.Fixable) - { - messages.Add("Reinitializing the .git\\config did not fix the issue. Check the errors below for more details:"); - messages.AddRange(validationMessages); - - this.RestoreFromBackupFile(configBackupPath, configPath, messages); - - return FixResult.Failure; - } - - if (!this.TryDeleteFile(configBackupPath)) - { - messages.Add("Failed to delete .git\\config backup file: " + configBackupPath); - } - - messages.Add("Reinitialized .git\\config. You will need to manually add the origin remote by running"); - messages.Add("git remote add origin "); - messages.Add("If you previously configured a custom cache server, you will need to configure it again."); - - return FixResult.ManualStepsRequired; - } - } -} +using Scalar.CommandLine; +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.RepairJobs +{ + public class GitConfigRepairJob : RepairJob + { + public GitConfigRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) + : base(tracer, output, enlistment) + { + } + + public override string Name + { + get { return ".git\\config"; } + } + + public override IssueType HasIssue(List messages) + { + GitProcess git = new GitProcess(this.Enlistment); + GitProcess.ConfigResult originResult = git.GetOriginUrl(); + string error; + string originUrl; + if (!originResult.TryParseAsString(out originUrl, out error)) + { + if (error.Contains("--local")) + { + // example error: '--local can only be used inside a git repository' + // Corrupting the git config does not cause git to not recognize the current folder as "not a git repository". + // This is a symptom of deeper issues such as missing HEAD file or refs folders. + messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'scalar repair --confirm' then 'scalar repair' again."); + return IssueType.CantFix; + } + + messages.Add("Could not read origin url: " + error); + return IssueType.Fixable; + } + + if (originUrl == null) + { + messages.Add("Remote 'origin' is not configured for this repo. You can fix this by running 'git remote add origin '"); + return IssueType.CantFix; + } + + // We've validated the repo URL, so now make sure we can authenticate + try + { + ScalarEnlistment enlistment = ScalarEnlistment.CreateFromDirectory( + this.Enlistment.EnlistmentRoot, + this.Enlistment.GitBinPath, + authentication: null); + + string authError; + if (!enlistment.Authentication.TryInitialize(this.Tracer, enlistment, out authError)) + { + messages.Add("Authentication failed. Run 'scalar log' for more info."); + messages.Add(".git\\config is valid and remote 'origin' is set, but may have a typo:"); + messages.Add(originUrl.Trim()); + return IssueType.CantFix; + } + } + catch (InvalidRepoException) + { + messages.Add("An issue was found that may be a side-effect of other issues. Fix them with 'scalar repair --confirm' then 'scalar repair' again."); + return IssueType.CantFix; + } + + return IssueType.None; + } + + public override FixResult TryFixIssues(List messages) + { + string configPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Config); + string configBackupPath; + if (!this.TryRenameToBackupFile(configPath, out configBackupPath, messages)) + { + return FixResult.Failure; + } + + File.WriteAllText(configPath, string.Empty); + this.Tracer.RelatedInfo("Created empty file: " + configPath); + + if (!ScalarVerb.TrySetRequiredGitConfigSettings(this.Enlistment) || + !ScalarVerb.TrySetOptionalGitConfigSettings(this.Enlistment)) + { + messages.Add("Unable to create default .git\\config."); + this.RestoreFromBackupFile(configBackupPath, configPath, messages); + + return FixResult.Failure; + } + + // Don't output the validation output unless it turns out we couldn't fix the problem + List validationMessages = new List(); + + // HasIssue should return CantFix because we can't set the repo url ourselves, + // but getting Fixable means that we still failed + if (this.HasIssue(validationMessages) == IssueType.Fixable) + { + messages.Add("Reinitializing the .git\\config did not fix the issue. Check the errors below for more details:"); + messages.AddRange(validationMessages); + + this.RestoreFromBackupFile(configBackupPath, configPath, messages); + + return FixResult.Failure; + } + + if (!this.TryDeleteFile(configBackupPath)) + { + messages.Add("Failed to delete .git\\config backup file: " + configBackupPath); + } + + messages.Add("Reinitialized .git\\config. You will need to manually add the origin remote by running"); + messages.Add("git remote add origin "); + messages.Add("If you previously configured a custom cache server, you will need to configure it again."); + + return FixResult.ManualStepsRequired; + } + } +} diff --git a/Scalar/RepairJobs/GitHeadRepairJob.cs b/Scalar/RepairJobs/GitHeadRepairJob.cs index 6433db523a..638373e11c 100644 --- a/Scalar/RepairJobs/GitHeadRepairJob.cs +++ b/Scalar/RepairJobs/GitHeadRepairJob.cs @@ -1,186 +1,186 @@ -using Scalar.Common; -using Scalar.Common.Git; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Scalar.RepairJobs -{ - public class GitHeadRepairJob : RepairJob - { - public GitHeadRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) - : base(tracer, output, enlistment) - { - } - - public override string Name - { - get { return @".git\HEAD"; } - } - - public override IssueType HasIssue(List messages) - { - if (TryParseHead(this.Enlistment, messages)) - { - return IssueType.None; - } - - if (!this.CanBeRepaired(messages)) - { - return IssueType.CantFix; - } - - return IssueType.Fixable; - } - - /// - /// Fixes the HEAD using the reflog to find the last SHA. - /// We detach HEAD as a side-effect of repair. - /// - public override FixResult TryFixIssues(List messages) - { - string error; - RefLogEntry refLog; - if (!TryReadLastRefLogEntry(this.Enlistment, ScalarConstants.DotGit.HeadName, out refLog, out error)) - { - this.Tracer.RelatedError(error); - messages.Add(error); - return FixResult.Failure; - } - - try - { - string refPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Head); - File.WriteAllText(refPath, refLog.TargetSha); - } - catch (IOException ex) - { - EventMetadata metadata = new EventMetadata(); - this.Tracer.RelatedError(metadata, "Failed to write HEAD: " + ex.ToString()); - return FixResult.Failure; - } - - this.Tracer.RelatedEvent( - EventLevel.Informational, - "MovedHead", - new EventMetadata - { - { "DestinationCommit", refLog.TargetSha } - }); - - messages.Add("As a result of the repair, 'git status' will now complain that HEAD is detached"); - messages.Add("You can fix this by creating a branch using 'git checkout -b '"); - - return FixResult.Success; - } - - /// - /// 'git ref-log' doesn't work if the repo is corrupted, so parsing reflogs seems like the only solution. - /// - /// A full symbolic ref name. eg. HEAD, refs/remotes/origin/HEAD, refs/heads/master - private static bool TryReadLastRefLogEntry(Enlistment enlistment, string fullSymbolicRef, out RefLogEntry refLog, out string error) - { - string refLogPath = Path.Combine(enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Logs.Root, fullSymbolicRef); - if (!File.Exists(refLogPath)) - { - refLog = null; - error = "Could not find reflog for ref '" + fullSymbolicRef + "'"; - return false; - } - - try - { - string refLogContents = File.ReadLines(refLogPath).Last(); - if (!RefLogEntry.TryParse(refLogContents, out refLog)) - { - error = "Last ref log entry for " + fullSymbolicRef + " is unparsable."; - return false; - } - } - catch (IOException ex) - { - refLog = null; - error = "IOException while reading reflog '" + refLogPath + "': " + ex.Message; - return false; - } - - error = null; - return true; - } - - private static bool TryParseHead(Enlistment enlistment, List messages) - { - string refPath = Path.Combine(enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Head); - if (!File.Exists(refPath)) - { - messages.Add("Could not find ref file for '" + ScalarConstants.DotGit.Head + "'"); - return false; - } - - string refContents; - try - { - refContents = File.ReadAllText(refPath).Trim(); - } - catch (IOException ex) - { - messages.Add("IOException while reading .git\\HEAD: " + ex.Message); - return false; - } - - const string MinimallyValidRef = "ref: refs/"; - if (refContents.StartsWith(MinimallyValidRef, StringComparison.OrdinalIgnoreCase) || - SHA1Util.IsValidShaFormat(refContents)) - { - return true; - } - - messages.Add("Invalid contents found in '" + ScalarConstants.DotGit.Head + "': " + refContents); - return false; - } - - private bool CanBeRepaired(List messages) - { - Func createErrorMessage = operation => string.Format("Can't repair HEAD while a {0} operation is in progress", operation); - - string rebasePath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.RebaseApply); - if (Directory.Exists(rebasePath)) - { - messages.Add(createErrorMessage("rebase")); - return false; - } - - string mergeHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.MergeHead); - if (File.Exists(mergeHeadPath)) - { - messages.Add(createErrorMessage("merge")); - return false; - } - - string bisectStartPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.BisectStart); - if (File.Exists(bisectStartPath)) - { - messages.Add(createErrorMessage("bisect")); - return false; - } - - string cherrypickHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.CherryPickHead); - if (File.Exists(cherrypickHeadPath)) - { - messages.Add(createErrorMessage("cherry-pick")); - return false; - } - - string revertHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.RevertHead); - if (File.Exists(revertHeadPath)) - { - messages.Add(createErrorMessage("revert")); - return false; - } - - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.Git; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; + +namespace Scalar.RepairJobs +{ + public class GitHeadRepairJob : RepairJob + { + public GitHeadRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) + : base(tracer, output, enlistment) + { + } + + public override string Name + { + get { return @".git\HEAD"; } + } + + public override IssueType HasIssue(List messages) + { + if (TryParseHead(this.Enlistment, messages)) + { + return IssueType.None; + } + + if (!this.CanBeRepaired(messages)) + { + return IssueType.CantFix; + } + + return IssueType.Fixable; + } + + /// + /// Fixes the HEAD using the reflog to find the last SHA. + /// We detach HEAD as a side-effect of repair. + /// + public override FixResult TryFixIssues(List messages) + { + string error; + RefLogEntry refLog; + if (!TryReadLastRefLogEntry(this.Enlistment, ScalarConstants.DotGit.HeadName, out refLog, out error)) + { + this.Tracer.RelatedError(error); + messages.Add(error); + return FixResult.Failure; + } + + try + { + string refPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Head); + File.WriteAllText(refPath, refLog.TargetSha); + } + catch (IOException ex) + { + EventMetadata metadata = new EventMetadata(); + this.Tracer.RelatedError(metadata, "Failed to write HEAD: " + ex.ToString()); + return FixResult.Failure; + } + + this.Tracer.RelatedEvent( + EventLevel.Informational, + "MovedHead", + new EventMetadata + { + { "DestinationCommit", refLog.TargetSha } + }); + + messages.Add("As a result of the repair, 'git status' will now complain that HEAD is detached"); + messages.Add("You can fix this by creating a branch using 'git checkout -b '"); + + return FixResult.Success; + } + + /// + /// 'git ref-log' doesn't work if the repo is corrupted, so parsing reflogs seems like the only solution. + /// + /// A full symbolic ref name. eg. HEAD, refs/remotes/origin/HEAD, refs/heads/master + private static bool TryReadLastRefLogEntry(Enlistment enlistment, string fullSymbolicRef, out RefLogEntry refLog, out string error) + { + string refLogPath = Path.Combine(enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Logs.Root, fullSymbolicRef); + if (!File.Exists(refLogPath)) + { + refLog = null; + error = "Could not find reflog for ref '" + fullSymbolicRef + "'"; + return false; + } + + try + { + string refLogContents = File.ReadLines(refLogPath).Last(); + if (!RefLogEntry.TryParse(refLogContents, out refLog)) + { + error = "Last ref log entry for " + fullSymbolicRef + " is unparsable."; + return false; + } + } + catch (IOException ex) + { + refLog = null; + error = "IOException while reading reflog '" + refLogPath + "': " + ex.Message; + return false; + } + + error = null; + return true; + } + + private static bool TryParseHead(Enlistment enlistment, List messages) + { + string refPath = Path.Combine(enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.Head); + if (!File.Exists(refPath)) + { + messages.Add("Could not find ref file for '" + ScalarConstants.DotGit.Head + "'"); + return false; + } + + string refContents; + try + { + refContents = File.ReadAllText(refPath).Trim(); + } + catch (IOException ex) + { + messages.Add("IOException while reading .git\\HEAD: " + ex.Message); + return false; + } + + const string MinimallyValidRef = "ref: refs/"; + if (refContents.StartsWith(MinimallyValidRef, StringComparison.OrdinalIgnoreCase) || + SHA1Util.IsValidShaFormat(refContents)) + { + return true; + } + + messages.Add("Invalid contents found in '" + ScalarConstants.DotGit.Head + "': " + refContents); + return false; + } + + private bool CanBeRepaired(List messages) + { + Func createErrorMessage = operation => string.Format("Can't repair HEAD while a {0} operation is in progress", operation); + + string rebasePath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.RebaseApply); + if (Directory.Exists(rebasePath)) + { + messages.Add(createErrorMessage("rebase")); + return false; + } + + string mergeHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.MergeHead); + if (File.Exists(mergeHeadPath)) + { + messages.Add(createErrorMessage("merge")); + return false; + } + + string bisectStartPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.BisectStart); + if (File.Exists(bisectStartPath)) + { + messages.Add(createErrorMessage("bisect")); + return false; + } + + string cherrypickHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.CherryPickHead); + if (File.Exists(cherrypickHeadPath)) + { + messages.Add(createErrorMessage("cherry-pick")); + return false; + } + + string revertHeadPath = Path.Combine(this.Enlistment.WorkingDirectoryRoot, ScalarConstants.DotGit.RevertHead); + if (File.Exists(revertHeadPath)) + { + messages.Add(createErrorMessage("revert")); + return false; + } + + return true; + } + } +} diff --git a/Scalar/RepairJobs/RepairJob.cs b/Scalar/RepairJobs/RepairJob.cs index dee94da6fd..58fa653ae7 100644 --- a/Scalar/RepairJobs/RepairJob.cs +++ b/Scalar/RepairJobs/RepairJob.cs @@ -1,111 +1,111 @@ -using Scalar.Common; -using Scalar.Common.FileSystem; -using Scalar.Common.Tracing; -using System; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.RepairJobs -{ - public abstract class RepairJob - { - private const string BackupExtension = ".bak"; - private PhysicalFileSystem fileSystem; - - public RepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) - { - this.Tracer = tracer; - this.Output = output; - this.Enlistment = enlistment; - this.fileSystem = new PhysicalFileSystem(); - } - - public enum IssueType - { - None, - Fixable, - CantFix - } - - public enum FixResult - { - Success, - Failure, - ManualStepsRequired - } - - public abstract string Name { get; } - - protected ITracer Tracer { get; } - protected TextWriter Output { get; } - protected ScalarEnlistment Enlistment { get; } - - public abstract IssueType HasIssue(List messages); - public abstract FixResult TryFixIssues(List messages); - - protected bool TryRenameToBackupFile(string filePath, out string backupPath, List messages) - { - backupPath = filePath + BackupExtension; - try - { - File.Move(filePath, backupPath); - this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", filePath }, { "DestinationPath", backupPath } }); - } - catch (Exception e) - { - messages.Add("Failed to back up " + filePath + " to " + backupPath); - this.Tracer.RelatedError("Exception while moving " + filePath + " to " + backupPath + ": " + e.ToString()); - return false; - } - - return true; - } - - protected void RestoreFromBackupFile(string backupPath, string originalPath, List messages) - { - try - { - File.Delete(originalPath); - File.Move(backupPath, originalPath); - this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", backupPath }, { "DestinationPath", originalPath } }); - } - catch (Exception e) - { - messages.Add("Could not restore " + originalPath + " from " + backupPath); - this.Tracer.RelatedError("Exception while restoring " + originalPath + " from " + backupPath + ": " + e.ToString()); - } - } - - protected bool TryDeleteFile(string filePath) - { - try - { - File.Delete(filePath); - this.Tracer.RelatedEvent(EventLevel.Informational, "FileDeleted", new EventMetadata { { "SourcePath", filePath } }); - } - catch (Exception e) - { - this.Tracer.RelatedError("Exception while deleting file " + filePath + ": " + e.ToString()); - return false; - } - - return true; - } - - protected bool TryDeleteFolder(string filePath) - { - try - { - this.fileSystem.DeleteDirectory(filePath); - this.Tracer.RelatedEvent(EventLevel.Informational, "FolderDeleted", new EventMetadata { { "SourcePath", filePath } }); - } - catch (Exception e) - { - this.Tracer.RelatedError("Exception while deleting folder " + filePath + ": " + e.ToString()); - return false; - } - - return true; - } - } -} +using Scalar.Common; +using Scalar.Common.FileSystem; +using Scalar.Common.Tracing; +using System; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.RepairJobs +{ + public abstract class RepairJob + { + private const string BackupExtension = ".bak"; + private PhysicalFileSystem fileSystem; + + public RepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) + { + this.Tracer = tracer; + this.Output = output; + this.Enlistment = enlistment; + this.fileSystem = new PhysicalFileSystem(); + } + + public enum IssueType + { + None, + Fixable, + CantFix + } + + public enum FixResult + { + Success, + Failure, + ManualStepsRequired + } + + public abstract string Name { get; } + + protected ITracer Tracer { get; } + protected TextWriter Output { get; } + protected ScalarEnlistment Enlistment { get; } + + public abstract IssueType HasIssue(List messages); + public abstract FixResult TryFixIssues(List messages); + + protected bool TryRenameToBackupFile(string filePath, out string backupPath, List messages) + { + backupPath = filePath + BackupExtension; + try + { + File.Move(filePath, backupPath); + this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", filePath }, { "DestinationPath", backupPath } }); + } + catch (Exception e) + { + messages.Add("Failed to back up " + filePath + " to " + backupPath); + this.Tracer.RelatedError("Exception while moving " + filePath + " to " + backupPath + ": " + e.ToString()); + return false; + } + + return true; + } + + protected void RestoreFromBackupFile(string backupPath, string originalPath, List messages) + { + try + { + File.Delete(originalPath); + File.Move(backupPath, originalPath); + this.Tracer.RelatedEvent(EventLevel.Informational, "FileMoved", new EventMetadata { { "SourcePath", backupPath }, { "DestinationPath", originalPath } }); + } + catch (Exception e) + { + messages.Add("Could not restore " + originalPath + " from " + backupPath); + this.Tracer.RelatedError("Exception while restoring " + originalPath + " from " + backupPath + ": " + e.ToString()); + } + } + + protected bool TryDeleteFile(string filePath) + { + try + { + File.Delete(filePath); + this.Tracer.RelatedEvent(EventLevel.Informational, "FileDeleted", new EventMetadata { { "SourcePath", filePath } }); + } + catch (Exception e) + { + this.Tracer.RelatedError("Exception while deleting file " + filePath + ": " + e.ToString()); + return false; + } + + return true; + } + + protected bool TryDeleteFolder(string filePath) + { + try + { + this.fileSystem.DeleteDirectory(filePath); + this.Tracer.RelatedEvent(EventLevel.Informational, "FolderDeleted", new EventMetadata { { "SourcePath", filePath } }); + } + catch (Exception e) + { + this.Tracer.RelatedError("Exception while deleting folder " + filePath + ": " + e.ToString()); + return false; + } + + return true; + } + } +} diff --git a/Scalar/RepairJobs/RepoMetadataDatabaseRepairJob.cs b/Scalar/RepairJobs/RepoMetadataDatabaseRepairJob.cs index bc806b35f2..a2030de4ac 100644 --- a/Scalar/RepairJobs/RepoMetadataDatabaseRepairJob.cs +++ b/Scalar/RepairJobs/RepoMetadataDatabaseRepairJob.cs @@ -1,44 +1,44 @@ -using Scalar.Common; -using Scalar.Common.Tracing; -using System.Collections.Generic; -using System.IO; - -namespace Scalar.RepairJobs -{ - public class RepoMetadataDatabaseRepairJob : RepairJob - { - public RepoMetadataDatabaseRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) - : base(tracer, output, enlistment) - { - } - - public override string Name - { - get { return "Repo Metadata Database"; } - } - - public override IssueType HasIssue(List messages) - { - string error; - try - { - if (!RepoMetadata.TryInitialize(this.Tracer, this.Enlistment.DotScalarRoot, out error)) - { - messages.Add("Could not open repo metadata: " + error); - return IssueType.CantFix; - } - } - finally - { - RepoMetadata.Shutdown(); - } - - return IssueType.None; - } - - public override FixResult TryFixIssues(List messages) - { - return FixResult.Failure; - } - } -} +using Scalar.Common; +using Scalar.Common.Tracing; +using System.Collections.Generic; +using System.IO; + +namespace Scalar.RepairJobs +{ + public class RepoMetadataDatabaseRepairJob : RepairJob + { + public RepoMetadataDatabaseRepairJob(ITracer tracer, TextWriter output, ScalarEnlistment enlistment) + : base(tracer, output, enlistment) + { + } + + public override string Name + { + get { return "Repo Metadata Database"; } + } + + public override IssueType HasIssue(List messages) + { + string error; + try + { + if (!RepoMetadata.TryInitialize(this.Tracer, this.Enlistment.DotScalarRoot, out error)) + { + messages.Add("Could not open repo metadata: " + error); + return IssueType.CantFix; + } + } + finally + { + RepoMetadata.Shutdown(); + } + + return IssueType.None; + } + + public override FixResult TryFixIssues(List messages) + { + return FixResult.Failure; + } + } +} diff --git a/Scalar/Scalar.Windows.csproj b/Scalar/Scalar.Windows.csproj index d9052a98dc..8718be1f27 100644 --- a/Scalar/Scalar.Windows.csproj +++ b/Scalar/Scalar.Windows.csproj @@ -1,186 +1,186 @@ - - - - - - {32220664-594C-4425-B9A0-88E0BE2F3D2A} - Exe - Properties - Scalar - Scalar - v4.6.1 - 512 - true - - - - - true - DEBUG;TRACE - full - x64 - prompt - true - - - TRACE - true - pdbonly - x64 - prompt - true - - - - False - ..\..\packages\CommandLineParser.2.1.1-beta\lib\net45\CommandLine.dll - True - - - ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll - - - False - ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll - True - - - ..\..\packages\NuGet.Commands.4.9.2\lib\net46\NuGet.Commands.dll - - - ..\..\packages\NuGet.Common.4.9.2\lib\net46\NuGet.Common.dll - - - ..\..\packages\NuGet.Configuration.4.9.2\lib\net46\NuGet.Configuration.dll - - - ..\..\packages\NuGet.Credentials.4.9.2\lib\net46\NuGet.Credentials.dll - - - ..\..\packages\NuGet.DependencyResolver.Core.4.9.2\lib\net46\NuGet.DependencyResolver.Core.dll - - - ..\..\packages\NuGet.Frameworks.4.9.2\lib\net46\NuGet.Frameworks.dll - - - ..\..\packages\NuGet.LibraryModel.4.9.2\lib\net46\NuGet.LibraryModel.dll - - - ..\..\packages\NuGet.Packaging.4.9.2\lib\net46\NuGet.Packaging.dll - - - ..\..\packages\NuGet.Packaging.Core.4.9.2\lib\net46\NuGet.Packaging.Core.dll - - - ..\..\packages\NuGet.ProjectModel.4.9.2\lib\net46\NuGet.ProjectModel.dll - - - ..\..\packages\NuGet.Protocol.4.9.2\lib\net46\NuGet.Protocol.dll - - - ..\..\packages\NuGet.Versioning.4.9.2\lib\net46\NuGet.Versioning.dll - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll - - - ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll - - - ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll - - - ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PlatformLoader.Windows.cs - - - - - - - Designer - - - PreserveNewest - - - - - {374bf1e5-0b2d-4d4a-bd5e-4212299def09} - Scalar.Common - - - {4ce404e7-d3fc-471c-993c-64615861ea63} - Scalar.Platform.Windows - - - - - - - - - xcopy /Y $(BuildOutputDir)\Scalar.ReadObjectHook.Windows\bin\$(Platform)\$(Configuration)\Scalar.ReadObjectHook.* $(TargetDir) - - - - - - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - - - - - + + + + + + {32220664-594C-4425-B9A0-88E0BE2F3D2A} + Exe + Properties + Scalar + Scalar + v4.6.1 + 512 + true + + + + + true + DEBUG;TRACE + full + x64 + prompt + true + + + TRACE + true + pdbonly + x64 + prompt + true + + + + False + ..\..\packages\CommandLineParser.2.1.1-beta\lib\net45\CommandLine.dll + True + + + ..\..\packages\Microsoft.Data.Sqlite.Core.2.2.4\lib\netstandard2.0\Microsoft.Data.Sqlite.dll + + + False + ..\..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + True + + + ..\..\packages\NuGet.Commands.4.9.2\lib\net46\NuGet.Commands.dll + + + ..\..\packages\NuGet.Common.4.9.2\lib\net46\NuGet.Common.dll + + + ..\..\packages\NuGet.Configuration.4.9.2\lib\net46\NuGet.Configuration.dll + + + ..\..\packages\NuGet.Credentials.4.9.2\lib\net46\NuGet.Credentials.dll + + + ..\..\packages\NuGet.DependencyResolver.Core.4.9.2\lib\net46\NuGet.DependencyResolver.Core.dll + + + ..\..\packages\NuGet.Frameworks.4.9.2\lib\net46\NuGet.Frameworks.dll + + + ..\..\packages\NuGet.LibraryModel.4.9.2\lib\net46\NuGet.LibraryModel.dll + + + ..\..\packages\NuGet.Packaging.4.9.2\lib\net46\NuGet.Packaging.dll + + + ..\..\packages\NuGet.Packaging.Core.4.9.2\lib\net46\NuGet.Packaging.Core.dll + + + ..\..\packages\NuGet.ProjectModel.4.9.2\lib\net46\NuGet.ProjectModel.dll + + + ..\..\packages\NuGet.Protocol.4.9.2\lib\net46\NuGet.Protocol.dll + + + ..\..\packages\NuGet.Versioning.4.9.2\lib\net46\NuGet.Versioning.dll + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_green.dll + + + ..\..\packages\SQLitePCLRaw.bundle_green.1.1.12\lib\net45\SQLitePCLRaw.batteries_v2.dll + + + ..\..\packages\SQLitePCLRaw.core.1.1.12\lib\net45\SQLitePCLRaw.core.dll + + + ..\..\packages\SQLitePCLRaw.provider.e_sqlite3.net45.1.1.12\lib\net45\SQLitePCLRaw.provider.e_sqlite3.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PlatformLoader.Windows.cs + + + + + + + Designer + + + PreserveNewest + + + + + {374bf1e5-0b2d-4d4a-bd5e-4212299def09} + Scalar.Common + + + {4ce404e7-d3fc-471c-993c-64615861ea63} + Scalar.Platform.Windows + + + + + + + + + xcopy /Y $(BuildOutputDir)\Scalar.ReadObjectHook.Windows\bin\$(Platform)\$(Configuration)\Scalar.ReadObjectHook.* $(TargetDir) + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + + + + + + diff --git a/Scalar/packages.config b/Scalar/packages.config index 3a637b3962..7bc2625259 100644 --- a/Scalar/packages.config +++ b/Scalar/packages.config @@ -1,27 +1,27 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Scripts/BuildScalarForWindows.bat b/Scripts/BuildScalarForWindows.bat index 738f18b0f8..06f3bd1aaf 100644 --- a/Scripts/BuildScalarForWindows.bat +++ b/Scripts/BuildScalarForWindows.bat @@ -1,42 +1,42 @@ -@ECHO OFF -SETLOCAL -setlocal enabledelayedexpansion -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") -IF "%2"=="" (SET "ScalarVersion=0.2.173.2") ELSE (SET "ScalarVersion=%2") - -SET SolutionConfiguration=%Configuration%.Windows - -SET nuget="%Scalar_TOOLSDIR%\nuget.exe" -IF NOT EXIST %nuget% ( - mkdir %nuget%\.. - powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile %nuget%" -) - -:: Acquire vswhere to find dev15 installations reliably. -SET vswherever=2.6.7 -%nuget% install vswhere -Version %vswherever% || exit /b 1 -SET vswhere=%Scalar_PACKAGESDIR%\vswhere.%vswherever%\tools\vswhere.exe - -:: Assumes default installation location for Windows 10 SDKs -IF NOT EXIST "c:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0" ( - echo ERROR: Could not find Windows 10 SDK Version 10240 - exit /b 1 -) - -:: Use vswhere to find the latest VS installation with the msbuild component. -:: See https://github.com/Microsoft/vswhere/wiki/Find-MSBuild -for /f "usebackq tokens=*" %%i in (`%vswhere% -all -prerelease -latest -products * -requires Microsoft.Component.MSBuild Microsoft.VisualStudio.Workload.ManagedDesktop Microsoft.VisualStudio.Workload.NativeDesktop Microsoft.VisualStudio.Workload.NetCoreTools Microsoft.Net.Core.Component.SDK.2.1 -find MSBuild\**\Bin\amd64\MSBuild.exe`) do ( - set msbuild="%%i" -) - -IF NOT DEFINED msbuild ( - echo ERROR: Could not locate a Visual Studio installation with required components. - echo Refer to Readme.md for a list of the required Visual Studio components. - exit /b 10 -) - -%msbuild% %Scalar_SRCDIR%\Scalar.sln /p:ScalarVersion=%ScalarVersion% /p:Configuration=%SolutionConfiguration% /p:Platform=x64 || exit /b 1 - -ENDLOCAL +@ECHO OFF +SETLOCAL +setlocal enabledelayedexpansion +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") +IF "%2"=="" (SET "ScalarVersion=0.2.173.2") ELSE (SET "ScalarVersion=%2") + +SET SolutionConfiguration=%Configuration%.Windows + +SET nuget="%Scalar_TOOLSDIR%\nuget.exe" +IF NOT EXIST %nuget% ( + mkdir %nuget%\.. + powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile %nuget%" +) + +:: Acquire vswhere to find dev15 installations reliably. +SET vswherever=2.6.7 +%nuget% install vswhere -Version %vswherever% || exit /b 1 +SET vswhere=%Scalar_PACKAGESDIR%\vswhere.%vswherever%\tools\vswhere.exe + +:: Assumes default installation location for Windows 10 SDKs +IF NOT EXIST "c:\Program Files (x86)\Windows Kits\10\Include\10.0.10240.0" ( + echo ERROR: Could not find Windows 10 SDK Version 10240 + exit /b 1 +) + +:: Use vswhere to find the latest VS installation with the msbuild component. +:: See https://github.com/Microsoft/vswhere/wiki/Find-MSBuild +for /f "usebackq tokens=*" %%i in (`%vswhere% -all -prerelease -latest -products * -requires Microsoft.Component.MSBuild Microsoft.VisualStudio.Workload.ManagedDesktop Microsoft.VisualStudio.Workload.NativeDesktop Microsoft.VisualStudio.Workload.NetCoreTools Microsoft.Net.Core.Component.SDK.2.1 -find MSBuild\**\Bin\amd64\MSBuild.exe`) do ( + set msbuild="%%i" +) + +IF NOT DEFINED msbuild ( + echo ERROR: Could not locate a Visual Studio installation with required components. + echo Refer to Readme.md for a list of the required Visual Studio components. + exit /b 10 +) + +%msbuild% %Scalar_SRCDIR%\Scalar.sln /p:ScalarVersion=%ScalarVersion% /p:Configuration=%SolutionConfiguration% /p:Platform=x64 || exit /b 1 + +ENDLOCAL diff --git a/Scripts/CI/CreateBuildDrop.bat b/Scripts/CI/CreateBuildDrop.bat index e97291e58f..b61c50e73e 100644 --- a/Scripts/CI/CreateBuildDrop.bat +++ b/Scripts/CI/CreateBuildDrop.bat @@ -1,36 +1,36 @@ -@ECHO OFF -CALL %~dp0\..\InitializeEnvironment.bat || EXIT /b 10 - -IF "%1"=="" GOTO USAGE -IF "%2"=="" GOTO USAGE - -SETLOCAL enableextensions -SET Configuration=%1 -SET Scalar_STAGEDIR=%2 - -REM Prepare the staging directories for functional tests. -IF EXIST %Scalar_STAGEDIR% ( - rmdir /s /q %Scalar_STAGEDIR% -) - -mkdir %Scalar_STAGEDIR%\src\Scripts -mkdir %Scalar_STAGEDIR%\BuildOutput\Scalar.Build -mkdir %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1 - -REM Make a minimal 'test' enlistment to pass along our pipeline. -copy %Scalar_SCRIPTSDIR%\*.* %Scalar_STAGEDIR%\src\Scripts\ || exit /b 1 -copy %Scalar_OUTPUTDIR%\Scalar.Build\*.* %Scalar_STAGEDIR%\BuildOutput\Scalar.Build -dotnet publish %Scalar_SRCDIR%\Scalar.FunctionalTests\Scalar.FunctionalTests.csproj -p:StyleCopEnabled=False --self-contained --framework netcoreapp2.1 -r win-x64 -c Release -o %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ || exit /b 1 -robocopy %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ /E /XC /XN /XO -IF %ERRORLEVEL% GTR 7 ( - echo "ERROR: robocopy had at least one failure" - exit /b 1 -) -GOTO END - -:USAGE -echo "ERROR: Usage: CreateBuildDrop.bat [configuration] [build drop root directory]" -exit /b 1 - -:END -exit 0 +@ECHO OFF +CALL %~dp0\..\InitializeEnvironment.bat || EXIT /b 10 + +IF "%1"=="" GOTO USAGE +IF "%2"=="" GOTO USAGE + +SETLOCAL enableextensions +SET Configuration=%1 +SET Scalar_STAGEDIR=%2 + +REM Prepare the staging directories for functional tests. +IF EXIST %Scalar_STAGEDIR% ( + rmdir /s /q %Scalar_STAGEDIR% +) + +mkdir %Scalar_STAGEDIR%\src\Scripts +mkdir %Scalar_STAGEDIR%\BuildOutput\Scalar.Build +mkdir %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1 + +REM Make a minimal 'test' enlistment to pass along our pipeline. +copy %Scalar_SCRIPTSDIR%\*.* %Scalar_STAGEDIR%\src\Scripts\ || exit /b 1 +copy %Scalar_OUTPUTDIR%\Scalar.Build\*.* %Scalar_STAGEDIR%\BuildOutput\Scalar.Build +dotnet publish %Scalar_SRCDIR%\Scalar.FunctionalTests\Scalar.FunctionalTests.csproj -p:StyleCopEnabled=False --self-contained --framework netcoreapp2.1 -r win-x64 -c Release -o %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ || exit /b 1 +robocopy %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ %Scalar_STAGEDIR%\BuildOutput\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\ /E /XC /XN /XO +IF %ERRORLEVEL% GTR 7 ( + echo "ERROR: robocopy had at least one failure" + exit /b 1 +) +GOTO END + +:USAGE +echo "ERROR: Usage: CreateBuildDrop.bat [configuration] [build drop root directory]" +exit /b 1 + +:END +exit 0 diff --git a/Scripts/CapturePerfView.bat b/Scripts/CapturePerfView.bat index b7ba020998..2721c5db36 100644 --- a/Scripts/CapturePerfView.bat +++ b/Scripts/CapturePerfView.bat @@ -1,114 +1,114 @@ -@if not defined _echo echo off -setlocal - -set filename=%~n0 -goto :parseargs - -:showhelp -echo. -echo Captures a system wide PerfView while running a command then compresses it into a zip. -echo. -echo USAGE: %filename% command -echo. -echo EXAMPLES: -echo %filename% git status -echo %filename% git fetch -goto :end - -:parseargs -if "%1" == "" goto :showhelp -if /i "%1" == "/?" goto :showhelp -if /i "%1" == "-?" goto :showhelp -if /i "%1" == "/h" goto :showhelp -if /i "%1" == "-h" goto :showhelp -if /i "%1" == "/help" goto :showhelp -if /i "%1" == "-help" goto :showhelp - -:: Find the given command on the path, then look for a .PDB file next to it -:VerifyPDB -set P2=.;%PATH% -for %%e in (%PATHEXT%) do @for %%i in (%~n1%%e) do @if NOT "%%~$P2:i"=="" if NOT exist "%%~dpn$P2:i.pdb" ( - echo Unable to locate PDB file %%~dpn$P2:i.pdb. Aborting %filename% 1>&2 - exit /B 1 -) - -:VerifyPerfView -where /q perfview || ( - echo Please see the PerfView GitHub Download Page to download an up-to-date version 1>&2 - echo of PerfView and copy it to a directory in your path. 1>&2 - echo. 1>&2 - echo https://github.com/Microsoft/perfview/blob/master/documentation/Downloading.md 1>&2 - exit /B 2 -) - -:: Generate output filenames -if NOT "%_NTUSER%" == "" ( - set perfviewfilename=%_NTUSER%-%~n1-%2 -) ELSE ( - if NOT "%USERNAME%" == "" ( - set perfviewfilename=%USERNAME%-%~n1-%2 - ) ELSE ( - set perfviewfilename=%~n1-%2 - ) -) -set perfviewstartlog=%perfviewfilename%.start.log.txt -set perfviewstoplog=%perfviewfilename%.end.log.txt - -:: Capture the perfview without requiring any human intervention -:CapturePerfView -echo Capture perf view for '%*'... -perfview start /AcceptEULA /NoGui /NoNGenRundown /Merge /Zip /Providers:*Microsoft.Git.Scalar:@StacksEnabled=true,*Microsoft.Internal.Git.Plugin:@StacksEnabled=true,*Microsoft.OSGENG.Testing.GitMsWrapper:@StacksEnabled=true /kernelEvents=default+FileIOInit /logfile:"%perfviewstartlog%" "%perfviewfilename%" || goto :HandlePerfViewStartError -echo. -set STARTTIME=%TIME% -%* -set ENDTIME=%TIME% -echo. -CALL :PrintElapsedTime - -:: Merge perfview into ZIP file -echo Merging and compressing perf view... -perfview stop /AcceptEULA /NoGui /NoNGenRundown /Merge /Zip /Providers:*Microsoft.Git.Scalar:@StacksEnabled=true,*Microsoft.Internal.Git.Plugin:@StacksEnabled=true,*Microsoft.OSGENG.Testing.GitMsWrapper:@StacksEnabled=true /kernelEvents=default+FileIOInit /logfile:"%perfviewstoplog%" || goto :HandlePerfViewStopError -CALL :CheckForFile -echo PerfView trace can be found in "%perfviewfilename%.etl.zip" -goto :end - -:HandlePerfViewStartError -echo Could not start perfview, please see %perfviewstartlog% for details. -EXIT /B 3 - -:HandlePerfViewStopError -echo Could not stop perfview, please see %perfviewstoplog% for details. -EXIT /B 4 - -:: Now wait for perfview to complete writing out the file -:CheckForFile -IF EXIST "%perfviewfilename%.etl.zip" EXIT /B 0 -TIMEOUT /T 1 >nul -goto :CheckForFile - -:PrintElapsedTime -:: Change formatting for the start and end times -for /F "tokens=1-4 delims=:.," %%a in ("%STARTTIME%") do ( - set /A "start=(((%%a*60)+1%%b %% 100)*60+1%%c %% 100)*100+1%%d %% 100" -) - -for /F "tokens=1-4 delims=:.," %%a in ("%ENDTIME%") do ( - set /A "end=(((%%a*60)+1%%b %% 100)*60+1%%c %% 100)*100+1%%d %% 100" -) - -:: Calculate the elapsed time by subtracting values -set /A elapsed=end-start - -:: Format the results for output -set /A hh=elapsed/(60*60*100), rest=elapsed%%(60*60*100), mm=rest/(60*100), rest%%=60*100, ss=rest/100, cc=rest%%100 -if %hh% lss 10 set hh=0%hh% -if %mm% lss 10 set mm=0%mm% -if %ss% lss 10 set ss=0%ss% -if %cc% lss 10 set cc=0%cc% - -set DURATION=%hh%:%mm%:%ss%.%cc% -echo Command duration : %DURATION% -EXIT /B 0 - -:end -endlocal +@if not defined _echo echo off +setlocal + +set filename=%~n0 +goto :parseargs + +:showhelp +echo. +echo Captures a system wide PerfView while running a command then compresses it into a zip. +echo. +echo USAGE: %filename% command +echo. +echo EXAMPLES: +echo %filename% git status +echo %filename% git fetch +goto :end + +:parseargs +if "%1" == "" goto :showhelp +if /i "%1" == "/?" goto :showhelp +if /i "%1" == "-?" goto :showhelp +if /i "%1" == "/h" goto :showhelp +if /i "%1" == "-h" goto :showhelp +if /i "%1" == "/help" goto :showhelp +if /i "%1" == "-help" goto :showhelp + +:: Find the given command on the path, then look for a .PDB file next to it +:VerifyPDB +set P2=.;%PATH% +for %%e in (%PATHEXT%) do @for %%i in (%~n1%%e) do @if NOT "%%~$P2:i"=="" if NOT exist "%%~dpn$P2:i.pdb" ( + echo Unable to locate PDB file %%~dpn$P2:i.pdb. Aborting %filename% 1>&2 + exit /B 1 +) + +:VerifyPerfView +where /q perfview || ( + echo Please see the PerfView GitHub Download Page to download an up-to-date version 1>&2 + echo of PerfView and copy it to a directory in your path. 1>&2 + echo. 1>&2 + echo https://github.com/Microsoft/perfview/blob/master/documentation/Downloading.md 1>&2 + exit /B 2 +) + +:: Generate output filenames +if NOT "%_NTUSER%" == "" ( + set perfviewfilename=%_NTUSER%-%~n1-%2 +) ELSE ( + if NOT "%USERNAME%" == "" ( + set perfviewfilename=%USERNAME%-%~n1-%2 + ) ELSE ( + set perfviewfilename=%~n1-%2 + ) +) +set perfviewstartlog=%perfviewfilename%.start.log.txt +set perfviewstoplog=%perfviewfilename%.end.log.txt + +:: Capture the perfview without requiring any human intervention +:CapturePerfView +echo Capture perf view for '%*'... +perfview start /AcceptEULA /NoGui /NoNGenRundown /Merge /Zip /Providers:*Microsoft.Git.Scalar:@StacksEnabled=true,*Microsoft.Internal.Git.Plugin:@StacksEnabled=true,*Microsoft.OSGENG.Testing.GitMsWrapper:@StacksEnabled=true /kernelEvents=default+FileIOInit /logfile:"%perfviewstartlog%" "%perfviewfilename%" || goto :HandlePerfViewStartError +echo. +set STARTTIME=%TIME% +%* +set ENDTIME=%TIME% +echo. +CALL :PrintElapsedTime + +:: Merge perfview into ZIP file +echo Merging and compressing perf view... +perfview stop /AcceptEULA /NoGui /NoNGenRundown /Merge /Zip /Providers:*Microsoft.Git.Scalar:@StacksEnabled=true,*Microsoft.Internal.Git.Plugin:@StacksEnabled=true,*Microsoft.OSGENG.Testing.GitMsWrapper:@StacksEnabled=true /kernelEvents=default+FileIOInit /logfile:"%perfviewstoplog%" || goto :HandlePerfViewStopError +CALL :CheckForFile +echo PerfView trace can be found in "%perfviewfilename%.etl.zip" +goto :end + +:HandlePerfViewStartError +echo Could not start perfview, please see %perfviewstartlog% for details. +EXIT /B 3 + +:HandlePerfViewStopError +echo Could not stop perfview, please see %perfviewstoplog% for details. +EXIT /B 4 + +:: Now wait for perfview to complete writing out the file +:CheckForFile +IF EXIST "%perfviewfilename%.etl.zip" EXIT /B 0 +TIMEOUT /T 1 >nul +goto :CheckForFile + +:PrintElapsedTime +:: Change formatting for the start and end times +for /F "tokens=1-4 delims=:.," %%a in ("%STARTTIME%") do ( + set /A "start=(((%%a*60)+1%%b %% 100)*60+1%%c %% 100)*100+1%%d %% 100" +) + +for /F "tokens=1-4 delims=:.," %%a in ("%ENDTIME%") do ( + set /A "end=(((%%a*60)+1%%b %% 100)*60+1%%c %% 100)*100+1%%d %% 100" +) + +:: Calculate the elapsed time by subtracting values +set /A elapsed=end-start + +:: Format the results for output +set /A hh=elapsed/(60*60*100), rest=elapsed%%(60*60*100), mm=rest/(60*100), rest%%=60*100, ss=rest/100, cc=rest%%100 +if %hh% lss 10 set hh=0%hh% +if %mm% lss 10 set mm=0%mm% +if %ss% lss 10 set ss=0%ss% +if %cc% lss 10 set cc=0%cc% + +set DURATION=%hh%:%mm%:%ss%.%cc% +echo Command duration : %DURATION% +EXIT /B 0 + +:end +endlocal diff --git a/Scripts/InitializeEnvironment.bat b/Scripts/InitializeEnvironment.bat index 3710a2418d..0c318cf825 100644 --- a/Scripts/InitializeEnvironment.bat +++ b/Scripts/InitializeEnvironment.bat @@ -1,26 +1,26 @@ -@ECHO OFF - -REM Set environment variables for interesting paths that scripts might need access to. -PUSHD %~dp0 -SET Scalar_SCRIPTSDIR=%CD% -POPD - -CALL :RESOLVEPATH "%Scalar_SCRIPTSDIR%\.." -SET Scalar_SRCDIR=%_PARSED_PATH_% - -CALL :RESOLVEPATH "%Scalar_SRCDIR%\.." -SET Scalar_ENLISTMENTDIR=%_PARSED_PATH_% - -SET Scalar_OUTPUTDIR=%Scalar_ENLISTMENTDIR%\BuildOutput -SET Scalar_PACKAGESDIR=%Scalar_ENLISTMENTDIR%\packages -SET Scalar_PUBLISHDIR=%Scalar_ENLISTMENTDIR%\Publish -SET Scalar_TOOLSDIR=%Scalar_ENLISTMENTDIR%\.tools - -REM Clean up -SET _PARSED_PATH_= - -GOTO :EOF - -:RESOLVEPATH -SET "_PARSED_PATH_=%~f1" -GOTO :EOF +@ECHO OFF + +REM Set environment variables for interesting paths that scripts might need access to. +PUSHD %~dp0 +SET Scalar_SCRIPTSDIR=%CD% +POPD + +CALL :RESOLVEPATH "%Scalar_SCRIPTSDIR%\.." +SET Scalar_SRCDIR=%_PARSED_PATH_% + +CALL :RESOLVEPATH "%Scalar_SRCDIR%\.." +SET Scalar_ENLISTMENTDIR=%_PARSED_PATH_% + +SET Scalar_OUTPUTDIR=%Scalar_ENLISTMENTDIR%\BuildOutput +SET Scalar_PACKAGESDIR=%Scalar_ENLISTMENTDIR%\packages +SET Scalar_PUBLISHDIR=%Scalar_ENLISTMENTDIR%\Publish +SET Scalar_TOOLSDIR=%Scalar_ENLISTMENTDIR%\.tools + +REM Clean up +SET _PARSED_PATH_= + +GOTO :EOF + +:RESOLVEPATH +SET "_PARSED_PATH_=%~f1" +GOTO :EOF diff --git a/Scripts/NukeBuildOutputs.bat b/Scripts/NukeBuildOutputs.bat index 696e5fe244..f3195ae9d9 100644 --- a/Scripts/NukeBuildOutputs.bat +++ b/Scripts/NukeBuildOutputs.bat @@ -1,42 +1,42 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -taskkill /f /im Scalar.Mount.exe 2>&1 -verify >nul - -powershell -NonInteractive -NoProfile -Command "& { (Get-MpPreference).ExclusionPath | ? {$_.StartsWith('C:\Repos\')} | %%{Remove-MpPreference -ExclusionPath $_} }" - -IF EXIST C:\Repos\ScalarFunctionalTests\enlistment ( - rmdir /s /q C:\Repos\ScalarFunctionalTests\enlistment -) ELSE ( - ECHO no test enlistment found -) - -IF EXIST C:\Repos\ScalarPerfTest ( - rmdir /s /q C:\Repos\ScalarPerfTest -) ELSE ( - ECHO no perf test enlistment found -) - -IF EXIST %Scalar_OUTPUTDIR% ( - ECHO deleting build outputs - rmdir /s /q %Scalar_OUTPUTDIR% -) ELSE ( - ECHO no build outputs found -) - -IF EXIST %Scalar_PUBLISHDIR% ( - ECHO deleting published output - rmdir /s /q %Scalar_PUBLISHDIR% -) ELSE ( - ECHO no packages found -) - -IF EXIST %Scalar_PACKAGESDIR% ( - ECHO deleting packages - rmdir /s /q %Scalar_PACKAGESDIR% -) ELSE ( - ECHO no packages found -) - -call %Scalar_SCRIPTSDIR%\StopAllServices.bat +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +taskkill /f /im Scalar.Mount.exe 2>&1 +verify >nul + +powershell -NonInteractive -NoProfile -Command "& { (Get-MpPreference).ExclusionPath | ? {$_.StartsWith('C:\Repos\')} | %%{Remove-MpPreference -ExclusionPath $_} }" + +IF EXIST C:\Repos\ScalarFunctionalTests\enlistment ( + rmdir /s /q C:\Repos\ScalarFunctionalTests\enlistment +) ELSE ( + ECHO no test enlistment found +) + +IF EXIST C:\Repos\ScalarPerfTest ( + rmdir /s /q C:\Repos\ScalarPerfTest +) ELSE ( + ECHO no perf test enlistment found +) + +IF EXIST %Scalar_OUTPUTDIR% ( + ECHO deleting build outputs + rmdir /s /q %Scalar_OUTPUTDIR% +) ELSE ( + ECHO no build outputs found +) + +IF EXIST %Scalar_PUBLISHDIR% ( + ECHO deleting published output + rmdir /s /q %Scalar_PUBLISHDIR% +) ELSE ( + ECHO no packages found +) + +IF EXIST %Scalar_PACKAGESDIR% ( + ECHO deleting packages + rmdir /s /q %Scalar_PACKAGESDIR% +) ELSE ( + ECHO no packages found +) + +call %Scalar_SCRIPTSDIR%\StopAllServices.bat diff --git a/Scripts/ReinstallScalar.bat b/Scripts/ReinstallScalar.bat index 2ae14cbfca..99e9fb01ae 100644 --- a/Scripts/ReinstallScalar.bat +++ b/Scripts/ReinstallScalar.bat @@ -1,19 +1,19 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") - -call %Scalar_SCRIPTSDIR%\UninstallScalar.bat - -if not exist "c:\Program Files\Git" goto :noGit -for /F "delims=" %%g in ('dir "c:\Program Files\Git\unins*.exe" /B /S /O:-D') do %%g /VERYSILENT /SUPPRESSMSGBOXES /NORESTART & goto :deleteGit - -:deleteGit -rmdir /q/s "c:\Program Files\Git" - -:noGit -REM This is a hacky way to sleep for 2 seconds in a non-interactive window. The timeout command does not work if it can't redirect stdin. -ping 1.1.1.1 -n 1 -w 2000 >NUL - -:runInstallers -call %Scalar_OUTPUTDIR%\Scalar.Build\InstallProduct.bat +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +call %Scalar_SCRIPTSDIR%\UninstallScalar.bat + +if not exist "c:\Program Files\Git" goto :noGit +for /F "delims=" %%g in ('dir "c:\Program Files\Git\unins*.exe" /B /S /O:-D') do %%g /VERYSILENT /SUPPRESSMSGBOXES /NORESTART & goto :deleteGit + +:deleteGit +rmdir /q/s "c:\Program Files\Git" + +:noGit +REM This is a hacky way to sleep for 2 seconds in a non-interactive window. The timeout command does not work if it can't redirect stdin. +ping 1.1.1.1 -n 1 -w 2000 >NUL + +:runInstallers +call %Scalar_OUTPUTDIR%\Scalar.Build\InstallProduct.bat diff --git a/Scripts/RestorePackages.bat b/Scripts/RestorePackages.bat index e47d635eb0..3eaf226246 100644 --- a/Scripts/RestorePackages.bat +++ b/Scripts/RestorePackages.bat @@ -1,20 +1,20 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -SETLOCAL - -IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") - -SET SolutionConfiguration=%Configuration%.Windows - -SET nuget="%Scalar_TOOLSDIR%\nuget.exe" -IF NOT EXIST %nuget% ( - mkdir %nuget%\.. - powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile %nuget%" -) - -%nuget% restore %Scalar_SRCDIR%\Scalar.sln || exit /b 1 - -dotnet restore %Scalar_SRCDIR%\Scalar.sln /p:Configuration=%SolutionConfiguration% /p:VCTargetsPath="C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V140" --packages %Scalar_PACKAGESDIR% || exit /b 1 - +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +SETLOCAL + +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +SET SolutionConfiguration=%Configuration%.Windows + +SET nuget="%Scalar_TOOLSDIR%\nuget.exe" +IF NOT EXIST %nuget% ( + mkdir %nuget%\.. + powershell -ExecutionPolicy Bypass -Command "Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' -OutFile %nuget%" +) + +%nuget% restore %Scalar_SRCDIR%\Scalar.sln || exit /b 1 + +dotnet restore %Scalar_SRCDIR%\Scalar.sln /p:Configuration=%SolutionConfiguration% /p:VCTargetsPath="C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\V140" --packages %Scalar_PACKAGESDIR% || exit /b 1 + ENDLOCAL diff --git a/Scripts/RunFunctionalTests.bat b/Scripts/RunFunctionalTests.bat index 0f2c6db253..3369546fcb 100644 --- a/Scripts/RunFunctionalTests.bat +++ b/Scripts/RunFunctionalTests.bat @@ -1,41 +1,41 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") - -SETLOCAL -SET PATH=C:\Program Files\Scalar;C:\Program Files\Git\cmd;%PATH% - -if not "%2"=="--test-scalar-on-path" goto :startFunctionalTests - -REM Force Scalar.FunctionalTests.exe to use the installed version of Scalar -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.ReadObjectHook.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Mount.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Service.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Service.UI.exe - -REM Same for Scalar.FunctionalTests.Windows.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.ReadObjectHook.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Mount.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Service.exe -del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Service.UI.exe - -echo PATH = %PATH% -echo scalar location: -where scalar -echo Scalar.Service location: -where Scalar.Service -echo git location: -where git - -:startFunctionalTests -dotnet %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.FunctionalTests.dll /result:TestResultNetCore.xml %2 %3 %4 %5 || goto :endFunctionalTests - -:endFunctionalTests -set error=%errorlevel% - -call %Scalar_SCRIPTSDIR%\StopAllServices.bat - -exit /b %error% +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +SETLOCAL +SET PATH=C:\Program Files\Scalar;C:\Program Files\Git\cmd;%PATH% + +if not "%2"=="--test-scalar-on-path" goto :startFunctionalTests + +REM Force Scalar.FunctionalTests.exe to use the installed version of Scalar +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.ReadObjectHook.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Mount.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Service.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.Service.UI.exe + +REM Same for Scalar.FunctionalTests.Windows.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.ReadObjectHook.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Mount.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Service.exe +del %Scalar_OUTPUTDIR%\Scalar.FunctionalTests.Windows\bin\x64\%Configuration%\Scalar.Service.UI.exe + +echo PATH = %PATH% +echo scalar location: +where scalar +echo Scalar.Service location: +where Scalar.Service +echo git location: +where git + +:startFunctionalTests +dotnet %Scalar_OUTPUTDIR%\Scalar.FunctionalTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.FunctionalTests.dll /result:TestResultNetCore.xml %2 %3 %4 %5 || goto :endFunctionalTests + +:endFunctionalTests +set error=%errorlevel% + +call %Scalar_SCRIPTSDIR%\StopAllServices.bat + +exit /b %error% diff --git a/Scripts/RunUnitTests.bat b/Scripts/RunUnitTests.bat index e5bdff576f..20edb50fce 100644 --- a/Scripts/RunUnitTests.bat +++ b/Scripts/RunUnitTests.bat @@ -1,11 +1,11 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") - -set RESULT=0 - -%Scalar_OUTPUTDIR%\Scalar.UnitTests.Windows\bin\x64\%Configuration%\Scalar.UnitTests.Windows.exe || set RESULT=1 -dotnet %Scalar_OUTPUTDIR%\Scalar.UnitTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.UnitTests.dll || set RESULT=1 - +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +IF "%1"=="" (SET "Configuration=Debug") ELSE (SET "Configuration=%1") + +set RESULT=0 + +%Scalar_OUTPUTDIR%\Scalar.UnitTests.Windows\bin\x64\%Configuration%\Scalar.UnitTests.Windows.exe || set RESULT=1 +dotnet %Scalar_OUTPUTDIR%\Scalar.UnitTests\bin\x64\%Configuration%\netcoreapp2.1\Scalar.UnitTests.dll || set RESULT=1 + exit /b %RESULT% diff --git a/Scripts/StopAllServices.bat b/Scripts/StopAllServices.bat index e724b1d0d6..baa60c8b6e 100644 --- a/Scripts/StopAllServices.bat +++ b/Scripts/StopAllServices.bat @@ -1,5 +1,5 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -call %Scalar_SCRIPTSDIR%\StopService.bat Scalar.Service -call %Scalar_SCRIPTSDIR%\StopService.bat Test.Scalar.Service +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +call %Scalar_SCRIPTSDIR%\StopService.bat Scalar.Service +call %Scalar_SCRIPTSDIR%\StopService.bat Test.Scalar.Service diff --git a/Scripts/StopService.bat b/Scripts/StopService.bat index e380f0e250..c1c5bdd09b 100644 --- a/Scripts/StopService.bat +++ b/Scripts/StopService.bat @@ -1,2 +1,2 @@ -sc stop %1 -verify >nul +sc stop %1 +verify >nul diff --git a/Scripts/UninstallScalar.bat b/Scripts/UninstallScalar.bat index 82461f8877..e09c321d30 100644 --- a/Scripts/UninstallScalar.bat +++ b/Scripts/UninstallScalar.bat @@ -1,21 +1,21 @@ -@ECHO OFF -CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 - -taskkill /F /T /FI "IMAGENAME eq git.exe" -taskkill /F /T /FI "IMAGENAME eq Scalar.exe" -taskkill /F /T /FI "IMAGENAME eq Scalar.Mount.exe" - -if not exist "c:\Program Files\Scalar" goto :end - -call %Scalar_SCRIPTSDIR%\StopAllServices.bat - -REM Find the latest uninstaller file by date and run it. Goto the next step after a single execution. -for /F "delims=" %%f in ('dir "c:\Program Files\Scalar\unins*.exe" /B /S /O:-D') do %%f /VERYSILENT /SUPPRESSMSGBOXES /NORESTART & goto :deleteScalar - -:deleteScalar -rmdir /q/s "c:\Program Files\Scalar" - -REM Delete ProgramData\Scalar directory (logs, downloaded upgrades, repo-registry, scalar.config). It can affect the behavior of a future Scalar install. -if exist "C:\ProgramData\Scalar" rmdir /q/s "C:\ProgramData\Scalar" - -:end +@ECHO OFF +CALL %~dp0\InitializeEnvironment.bat || EXIT /b 10 + +taskkill /F /T /FI "IMAGENAME eq git.exe" +taskkill /F /T /FI "IMAGENAME eq Scalar.exe" +taskkill /F /T /FI "IMAGENAME eq Scalar.Mount.exe" + +if not exist "c:\Program Files\Scalar" goto :end + +call %Scalar_SCRIPTSDIR%\StopAllServices.bat + +REM Find the latest uninstaller file by date and run it. Goto the next step after a single execution. +for /F "delims=" %%f in ('dir "c:\Program Files\Scalar\unins*.exe" /B /S /O:-D') do %%f /VERYSILENT /SUPPRESSMSGBOXES /NORESTART & goto :deleteScalar + +:deleteScalar +rmdir /q/s "c:\Program Files\Scalar" + +REM Delete ProgramData\Scalar directory (logs, downloaded upgrades, repo-registry, scalar.config). It can affect the behavior of a future Scalar install. +if exist "C:\ProgramData\Scalar" rmdir /q/s "C:\ProgramData\Scalar" + +:end diff --git a/nuget.config b/nuget.config index 91b9aaac5d..6997505944 100644 --- a/nuget.config +++ b/nuget.config @@ -1,17 +1,17 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + +