4

I am writing a solution for users to open a file, and this file should navigate to a certain website and insert the user's username into the login form for them. This file needs to be accessed by users which are on a citrix session.

This should be extremely simple, and I have discovered a way of doing it via Powershell :

$aduser = Get-ADUser $env:USERNAME -Properties EmailAddress
$emailaddress = $aduser.EmailAddress

$url = "https://website.org/loginpage.asp"
$ie = New-Object -comobject "InternetExplorer.Application"
$ie.visible = $true
$ie.Navigate($url)
WaitForPage 10
$ie.Document.GetElementById("USERID").Value = $emailaddress

This works perfectly - it opens the web page, and inserts the username (email address).

However, when a user runs this from their machine, it seems impossible to hide either the CMD window (if running from .cmd or .bat) as well as the Powershell window. -WindowStyle Hidden just reduced the length of time the window appears for - which is not an acceptable solution.

So my next plan of action was to recreate the above code in c# and distribute it as an exe (as this is unlikely to show any console windows). However, I can't seem to find any method of doing this in C# which does not depend on external libraries (e.g. Selenium, which also requires a driver to be installed which is not a valid option for me).

I guess my question is - can the above Powershell script be recreated in C#? Is the -comobject from that script a .NET object, and if so how can I harness that in C#?


For reference - I am currently calling the .ps1 file as follows (in a CMD file) :

START Powershell.exe -WindowStyle Hidden -File \\file\Folder\SK\scripts\powershell\opensite.ps1

And I have not found any way of actually hiding the console windows which appear. I either need to find a solution to this, or a simple way of implementing the same thing in C#.

Bassie
  • 9,529
  • 8
  • 68
  • 159
  • 3
    You could convert your code to vbscript or jscript and run it with `wscript.exe`. Voila! No console window. – rojo May 05 '16 at 14:20
  • 1
    @rojo ugh as a powershell junkie this seems so backwards but I have to admit it's straightforward, well-supported, and easy. Worthy of an answer I think. – briantist May 05 '16 at 14:31

3 Answers3

1

You can certainly call COM objects in C#, as explained in this existing answer:

If the library is already registered, you can perform the following steps to have Visual Studio generate an interop assembly for you:

  • Open to your Visual Studio project.
  • Right click on 'References' (right under the project in your Solution Explorer) and select 'Add Reference'.
  • Select the COM tab.
  • Select the Component you wish to interop with.
  • Select 'ok'.

This will be a class or set of C# classes that wrap all of the COM interface stuff with a normal C# class. Then you just use it like any other C# library. If the import of the reference worked well, you can explore it like any other reference and the methods/structs/classes/constants should show up in that namespace and intellisense.

Alternatively, you could execute the PowerShell within C# using a Runspace and a Pipeline. See Runspace samples on MSDN (here's example 3 from the link):

namespace Microsoft.Samples.PowerShell.Runspaces
{
  using System;
  using System.Collections;
  using System.Management.Automation;
  using System.Management.Automation.Runspaces;
  using PowerShell = System.Management.Automation.PowerShell;

  /// <summary>
  /// This class contains the Main entry point for this host application.
  /// </summary>
  internal class Runspace03
  {
    /// <summary>
    /// This sample shows how to use the PowerShell class to run a
    /// script that retrieves process information for the list of 
    /// process names passed to the script. It shows how to pass input 
    /// objects to a script and how to retrieve error objects as well 
    /// as the output objects.
    /// </summary>
    /// <param name="args">Parameter not used.</param>
    /// <remarks>
    /// This sample demonstrates the following:
    /// 1. Creating a PowerSHell object to run a script.
    /// 2. Adding a script to the pipeline of the PowerShell object.
    /// 3. Passing input objects to the script from the calling program.
    /// 4. Running the script synchronously.
    /// 5. Using PSObject objects to extract and display properties from 
    ///    the objects returned by the script.
    /// 6. Retrieving and displaying error records that were generated
    ///    when the script was run.
    /// </remarks>
    private static void Main(string[] args)
    {
      // Define a list of processes to look for.
      string[] processNames = new string[] 
      {
        "lsass", "nosuchprocess", "services", "nosuchprocess2" 
      };

      // The script to run to get these processes. Input passed
      // to the script will be available in the $input variable.
      string script = "$input | get-process -name {$_}";

      // Create a PowerShell object. Creating this object takes care of 
      // building all of the other data structures needed to run the script.
      using (PowerShell powershell = PowerShell.Create())
      {
        powershell.AddScript(script);

        Console.WriteLine("Process              HandleCount");
        Console.WriteLine("--------------------------------");

        // Invoke the script synchronously and display the   
        // ProcessName and HandleCount properties of the 
        // objects that are returned.
        foreach (PSObject result in powershell.Invoke(processNames))
        {
          Console.WriteLine(
                            "{0,-20} {1}",
                            result.Members["ProcessName"].Value,
                            result.Members["HandleCount"].Value);
        }

        // Process any error records that were generated while running 
        //  the script.
        Console.WriteLine("\nThe following non-terminating errors occurred:\n");
        PSDataCollection<ErrorRecord> errors = powershell.Streams.Error;
        if (errors != null && errors.Count > 0)
        {
          foreach (ErrorRecord err in errors)
          {
            System.Console.WriteLine("    error: {0}", err.ToString());
          }
        }
      }

      System.Console.WriteLine("\nHit any key to exit...");
      System.Console.ReadKey();
    }
  }
}

While the second approach is no doubt going to take longer, it may make sense if you want to keep the PowerShell code out of the executable so that you can more easily change it, without having to recompile every time.

Essentially, the exe could just be used to execute a given powershell script invisibly.

Community
  • 1
  • 1
briantist
  • 45,546
  • 6
  • 82
  • 127
  • Thank you for the helpful answer. Do you happen to know what the `COM` object is called that I used in my PowerShell script? In PowerShell it seems to be called `InternetExplorer.Application` but when I search for this in the `COM` references tab in Visual Studio I can't find it - the closest I can get is `Microsoft Internet Controls` which does not seem right. Thanks again – Bassie May 05 '16 at 14:29
  • 1
    @Bassie I'm afraid I don't know. [This page on code project](http://www.codeproject.com/Articles/9683/Automating-Internet-Explorer) seems to suggest that `Microsoft Internet Controls` is one of the references you'll need, but it might be more work than I or you expected. [The answers to this question](http://stackoverflow.com/q/8438782/3905079) suggest that using a third party library is the way to go. Also look at rojo's comment on your question; it's a pretty good alternative since `wscript.exe` is included with the OS already. – briantist May 05 '16 at 14:35
1

As I commented above, you could use VBScript or Jscript via wscript.exe to avoid launching another console window. Here's an example Jscript script, written as a batch + Jscript hybrid. Save it with a .bat extension, salt to taste, and give it a shot.

@if (@CodeSection == @Batch) @then
@echo off & setlocal

wscript /e:JScript "%~f0"

goto :EOF
@end // end batch / begin JScript hybrid code

var user = WSH.CreateObject("ADSystemInfo"),
    email = GetObject("LDAP://" + user.UserName).EmailAddress,
    url = "https://website.org/loginpage.asp",
    ie = WSH.CreateObject('InternetExplorer.Application');

ie.visible = true;
ie.Navigate(url);
while (ie.readyState != 4) WSH.Sleep(25);

ie.document.getElementById('USERID').value = email;

if (ie.document.getElementById('password'))
    ie.document.getElementById('password').focus();

It's actually a polyglot script, in that you can save it either with a .bat extension or .js. As .js, you can double-click it and it'll launch (assuming .js files are associated with wscript.exe, as they typically are by default) without any console window at all.


If you'd prefer an .exe file, the script above can be modified fairly easily into one that will self-compile and link a Jscript.NET executable. (This script still gets a .bat extension.)

@if (@CodeSection == @Batch) @then
@echo off & setlocal

for /f "delims=" %%I in ('dir /b /s "%windir%\microsoft.net\*jsc.exe"') do (
    if not exist "%~dpn0.exe" "%%~I" /nologo /target:winexe /out:"%~dpn0.exe" "%~f0"
)
"%~dpn0.exe"
goto :EOF
@end // end batch / begin JScript.NET hybrid code

import System;
import System.Diagnostics;

try {
    var wshShell:Object = new ActiveXObject("Wscript.Shell"),
        user:Object = new ActiveXObject("ADSystemInfo"),
        email:String = GetObject("LDAP://" + user.UserName).EmailAddress,
        url:String = "https://website.org/loginpage.asp",
        ie:Object = new ActiveXObject('InternetExplorer.Application');
}
catch(e:Exception) { System.Environment.Exit(1); }

ie.visible = true;
ie.Navigate(url);

// force IE window to the foreground and give it focus
var proc:System.Diagnostics.Process[] = System.Diagnostics.Process.GetProcesses();
for (var i:Int16 = proc.length, hwnd:IntPtr = IntPtr(ie.hwnd); i--;) {
    if (proc[i].MainWindowHandle === hwnd && wshShell.AppActivate(proc[i].Id)) break;
}

while (ie.readyState != 4) System.Threading.Thread.Sleep(25);
ie.document.getElementById('USERID').value = email;
if (ie.document.getElementById('password'))
    ie.document.getElementById('password').focus();
Community
  • 1
  • 1
rojo
  • 24,000
  • 5
  • 55
  • 101
  • Hi rojo, great answer thanks for the help. Unfortunately this still displays a console window when running the script so I cannot use it! I have found another solution however, which I will post shortly. – Bassie May 09 '16 at 08:41
  • 1
    @Bassie I think you're mistaken. My first solution saved with a .js extension shows no console window when handled by `wscript`. See, the "c" in `cscript` stands for *console*; whereas the "w" in `wscript` means you're *wrong*. For the second solution, the.exe generated also shows no console window per the `target:winexe` switch -- and it would ostensibly be the .exe that you would deploy, not the .bat that creates it. – rojo May 09 '16 at 09:58
  • I see now - I did not fully understand what you mean by "run via `wscript`" as I thought this was my default, but it seems my default was set to `cscript` which is why I got the console window. After running your 2nd solution again I see that an `.exe` is generated - do you think you could please point me towards any reading material related to this exe generation? I have never seen this before and it seems like a very useful way of creating standalone exes (my current solution as I'm sure you've noticed requires a script saved in a static location and so is not exactly standalone) – Bassie May 09 '16 at 10:48
  • 1
    @Bassie I'm unaware of any reading material other than the compilers' `/help` switch. If you navigate to `%windir%\Microsoft.NET\Framework` then pick a .NET version, you'll find `csc.exe` for compiling and linking C# projects; `jsc.exe` for JScript.NET projects, and `vbc.exe` for VB.NET projects. All the various compile option switches and toggles in Visual Studio are just a fancy GUI for those console apps. Regarding the batch + JScript.NET hybrid format, it's just a variation on [one of these](https://gist.github.com/davidruhmann/5199433). – rojo May 09 '16 at 11:50
0

I was using the below C# until I properly understood rojo's answer. This method does work, but is not as simple/elegant as rojo's so I marked that as the answer.

static class Program
{
    private static void Main()
    {
        var startInfo = new ProcessStartInfo(@"\\file\administration\Unused\Apps\opencascade\Alternate.bat")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            CreateNoWindow = true
        };
        Process.Start(startInfo);

        var startInfo = new ProcessStartInfo("Wscript.exe", @"\\file\administration\Unused\Apps\opencascade\Alternate.js")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            CreateNoWindow = true
        };
        Process.Start(startInfo);

        var startInfo = new ProcessStartInfo("Powershell.exe",
            @"\\file\administration\Unused\Apps\opencascade\opencascade.ps1")
        {
            WindowStyle = ProcessWindowStyle.Hidden,
            UseShellExecute = false,
            CreateNoWindow = true
        };
        Process.Start(startInfo);
    }
}

Note that the main method here is running 3 different solutions, one after each other. The different "versions" are each dependant on whether it makes more sense in the environment to use .bat or .ps1 or .js. I will be using the .js version, which works in this context and the executable generated by this C# code hides the resulting console window.

Bassie
  • 9,529
  • 8
  • 68
  • 159