Repository: microsoft/terminal (language: cpp)

Issue:
Terminal locks its starting directory; perhaps it shouldn't do that?
# Description of the new feature/enhancement

The directory that Windows Terminal starts up in can't be deleted (in Windows Explorer / Directory Opus / etc).  It would be nice if I could delete the folder without exiting wt first.

Use case:

When you configure a profile to include this:
`"startingDirectory" : ".",`
and then set up the right-click "Start Windows Terminal Here" functionality in Windows Explorer to start a wt instance in any folder you get to create Terminal instances quickly and conveniently.

But, if you get done with that folder and want to delete it then you need to close wt first.

It would be convenient to delete the folder without closing wt first.

What's weird is that this behavior (keeping the starting dir locked so you can't delete it) continues to happen even if you navigate away from that dir in the command line.
(Which makes sense - wt creates new tabs in that same dir so preventing it from disappearing is understandable)

# Proposed technical implementation details (optional)

* Do NOT lock the directory, allowing the user to delete it.
* Start new tabs in the default starting directory instead

# maintainer edit:

Also using to track the thing where `wt` needs to tell the existing window about the current CWD, for running actions in an existing window (which may have a different CWD), see #13430



Discussion / hints:
@MikePanitz-CCC If you tried doing the same thing in the legacy console (opening in a specific directory, navigating the shell away from that directory, then removing that directory), does the same thing happen?

I'd think that this would be the same behavior for all applications, where you can't remove the directory when a running process has that directory as it's current working directory, but I'm not 100% sure on that
Locking the dir that a shell is currently using be is reasonable.  Keeping the wt starting dir locked as long as wt is running is awkward

Xonsh in particular allows you to keep the dir  unlocked.

Get Outlook for Android<https://aka.ms/ghei36>

I don't disagree, but I do somewhat wonder how this'll interact with _future_ uses of profiles with `"startingDirectory": "."`. Like: WT can move out of the directory it's in (since I think we're only locking it by virtue of it being our process's CWD), but then any new tab spawned with a profile set up like that will spawn in the wrong directory. We would need to pre-resolve all startingDirectories against the WD at the time of _first settings load_.

Then, at least those new tabs would error out ("can't find directory") instead of pointing at the wrong directory ...
That sounds completely reasonable to me.
FWIW:
cmd does not lock the dir when it leaves (I created a new dir, started a new `CMD `in that dir, had the new cmd do a "`cd ..`", and then deleted the new dir no problem)

powershell does lock the directory  (I created a new dir is PS, `cd`'d to that new directory started powershell.exe in that new dir, had the original PS do a "`cd ..`", and then had the NEW powershell instance do a "`cd ..`", and was NOT able to delete the new dir.)
(I had the new ps.exe do an exit and was able to delete the new dir no problem which makes me very confident that the ps.exe was holding that dir open)

bash (from Git Bash - MinGW64) does not lock the current directory.  At least, I was able to start a new bash process in the background ( ` bash &` ) in a new dir, then switch back to my original bash process, have it `cd `out of the new dir, and then delete the new dir no problem.

What was really weird is that the child bash kept it's current working directory as being the folder I'd just deleted.  When I asked it to do an 'ls' it reasonably said `ls: cannot open directory '.': No such file or directory`
I'm throwing this one on the backlog as a "Help Wanted" bug; We need to figure out the right thing to do here and how that plays with the user's profile settings and what an appropriate fallback looks like.
I may try to look at this myself.  Obviously anyone else who's interested feel free to jump in.

Especially since I'm pretty busy until, like, August :)

Thanks for looking at this @DHowett-MSFT!
You know what, there's actually another issue on the repo that's effectively the same thing: #10849. Both threads have great discussion. 
> When you configure a profile to include this:
> "startingDirectory" : ".",
> and then set up the right-click "Start Windows Terminal Here" functionality in Windows Explorer to start a wt instance in any > folder you get to create Terminal instances quickly and conveniently.
>
> But, if you get done with that folder and want to delete it then you need to close wt first.

I have not set `startingDirectory` in my `settings.json` file, and still encounter this bug. 

> powershell does lock the directory (I created a new dir is PS, `cd`'d to that new directory started powershell.exe in that new dir, had the original PS do a "`cd ..`", and then had the NEW powershell instance do a "`cd ..`", and was NOT able to delete the new dir.) (I had the new ps.exe do an exit and was able to delete the new dir no problem which makes me very confident that the ps.exe was holding that dir open)

I cannot reproduce this behaviour. When I create a folder `C:\temp\x`, right-click that folder and select "Open PowerShell window here" and then change the directory to the parent, I can immediately delete that folder.

![image](https://user-images.githubusercontent.com/2971735/172368775-824dec9d-0368-4121-adf9-66e2158439f7.png)

I'm on Windows 10, 21H2. But I know that this was always working like that for previous Windows version as well.
> I don't disagree, but I do somewhat wonder how this'll interact with _future_ uses of profiles with `"startingDirectory": "."`. Like: WT can move out of the directory it's in (since I think we're only locking it by virtue of it being our process's CWD), but then any new tab spawned with a profile set up like that will spawn in the wrong directory. We would need to pre-resolve all startingDirectories against the WD at the time of _first settings load_.
> 
> Then, at least those new tabs would error out ("can't find directory") instead of pointing at the wrong directory ...

The bug also happens, when you don't have `startingDirectory` in your settings.json. So that train of thought, while helpful in that other case, doesn't help when `startingDirectory`is unset.

If you ask me, the behaviour could be as simple as: If the defined (or undefined for that matter) starting directory does not exist, fallback to `$env:USERPROFILE`. Then there is no need to lock that directory.
> I cannot reproduce this behaviour.

I can with both Powershell 5 and 7.
Created folder manually in explorer, started powershell and tried to remove folder. Not with both powershells at the same time of course.
<details>

![2022-06-08_18-39-31](https://user-images.githubusercontent.com/8836461/172659936-23ca15a4-267f-4b4e-aca4-0a569dc71621.png)
![2022-06-08_18-40-17](https://user-images.githubusercontent.com/8836461/172659945-f056da95-183e-4897-bf58-b297aa39baa0.png)

</details>

@SpL1Ne How do you open PS? Same way as I did?
@danielniccoli I don't have "open powershell here" on right-click so I just typed `powershell.exe` and `pwsh` into explorer address bar.
Edit: this video of me repeating steps from answer above which you quoted:
<details>

https://user-images.githubusercontent.com/8836461/172722788-6e54ebd9-5c20-4c6a-8d32-a5c873757ca4.mp4

</details>
Then there are differences here.

1. Open powershell.exe or pwsh.exe via context menu does **not** lock the directory.
2. Open wt.exe via context menu does lock the directory.
3. Open powershell.exe or pwsh.exe via address bar does lock the directory.

Option 1 is clearly supreme 😆 
> Open powershell.exe or pwsh.exe via context menu does not lock the directory.

As I saw from google, "open powershell here" from context menu doesn't really opens powershell in that folder. It opens powershell and than uses `set-location` to the folder.
So I believe powershell just locks start-up directory, instead of the folder where you opened poweshell from context menu.

Fix patch:
diff --git a/src/cascadia/Remoting/Peasant.idl b/src/cascadia/Remoting/Peasant.idl
index 89e2b52db2c..64ff512a1b4 100644
--- a/src/cascadia/Remoting/Peasant.idl
+++ b/src/cascadia/Remoting/Peasant.idl
@@ -10,7 +10,7 @@ namespace Microsoft.Terminal.Remoting
         CommandlineArgs(String[] args, String cwd, UInt32 showWindowCommand);
 
         String[] Commandline { get; set; };
-        String CurrentDirectory();
+        String CurrentDirectory { get; };
         UInt32 ShowWindowCommand { get; };
     };
 
diff --git a/src/cascadia/TerminalApp/TerminalPage.cpp b/src/cascadia/TerminalApp/TerminalPage.cpp
index 405640860b1..e7a31d6c648 100644
--- a/src/cascadia/TerminalApp/TerminalPage.cpp
+++ b/src/cascadia/TerminalApp/TerminalPage.cpp
@@ -551,27 +551,28 @@ namespace winrt::TerminalApp::implementation
         // Handle it on a subsequent pass of the UI thread.
         co_await wil::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal);
 
-        // If the caller provided a CWD, switch to that directory, then switch
+        // If the caller provided a CWD, "switch" to that directory, then switch
         // back once we're done. This looks weird though, because we have to set
         // up the scope_exit _first_. We'll release the scope_exit if we don't
         // actually need it.
-        auto originalCwd{ wil::GetCurrentDirectoryW<std::wstring>() };
-        auto restoreCwd = wil::scope_exit([&originalCwd]() {
+
+        auto originalVirtualCwd{ _WindowProperties.VirtualWorkingDirectory() };
+        auto restoreCwd = wil::scope_exit([&originalVirtualCwd, this]() {
             // ignore errors, we'll just power on through. We'd rather do
             // something rather than fail silently if the directory doesn't
             // actually exist.
-            LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectory(originalCwd.c_str()));
+            _WindowProperties.VirtualWorkingDirectory(originalVirtualCwd);
         });
+
         if (cwd.empty())
         {
+            // We didn't actually need to change the virtual CWD, so we don't
+            // need to restore it
             restoreCwd.release();
         }
         else
         {
-            // ignore errors, we'll just power on through. We'd rather do
-            // something rather than fail silently if the directory doesn't
-            // actually exist.
-            LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectory(cwd.c_str()));
+            _WindowProperties.VirtualWorkingDirectory(cwd);
         }
 
         if (auto page{ weakThis.get() })
@@ -1226,15 +1227,12 @@ namespace winrt::TerminalApp::implementation
             // construction, because the connection might not spawn the child
             // process until later, on another thread, after we've already
             // restored the CWD to its original value.
-            auto newWorkingDirectory{ settings.StartingDirectory() };
-            if (newWorkingDirectory.size() == 0 || newWorkingDirectory.size() == 1 &&
-                                                       !(newWorkingDirectory[0] == L'~' || newWorkingDirectory[0] == L'/'))
-            { // We only want to resolve the new WD against the CWD if it doesn't look like a Linux path (see GH#592)
-                auto cwdString{ wil::GetCurrentDirectoryW<std::wstring>() };
-                std::filesystem::path cwd{ cwdString };
-                cwd /= settings.StartingDirectory().c_str();
-                newWorkingDirectory = winrt::hstring{ cwd.wstring() };
-            }
+            const auto currentVirtualDir{ _WindowProperties.VirtualWorkingDirectory() };
+            const auto cwdString{ std::wstring_view{ currentVirtualDir } };
+            const auto requestedStartingDir{ settings.StartingDirectory() };
+            auto newWorkingDirectory = winrt::hstring{
+                Utils::EvaluateStartingDirectory(cwdString, std::wstring_view{ requestedStartingDir })
+            };
 
             auto conhostConn = TerminalConnection::ConptyConnection();
             auto valueSet = TerminalConnection::ConptyConnection::CreateSettings(settings.Commandline(),
diff --git a/src/cascadia/TerminalApp/TerminalPage.idl b/src/cascadia/TerminalApp/TerminalPage.idl
index 92052ed1677..4375c000738 100644
--- a/src/cascadia/TerminalApp/TerminalPage.idl
+++ b/src/cascadia/TerminalApp/TerminalPage.idl
@@ -51,6 +51,8 @@ namespace TerminalApp
         String WindowNameForDisplay { get; };
         String WindowIdForDisplay { get; };
 
+        String VirtualWorkingDirectory { get; set; };
+
         Boolean IsQuakeWindow();
     };
 
diff --git a/src/cascadia/TerminalApp/TerminalWindow.cpp b/src/cascadia/TerminalApp/TerminalWindow.cpp
index 6cb20121784..c95cf789315 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.cpp
+++ b/src/cascadia/TerminalApp/TerminalWindow.cpp
@@ -1019,11 +1019,14 @@ namespace winrt::TerminalApp::implementation
     //   returned.
     // Arguments:
     // - args: an array of strings to process as a commandline. These args can contain spaces
+    // - cwd: The CWD that this window should treat as its own "virtual" CWD
     // Return Value:
     // - the result of the first command who's parsing returned a non-zero code,
     //   or 0. (see TerminalWindow::_ParseArgs)
-    int32_t TerminalWindow::SetStartupCommandline(array_view<const winrt::hstring> args)
+    int32_t TerminalWindow::SetStartupCommandline(array_view<const winrt::hstring> args, winrt::hstring cwd)
     {
+        _WindowProperties->SetInitialCwd(std::move(cwd));
+
         // This is called in AppHost::ctor(), before we've created the window
         // (or called TerminalWindow::Initialize)
         const auto result = _appArgs.ParseArgs(args);
@@ -1345,6 +1348,7 @@ namespace winrt::TerminalApp::implementation
             CATCH_LOG();
         }
     }
+
     uint64_t WindowProperties::WindowId() const noexcept
     {
         return _WindowId;
diff --git a/src/cascadia/TerminalApp/TerminalWindow.h b/src/cascadia/TerminalApp/TerminalWindow.h
index 2d288ccfddf..a59c139a95c 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.h
+++ b/src/cascadia/TerminalApp/TerminalWindow.h
@@ -48,8 +48,14 @@ namespace winrt::TerminalApp::implementation
         winrt::hstring WindowNameForDisplay() const noexcept;
         bool IsQuakeWindow() const noexcept;
 
+        WINRT_OBSERVABLE_PROPERTY(winrt::hstring, VirtualWorkingDirectory, _PropertyChangedHandlers, L"");
+
         WINRT_CALLBACK(PropertyChanged, Windows::UI::Xaml::Data::PropertyChangedEventHandler);
 
+    public:
+        // Used for setting the initial CWD, before we have XAML set up for property change notifications.
+        void SetInitialCwd(winrt::hstring cwd) { _VirtualWorkingDirectory = std::move(cwd); };
+
     private:
         winrt::hstring _WindowName{};
         uint64_t _WindowId{ 0 };
@@ -71,7 +77,7 @@ namespace winrt::TerminalApp::implementation
 
         bool HasCommandlineArguments() const noexcept;
 
-        int32_t SetStartupCommandline(array_view<const winrt::hstring> actions);
+        int32_t SetStartupCommandline(array_view<const winrt::hstring> actions, winrt::hstring cwd);
         void SetStartupContent(const winrt::hstring& content, const Windows::Foundation::IReference<Windows::Foundation::Rect>& contentBounds);
         int32_t ExecuteCommandline(array_view<const winrt::hstring> actions, const winrt::hstring& cwd);
         void SetSettingsStartupArgs(const std::vector<winrt::Microsoft::Terminal::Settings::Model::ActionAndArgs>& actions);
diff --git a/src/cascadia/TerminalApp/TerminalWindow.idl b/src/cascadia/TerminalApp/TerminalWindow.idl
index 912663e4a0b..d91de2dce88 100644
--- a/src/cascadia/TerminalApp/TerminalWindow.idl
+++ b/src/cascadia/TerminalApp/TerminalWindow.idl
@@ -53,7 +53,7 @@ namespace TerminalApp
 
         Boolean HasCommandlineArguments();
 
-        Int32 SetStartupCommandline(String[] commands);
+        Int32 SetStartupCommandline(String[] commands, String cwd);
         void SetStartupContent(String json, Windows.Foundation.IReference<Windows.Foundation.Rect> bounds);
         Int32 ExecuteCommandline(String[] commands, String cwd);
         String ParseCommandlineMessage { get; };
diff --git a/src/cascadia/WindowsTerminal/AppHost.cpp b/src/cascadia/WindowsTerminal/AppHost.cpp
index 16be33a3db3..07f4c3e114d 100644
--- a/src/cascadia/WindowsTerminal/AppHost.cpp
+++ b/src/cascadia/WindowsTerminal/AppHost.cpp
@@ -177,7 +177,7 @@ void AppHost::_HandleCommandlineArgs(const Remoting::WindowRequestedArgs& window
         }
         else if (args)
         {
-            const auto result = _windowLogic.SetStartupCommandline(args.Commandline());
+            const auto result = _windowLogic.SetStartupCommandline(args.Commandline(), args.CurrentDirectory());
             const auto message = _windowLogic.ParseCommandlineMessage();
             if (!message.empty())
             {
diff --git a/src/cascadia/WindowsTerminal/WindowEmperor.cpp b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
index e847547c01e..882f8d097ae 100644
--- a/src/cascadia/WindowsTerminal/WindowEmperor.cpp
+++ b/src/cascadia/WindowsTerminal/WindowEmperor.cpp
@@ -71,7 +71,17 @@ bool WindowEmperor::HandleCommandlineArgs()
 {
     std::vector<winrt::hstring> args;
     _buildArgsFromCommandline(args);
-    auto cwd{ wil::GetCurrentDirectoryW<std::wstring>() };
+    const auto cwd{ wil::GetCurrentDirectoryW<std::wstring>() };
+
+    {
+        // ALWAYS change the _real_ CWD of the Terminal to system32, so that we
+        // don't lock the directory we were spawned in.
+        std::wstring system32{};
+        if (SUCCEEDED_LOG(wil::GetSystemDirectoryW<std::wstring>(system32)))
+        {
+            LOG_IF_WIN32_BOOL_FALSE(SetCurrentDirectoryW(system32.c_str()));
+        }
+    }
 
     // Get the requested initial state of the window from our startup info. For
     // something like `start /min`, this will set the wShowWindow member to
diff --git a/src/types/inc/utils.hpp b/src/types/inc/utils.hpp
index 45e16a049de..b8ce375f1ea 100644
--- a/src/types/inc/utils.hpp
+++ b/src/types/inc/utils.hpp
@@ -112,4 +112,7 @@ namespace Microsoft::Console::Utils
     // testing easier.
     std::wstring_view TrimPaste(std::wstring_view textView) noexcept;
 
+    // Same deal, but in TerminalPage::_evaluatePathForCwd
+    std::wstring EvaluateStartingDirectory(std::wstring_view cwd, std::wstring_view startingDirectory);
+
 }
diff --git a/src/types/ut_types/UtilsTests.cpp b/src/types/ut_types/UtilsTests.cpp
index f6246df87df..f83b1b4cad7 100644
--- a/src/types/ut_types/UtilsTests.cpp
+++ b/src/types/ut_types/UtilsTests.cpp
@@ -33,6 +33,8 @@ class UtilsTests
     TEST_METHOD(TestTrimTrailingWhitespace);
     TEST_METHOD(TestDontTrimTrailingWhitespace);
 
+    TEST_METHOD(TestEvaluateStartingDirectory);
+
     void _VerifyXTermColorResult(const std::wstring_view wstr, DWORD colorValue);
     void _VerifyXTermColorInvalid(const std::wstring_view wstr);
 };
@@ -546,3 +548,65 @@ void UtilsTests::TestDontTrimTrailingWhitespace()
     // * trim when there's a tab followed by only whitespace
     // * not trim then there's a tab in the middle, and the string ends in whitespace
 }
+
+void UtilsTests::TestEvaluateStartingDirectory()
+{
+    // Continue on failures
+    const WEX::TestExecution::DisableVerifyExceptions disableExceptionsScope;
+
+    auto test = [](auto& expected, auto& cwd, auto& startingDir) {
+        VERIFY_ARE_EQUAL(expected, EvaluateStartingDirectory(cwd, startingDir));
+    };
+
+    // A NOTE: EvaluateStartingDirectory makes no attempt to cannonicalize the
+    // path. So if you do any sort of relative paths, it'll literally just
+    // append.
+
+    {
+        std::wstring cwd = L"C:\\Windows\\System32";
+
+        // Literally blank
+        test(L"C:\\Windows\\System32\\", cwd, L"");
+
+        // Absolute Windows path
+        test(L"C:\\Windows", cwd, L"C:\\Windows");
+        test(L"C:/Users/migrie", cwd, L"C:/Users/migrie");
+
+        // Relative Windows path
+        test(L"C:\\Windows\\System32\\.", cwd, L"."); // ?
+        test(L"C:\\Windows\\System32\\.\\System32", cwd, L".\\System32"); // ?
+        test(L"C:\\Windows\\System32\\./dev", cwd, L"./dev");
+
+        // WSL '~' path
+        test(L"~", cwd, L"~");
+        test(L"~/dev", cwd, L"~/dev");
+
+        // WSL or Windows / path - this will ultimately be evaluated by the connection
+        test(L"/", cwd, L"/");
+        test(L"/dev", cwd, L"/dev");
+    }
+
+    {
+        std::wstring cwd = L"C:/Users/migrie";
+
+        // Literally blank
+        test(L"C:/Users/migrie\\", cwd, L"");
+
+        // Absolute Windows path
+        test(L"C:\\Windows", cwd, L"C:\\Windows");
+        test(L"C:/Users/migrie", cwd, L"C:/Users/migrie");
+
+        // Relative Windows path
+        test(L"C:/Users/migrie\\.", cwd, L"."); // ?
+        test(L"C:/Users/migrie\\.\\System32", cwd, L".\\System32"); // ?
+        test(L"C:/Users/migrie\\./dev", cwd, L"./dev");
+
+        // WSL '~' path
+        test(L"~", cwd, L"~");
+        test(L"~/dev", cwd, L"~/dev");
+
+        // WSL or Windows / path - this will ultimately be evaluated by the connection
+        test(L"/", cwd, L"/");
+        test(L"/dev", cwd, L"/dev");
+    }
+}
diff --git a/src/types/utils.cpp b/src/types/utils.cpp
index 94813bfc967..2386998e793 100644
--- a/src/types/utils.cpp
+++ b/src/types/utils.cpp
@@ -828,3 +828,27 @@ std::wstring_view Utils::TrimPaste(std::wstring_view textView) noexcept
 
     return textView.substr(0, lastNonSpace + 1);
 }
+
+std::wstring Utils::EvaluateStartingDirectory(
+    std::wstring_view currentDirectory,
+    std::wstring_view startingDirectory)
+{
+    std::wstring resultPath{ startingDirectory };
+
+    // We only want to resolve the new WD against the CWD if it doesn't look
+    // like a Linux path (see GH#592)
+
+    // Append only if it DOESN'T look like a linux-y path. A linux-y path starts
+    // with `~` or `/`.
+    const bool looksLikeLinux =
+        resultPath.size() >= 1 &&
+        (til::at(resultPath, 0) == L'~' || til::at(resultPath, 0) == L'/');
+
+    if (!looksLikeLinux)
+    {
+        std::filesystem::path cwd{ currentDirectory };
+        cwd /= startingDirectory;
+        resultPath = cwd.wstring();
+    }
+    return resultPath;
+}
diff --git a/tools/bcz.cmd b/tools/bcz.cmd
index 46640a725f4..72071fa7cca 100644
--- a/tools/bcz.cmd
+++ b/tools/bcz.cmd
@@ -32,6 +32,10 @@ if (%1) == (rel) (
     echo Manually building release
     set _LAST_BUILD_CONF=Release
 )
+if (%1) == (audit) (
+    echo Manually building audit mode
+    set _LAST_BUILD_CONF=AuditMode
+)
 if (%1) == (no_clean) (
     set _MSBUILD_TARGET=Build
 )


Test patch: