Visual Studio or VB6.0 script

Here’s the background:  In Windows 2012 (in workgroup mode) the only way to shadow a user's RDP session is via command line, which is really annoying.

So I wanted to build a small utility around the command line that would be more user friendly.

I wrote “shadow.bat” that eases the pain a little.  It accepts one parameter.  That parameter is the user who you want to shadow.
i.e. If I log on to a 2012 server and want to see what user BOB is doing, I need to run/type:  shadow BOB

My code for "shadow.bat" is simple:
for /F "tokens=3 skip=1" %%i in ('query user %1') DO (
mstsc.exe /shadow:%%i /noConsentPrompt /control
)

The incoming %1 parameter is the username I want to shadow. It obtains the session number of the user from there (%%I) and runs mstsc.exe with a shadow switch. I'm good to go.

What I really want to do is take away the need to run this .bat manually from command line.

How difficult would it be to have my code inside a compiled .exe where upon running, the program presents you with a dropdown list of logged on users, and you pick who you want to shadow ?

So I’d run “shadow.exe” and it would show a list of users logged on to my server, I'd pick a user to shadow, and it basically takes that username and puts it into the %1 variable in my bat script.

I'm good at .bat, vbscript, VB6.0....but .NET not really. This is a little beyond my area of expertise.  does anybody have code suggestions? maybe something I can copy & paste then compile?
LVL 15
ZabagaRAsked:
Who is Participating?

[Product update] Infrastructure Analysis Tool is now available with Business Accounts.Learn More

x
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

Kyle AbrahamsSenior .Net DeveloperCommented:
The good news with going with .Net is you can find a lot of samples online.  I didn't test this code but it looks good:


using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace EnumerateRDUsers
{
  class Program
  {
    [DllImport("wtsapi32.dll")]
    static extern IntPtr WTSOpenServer([MarshalAs(UnmanagedType.LPStr)] String pServerName);

    [DllImport("wtsapi32.dll")]
    static extern void WTSCloseServer(IntPtr hServer);

    [DllImport("wtsapi32.dll")]
    static extern Int32 WTSEnumerateSessions(
        IntPtr hServer,
        [MarshalAs(UnmanagedType.U4)] Int32 Reserved,
        [MarshalAs(UnmanagedType.U4)] Int32 Version,
        ref IntPtr ppSessionInfo,
        [MarshalAs(UnmanagedType.U4)] ref Int32 pCount);

    [DllImport("wtsapi32.dll")]
    static extern void WTSFreeMemory(IntPtr pMemory);

    [DllImport("Wtsapi32.dll")]
    static extern bool WTSQuerySessionInformation(
        System.IntPtr hServer, int sessionId, WTS_INFO_CLASS wtsInfoClass, out System.IntPtr ppBuffer, out uint pBytesReturned);

    [StructLayout(LayoutKind.Sequential)]
    private struct WTS_SESSION_INFO
    {
      public Int32 SessionID;

      [MarshalAs(UnmanagedType.LPStr)]
      public String pWinStationName;

      public WTS_CONNECTSTATE_CLASS State;
    }

    public enum WTS_INFO_CLASS
    {
      WTSInitialProgram,
      WTSApplicationName,
      WTSWorkingDirectory,
      WTSOEMId,
      WTSSessionId,
      WTSUserName,
      WTSWinStationName,
      WTSDomainName,
      WTSConnectState,
      WTSClientBuildNumber,
      WTSClientName,
      WTSClientDirectory,
      WTSClientProductId,
      WTSClientHardwareId,
      WTSClientAddress,
      WTSClientDisplay,
      WTSClientProtocolType
    }
    public enum WTS_CONNECTSTATE_CLASS
    {
      WTSActive,
      WTSConnected,
      WTSConnectQuery,
      WTSShadow,
      WTSDisconnected,
      WTSIdle,
      WTSListen,
      WTSReset,
      WTSDown,
      WTSInit
    }

    static void Main(string[] args)
    {
      ListUsers("<INSERT SERVERNAME HERE>");
    }

    public static IntPtr OpenServer(String Name)
    {
      IntPtr server = WTSOpenServer(Name);
      return server;
    }
    public static void CloseServer(IntPtr ServerHandle)
    {
      WTSCloseServer(ServerHandle);
    }
    public static void ListUsers(String ServerName)
    {
      IntPtr serverHandle = IntPtr.Zero;
      List<String> resultList = new List<string>();
      serverHandle = OpenServer(ServerName);

      try
      {
        IntPtr SessionInfoPtr = IntPtr.Zero;
        IntPtr userPtr = IntPtr.Zero;
        IntPtr domainPtr = IntPtr.Zero;
        Int32 sessionCount = 0;
        Int32 retVal = WTSEnumerateSessions(serverHandle, 0, 1, ref SessionInfoPtr, ref sessionCount);
        Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
        Int32 currentSession = (int)SessionInfoPtr;
        uint bytes = 0;

        if (retVal != 0)
        {
          for (int i = 0; i < sessionCount; i++)
          {
            WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)currentSession, typeof(WTS_SESSION_INFO));
            currentSession += dataSize;

            WTSQuerySessionInformation(serverHandle, si.SessionID, WTS_INFO_CLASS.WTSUserName, out userPtr, out bytes);
            WTSQuerySessionInformation(serverHandle, si.SessionID, WTS_INFO_CLASS.WTSDomainName, out domainPtr, out bytes);

            Console.WriteLine("Domain and User: " + Marshal.PtrToStringAnsi(domainPtr) + "\\" + Marshal.PtrToStringAnsi(userPtr));

            WTSFreeMemory(userPtr); 
            WTSFreeMemory(domainPtr);
          }

          WTSFreeMemory(SessionInfoPtr);
        }
      }
      finally
      {
        CloseServer(serverHandle);
      }

    }

  }
}

Open in new window


Instead of the  

Console.WriteLine("Domain and User: " + Marshal.PtrToStringAnsi(domainPtr) + "\\" + Marshal.PtrToStringAnsi(userPtr));

you would add the item to a dropdownlist.  

From there you would also need a button and a button click event to call your command line shell execute.  

In case you're looking at samples, http://www.developerfusion.com/tools/convert/csharp-to-vb/ to convert from c# to vb and vice versa.  

VB6 has been dead for years.  Never hurts to learn new technologies and the conversion isn't *that* bad.  For something like this it's the perfect project to go .Net.  Try it out, let us know if you have any questions.
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
ZabagaRAuthor Commented:
Thanks I've seen the sample code all over too...but unless somebody actually writes it for me in this case, I'm not going to get anywhere.
0
Kyle AbrahamsSenior .Net DeveloperCommented:
Maybe you're better off hiring someone to do it for you then.
0
käµfm³d 👽Commented:
I would agree with Kyle if you're not comfortable attempting this yourself. But honestly, you can find tons of introductory WinForms tutorials that would be simple and straight-forward. IMHO, this is a simple task, and after 1/2 with a decent tutorial, you should know how to the dropdown bit.

As for executing your batch script from .NET, you would simply need the Process class (example usage at bottom of page).
0
ZabagaRAuthor Commented:
Thanks, I appreciate the input.  A co-worker who does much more .net helped me out and I got it pretty quickly.
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
.NET Programming

From novice to tech pro — start learning today.