Coffeehouse Thread

15 posts

Forum Read Only

This forum has been made read only by the site admins. No new threads or comments can be added.

Invent a better solution for a deceptively simple problem (AKA how I change de folder)

Back to Forum: Coffeehouse
  • User profile image
    androidi

    I'd prefer a forward compatible/supported way but if no such way exist besides the one I suggest below, I'm willing to take your unsupported clever hacks into consideration.

    Problem:

    Imagine (or create) a .bat batch file which contains a command "@cd folder" and nothing else.

    When you run said bat file it changes the current command prompt process' directory to .\folder\ without echoing the change.

    Challenge:

    Write a C# (or other compiled language) program that performs same task, without launching a completely new cmd prompt (uses existing) Windows Vista/7/2008 64 bit cmd prompt. I can tell you the obvious ways don't work since when the compiled process exits, the "current directory" you set was only for your process, not for the command prompt.

    eg.

    C:\>md folder

    C:\>myCwdToFolder.exe

    (does something to change the existing cmd's current folder to .\folder so it's preserved, doesn't print anything - the only sign of success must be the changed folder)

    C:\folder\>

    If user now does exit  - it terminates the cmd window as no new cmd was launched inside the CwdToFolder process.

    ----------------------------

    My solution -- I'm not happy with and I'm not going to implement it since it's so stupid and suggests that .bat files are more powerful than C#. I won't take that for an answer!

    -- Is to encode the program somehow into a .bat file so the bat file runs (with echo off) the encoded program then takes the ".\folder" as result from the C# program and does the bog standard "cd folder".

     

    PS. I already looked at the ftp://ftp.drdobbs.com/sourcecode/wd/1998/oct98.zip which contains "CHGDIR.C" but after that program exits it leaves visible "cd targetfolder". This isn't accepted solution. See above example: there can be no stuff printed on the command prompt when the program executes.

     PS2. After reading the "rules" again I don't think "my solution" above even fits the rules, since the batch file is performing the folder change and it's not compiled. I tried one bat-to-exe compiler for fun but the resulting exe didn't do anything. Color me surprised.

  • User profile image
    felix9

    the power of BAT is it is executed by the cmd process itself, but any exe is another process.

  • User profile image
    androidi

    I haven't yet tested this but this was on cmd /?:

    HKEY_LOCAL_MACHINE\Software\Microsoft\Command Processor\AutoRun

    Based on some comments I read on ONT where Mr.Chen says he doesn't know any legit use for it, this just might do what I need. However it's HKLM. I'll see if it can be got to work without elevation, otherwise it's useless.

    @felix9:

    Right. But what was the power of the 16 bit app that performed this "CD" trick I'm trying to reproduce on 64 bit prompt now. 16 > 64 == true apparently.

  • User profile image
    Sven Groot

    There is no way to do this. Unlike in DOS, where INT21h could change the current directory of the command processor, in Windows a process cannot change the current directory of its parent.

    You can change the current directory in a batch file because, as felix9 said, they're executing by cmd.exe, not by a child process.

    Your C# application is a child process, therefore it cannot change the current directory of the cmd.exe that invoked it. Launching the process from a batch file, getting the folder from the executable, and then changing it, is the best way.

    The list.com replacement that I hacked together in that thread that you posted about that also uses that approach.

    This problem is not unique to Windows. In bash (and every other Linux shell as far as I'm aware), a child process can't affect the current directory either. What's more, a shell script can't affect it either unless it's invoke through the source command. So to do what you propose in Linux, not only must your program be invoked through a script, the script must be invoked with source. The solution there is to create a global function (or an alias) that invokes the script using source.

  • User profile image
    kettch

    Does the solution have to use cmd? I haven't tried it, but you might have more control with a PowerShell script and custom cmdlets.

  • User profile image
    DCMonkey

    Inject code into the parent process (assuming cmd.exe actually launched your program) that gets it to call SetCurrentDirectory() on itself (and that's assuming cmd.exe even uses that current directory internally to track the shell's current directory), or write you own command line shell that provides an API for programs to do this.

  • User profile image
    Sven Groot

    @kettch: Powershell has a similar problem. Although a cmdlet can modify the current directory, a child process would not, so unless you want to do everything in a cmdlet, you must either invoke your application through a script or a cmdlet, same as with cmd.

    In fact, the problem is worse, because Environment.CurrentDirectory doesn't reflect the current PowerShell path; after all, the current path in PowerShell doesn't need to be on the file system (in can be from any provider).

  • User profile image
    androidi

    @Sven Groot: Ah funny, that thread dropped so quick out of the first page and I didn't visit in a while so I completely managed to miss your post. I've been hacking my own replacement now, most of the time being spent in hacking the console palette back to VGA colors and figuring how to bring some net 4.0 features back into net 2.0. This thread was about adding that "X" feature. I've been trying to go for 1:1 as much as possible so that additional batch file to solve the X function was not satisfactory, but the encode idea might do the trick if the Auto Run key doesn't work without elevation.

     

  • User profile image
    evildictait​or

    You're looking at it all wrong. Why do you want to change the current working directory of your parent folder when all CMD.exe prompts are the same.

    The simple solution is a C# program which creates a CMD.exe with it's current working directory appropriately set, and just pipes stdin and stdout back to the C# program's stdin and stdout.

    That stdin and stdout are mapped to the parent CMD's stdin and stdout, so you'll have the same console window, a new working directory and the prompt will look and behave as it did before.

  • User profile image
    androidi

    @evildictaitor:

    The usage of the program is essentially this:

    l (browse to another path)

    x (exit with the new path -> create new cmd in your solution)

    (do stuff)

    ... (repeat above pattern potentially for very long time without closing the prompt)

    Doesn't this create potentially infinite amount of cmd.exe's as seen from task manager? What would be your preferred way to handle this? If sticking with your piping idea this doesn't seem "simple" at all but if you believe it still is then I'd love a proof of concept...

    BTW. I have to say I'm clueless what the piping is to accomplish in context of this program, so maybe the most practical solution is simply to do as you say in creating a new cmd but then somehow exit it* when L is run again... ?  I have doubts whether that will work though!

    ( * so in the new cmd.exe opened from l.exe, the next run of l.exe would instead exit the cmd and somehow pass the current directory to the existing l.exe process)

  • User profile image
    figuerres

    I just do not get it.... WHY?

    why is CD \ CHDIR in a command prompt not good enough ?

    it does what you want it to do for any normal use i can picture. the only thing it does not do is spawn a process....  what is the need to have an exe to try and do this ?

     

    is this just to see if you can do it and not for any real use ?

  • User profile image
    Blue Ink

    Technically there is a way: DLL injection. Minus the DLL injection part... as it turns out, kernel32 is always loaded in every process at the same address and SetCurrentDirectory is compatible with LPTHREAD_START_ROUTINE, so you can call CreateRemoteThread without having to write a DLL.

    Here's a rough proof of concept (as in: written in a rush, tested for two minutes on exactly one machine)

    #include "stdafx.h"
    #include <Windows.h>
    #include <TlHelp32.h>
    
    DWORD GetParentProcessId () {
      DWORD processId = GetCurrentProcessId ();
      HANDLE snapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, processId);
    
      if (snapshot == INVALID_HANDLE_VALUE) {
        _tprintf (_T("Cannot create shapshot.\n"));
        ExitProcess (0);
      }
    
      PROCESSENTRY32 processEntry;
      processEntry.dwSize = sizeof (PROCESSENTRY32);
      if (Process32First (snapshot, &processEntry)) {
        do {
          if (processEntry.th32ProcessID == processId) {
            return processEntry.th32ParentProcessID;
          }
        }  while (Process32Next (snapshot, &processEntry));
      }
    
      ExitProcess (0);
    }
    
    BOOL EnableDebugPrivileges () {
      HANDLE token;
      LUID sedebugnameValue;
      TOKEN_PRIVILEGES privileges;
    
      if (OpenProcessToken (GetCurrentProcess (), TOKEN_ADJUST_PRIVILEGES| TOKEN_QUERY, &token)) {
        if (LookupPrivilegeValue (NULL, SE_DEBUG_NAME, &sedebugnameValue)) {
          privileges.PrivilegeCount = 1;
          privileges.Privileges[0].Luid = sedebugnameValue;
          privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    
          if (AdjustTokenPrivileges (token, FALSE, &privileges, sizeof (privileges), NULL, NULL)) {
            CloseHandle (token);
            return TRUE;
          }
        }
        CloseHandle (token);    
      }
    
      return FALSE;
    }
    
    int _tmain (int argc, _TCHAR* argv []) {
      DWORD processId = GetCurrentProcessId ();
      DWORD parentProcessId = GetParentProcessId ();
      _TCHAR pathname [MAX_PATH];
    
      if (argc < 2) {
        if (GetCurrentDirectory (MAX_PATH, pathname)) {
          _tprintf (_T ("%s\n"), pathname);
        }
        return 0;
      }
    
      if (_tclen (argv [1]) >= MAX_PATH) {
        _tprintf (_T ("The filename or extension is too long.\n"));
        return 0;
      }
    
      if (! SetCurrentDirectory (argv [1])) {
        _tprintf (_T ("The system cannot find the path specified.\n"));
      }
    
      //TODO: get the actual casing in the pathname
      wcscpy_s (pathname, argv [1]);
    
      if (! EnableDebugPrivileges ()) {
        _tprintf (_T ("Error: Cannot enable debug privileges."));
        return 0;
      }
    
      HANDLE target = OpenProcess (PROCESS_ALL_ACCESS, FALSE, parentProcessId);
      if (target != INVALID_HANDLE_VALUE) {
        HMODULE kernel32 = LoadLibrary (_T("kernel32.dll"));
        if (kernel32 != NULL) {
          FARPROC address = GetProcAddress (kernel32, "SetCurrentDirectoryW");
          if (address != NULL) {
            LPVOID remoteMemory = VirtualAllocEx (target, 0, sizeof (pathname), MEM_COMMIT| MEM_RESERVE, PAGE_EXECUTE_READWRITE);
            if (remoteMemory != NULL) {
              if (WriteProcessMemory (target, remoteMemory, pathname, sizeof (pathname), NULL)) {
                HANDLE handle = CreateRemoteThread (target, NULL, 0, (LPTHREAD_START_ROUTINE) address, remoteMemory, 0, NULL);
                if (handle == NULL) {
                  _tprintf (_T ("Error: %d"), GetLastError ());
                } else {
                  WaitForSingleObject (handle, 1000);
                  CloseHandle (handle);
                }
              }
              VirtualFreeEx (target, remoteMemory, 0, MEM_RELEASE);
            }
          }
          FreeLibrary (kernel32);
        }
        CloseHandle (target);
      }
      return 0;
    }
    

    A couple of notes:

    1) Due to the assumption about kernel32, the program must match the "bitness" of the command prompt.

    2) The program doesn't require elevation, but I wouldn't be surprised if it didn't work for a restricted user.

    EDIT: fixed a bad typo in VirtualFreeEx

  • User profile image
    androidi

    Cool, I'll give that a try (and port to c# if it looks to work). I thought about the injection route but it seemed too complicated and hacky. This "SetCurrentDirectory is compatible with LPTHREAD_START" idea is much more simpler to work with so if it works that would be great.

    Sven's batch & temp file approach works fine for practical application but doesn't satisfy the criteria. As my list replacement is windows only, your solution (with a fallback to the batch solution incase this breaks for some reason) is much preferred.

    I'll probably implement the fallback by creating an /INSTALL switch that creates the batch files which user can then add to %path% with the app.

     

    edit: I think this method may need to verify that the parent is actually a console, otherwise there could be interesting results - as in starting this from debugger shows the parent is devenv.exe.

  • User profile image
    Blue Ink

    ,androidi wrote

    ...

    edit: I think this method may need to verify that the parent is actually a console, otherwise there could be interesting results - as in starting this from debugger shows the parent is devenv.exe.

    Maybe. In its current form, the program is just a dumb CD replacement, it doesn't have any side effect unless you pass an argument, which rules out most accidents. For the purposes of our little excercise I considered that to be good enough.

    For production use, this might require further scrutiny: if the side effect is generally benign, you may want to think twice before limiting the scope of your program to CMD alone.

  • User profile image
    androidi

    @Blue Ink:

    I'll probably just white list cmd.exe:

    With Any CPU configuration the ability (atleast in my C# port) works in 64 bit cmd prompt but not in 32 bit cmd prompt. It doesn't work in PowerShell of either bitness. The Any CPU however does allow it to work in 32 bit 2003 server I tested inside virtual machine.

    So it works in the default prompts that come up in either 32 or 64 bit machines with Run->cmd and that's good enough, for the cases of using 32 bit cmd in 64 bit Windows or PowerShell, I suppose the batch file approach will have to do.

    EDIT: I just tested the Sven Groot batch file method and it doesn't seem to work in PowerShell! 

    EDIT2: Ok looks like the batch file method works but requires use of .ps1 file instead of cmd. Good thing PowerShell picks up the ps1 before the cmd file so atleast the same name can be used on %path%. 

Conversation locked

This conversation has been locked by the site admins. No new comments can be made.