Powershell Service - Launch Process in User Session

This topic contains 0 replies, has 1 voice, and was last updated by Profile photo of Forums Archives Forums Archives 5 years, 4 months ago.

  • Author
    Posts
  • #5947

    by z3r0c00l12 at 2012-11-03 12:44:08

    Hi everybody,

    It's my first time here, I have an issue I'm trying to solve and I can't find any way of getting it to work.

    Here's the scenario:
    SCCM executes a Powershell script which performs the installation of an application, depending on the application, a restart may be required.
    Powershell creates a "Run on Startup" scheduled task.
    On Reboot, Powershell executes to complete the installation, but the user can't see the progress of the installation.
    I would like to be able to launch a process in the user's session which will display only the progress.

    I searched for how to break out of Session 0 and found that the Windows API has a function called CreateProcessAsUser as part of advapi32.dll, but all the example code is in C#.
    What I need to know is how to call that function from within Powershell.

    The process in the user's session doesn't require any administrative rights and doesn't need to get any data from Powershell.

    tl;dr : How to call "CreateProcessAsUser" from Powershell to break out of Session 0.

    Thank you.

    by poshoholic at 2012-11-05 06:55:11

    If your scheduled task is launched on startup, that happens before any users have logged in, does it not? If that's the case, how can you launch that process as a user such that the user will see the progress? It sounds to me like the user session doesn't exist when the scheduled task launches, unless I'm misunderstanding something here.

    by z3r0c00l12 at 2012-11-05 11:51:50

    Yes, you are correct, see below. This would mostly be to launch the process when the installation is initiated by SCCM, when a user is very likely to be logged in.

    My intention was to have a loop, waiting for the user to log in before launching it. I am still debating between these 3 options:
    1) Powershell script looping until a user logs in, then launch the process.
    2) Set the process in the HKLM\...\Run key and remove it at then end of the installation, so if no user logs in, the process isn't started.
    3) Creating a scheduled task set to "On logon" with the process.

    My preference was the first because it's easier to get the process id for that specific instance of the process.
    Second option was still feasible, although it meant that I wasn't getting the Process ID back when the process launched.
    I'm still unclear if the 3rd option would work properly.

    Are you in Downtown Ottawa?

    Thank you.

    by poshoholic at 2012-11-05 13:09:27

    I work at home in Orleans. I could come downtown though, I have to do that later this week anyway for a few things. Or we could meet up in Orleans. Email me off-list and we can see if we can work something out. My email address matches my username on this site, on both hotmail and gmail...take your pick.

    by Makovec at 2012-11-10 05:00:58

    Hi,
    We had the same "problems" in our company. What we did is that admin parts run normally from ConfigMgr and user part is started after reboot. We use RunOnce key (example here: http://msdn.microsoft.com/en-us/library/windows/hardware/ff550714(v=vs.85).aspx), to be sure it will disappear automatically after run.

    Regarding CreateProcessAsUser – as my colleague (packager) doesn't like RunOnce, I created for him short C# file where he can specify EXE file and it's run from ConfigMgr System account but as user. Unfortunately I am not sure I can share it, but if you'll look to pinvoke.net and will do some searches, you will find the way.

    From my point of view – RunOnce is easier and works for 99% of cases (we had some troubles with Java Updates over RunOnce). It's just there waiting for user login. C# way is complicated (at least for me non-developer).

    David

    by nohandle at 2012-11-14 14:45:36

    not a posh way but psexec can create a window visible to user from system account context just by specifying -i -s parameters.

    If I would try to solve that issue I would take this approach:
    1) create script that checks if any installation is pending (from log)

    2) set "pending installation" flag before starting the installation
    3) enable logon task that runs the app
    4) install
    5) restart
    6) the app checks regularily if the installation is finished and informs the user (in awesome gui app)
    7) disable logon task
    8) profit??

    by z3r0c00l12 at 2012-11-15 04:52:02

    Following Makovec's advice, I found an example online and modified it. It might not be the most elegant solution, but it works.

    Here's a sample of the code, that I made in a function to which you need to pass $Command, which will be the command executed. I've modified the code I found online to return the ProcessID of the program as that's what I needed in my case.

    This will create a file "ProcessName.exe", you can change the name if you wish, then you need to execute it, and in my case, I then delete the "ProcessName.exe" and "ProcessName.pdb" that the Add-Type command creates.

    I've tested using a scheduled task running as system and managed to start a calculator in my user session. Now I just need to modify it to pass the command as an argument and not have to recompile the application every time.

    As a side note, I don't think RunOnce is a good option, because it doesn't execute as a regular user on Windows 7, see http://support.microsoft.com/kb/2021405.


    $results = C:\windows\system32\query.exe session

    # Create object array of Query results
    $active = $results | %{
    $line = $_.ToString().Split(" ")
    $array = $line | Where { $_ -ne "" }

    Switch ($array.count) {
    3 {
    $sessionname = $array[0]
    $username = $null
    $id = $array[1]
    $state = $array[2]
    $type = $null
    Break
    }

    4 {
    $sessionname = $array[0]
    $username = $array[1]
    $id = $array[2]
    $state = $array[3]
    $type = $null
    Break
    }

    5 {
    $sessionname = $array[0]
    $username = $array[1]
    $id = $array[2]
    $state = $array[3]
    $type = $array[4]
    Break
    }
    } # Terminate Switch

    $object = New-Object PSObject -Property @{
    sessionname = $sessionname
    username = $username
    ID = $id
    state = $state
    type = $type
    }
    Write-Output $object
    } | Where { $_.State -eq "Active" }
    if ($active) {
    $ProgramSource = @"
    using System;
    using System.Text;
    using System.Runtime.InteropServices;
    using System.ServiceProcess;
    using System.Diagnostics;

    namespace PS
    {
    public class Emulate {

    static int Main(string[] args) {
    int iRet = 0;
    Process pid = EmulateSession.StartProcessInSession($($active.Id), @"$command");
    iRet = pid.Id;
    return iRet;
    }

    public static void WriteToEventLog(string message) {
    string cs = "$ScriptName";
    EventLog elog = new EventLog();
    if (!EventLog.SourceExists(cs))
    {
    EventLog.CreateEventSource(cs, "Application");
    }
    elog.Source = cs;
    elog.EnableRaisingEvents = true;
    EventLog.WriteEntry(cs, message, EventLogEntryType.Information);
    }
    }

    static public class EmulateSession
    {
    /* structs, enums, and external functions defined at end of code */

    public static System.Diagnostics.Process StartProcessInSession(int sessionID, String commandLine)
    {
    Emulate.WriteToEventLog("Inside StartProcessInSession");
    Emulate.WriteToEventLog("Session ID: " + sessionID.ToString());
    IntPtr userToken;
    if (WTSQueryUserToken(sessionID, out userToken))
    {
    //note that WTSQueryUserToken only works when in context of local system account with SE_TCB_NAME
    IntPtr lpEnvironment;
    Emulate.WriteToEventLog("Token: " + userToken.ToString());
    if (CreateEnvironmentBlock(out lpEnvironment, userToken, false))
    {
    Emulate.WriteToEventLog("User Env: " + lpEnvironment.ToString());
    StartupInfo si = new StartupInfo();
    si.cb = Marshal.SizeOf(si);
    si.lpDesktop = "winsta0\\default";
    si.dwFlags = STARTF.STARTF_USESHOWWINDOW;
    // Using the SW_HIDE will make the window hidden, see in the bottom section for more commands
    si.wShowWindow = ShowWindow.SW_HIDE;
    ProcessInformation pi;
    // Note the CreationFlags, they make this work as it must have both the CREATE_NEW_CONSOLE and CREATE_UNICODE_ENVIRONMENT
    if (CreateProcessAsUser(userToken, null, new StringBuilder(commandLine), IntPtr.Zero, IntPtr.Zero, false, CreationFlags.CREATE_NEW_CONSOLE | CreationFlags.CREATE_UNICODE_ENVIRONMENT, lpEnvironment, null, ref si, out pi))
    {
    try
    {
    Emulate.WriteToEventLog("Launched PID: " + pi.dwProcessId.ToString());
    return System.Diagnostics.Process.GetProcessById(pi.dwProcessId);
    }
    catch (ArgumentException)
    {
    return null;
    }
    }
    else
    {
    Emulate.WriteToEventLog("Could Not Create Process.");
    int err = Marshal.GetLastWin32Error();
    throw new System.ComponentModel.Win32Exception(err, "Could not create process.\nWin32 error: " + err.ToString());
    }
    }
    else
    {
    Emulate.WriteToEventLog("Could not create environment block.");
    int err = Marshal.GetLastWin32Error();
    throw new System.ComponentModel.Win32Exception(err, "Could not create environment block.\nWin32 error: " + err.ToString());
    }
    }
    else
    {
    Emulate.WriteToEventLog("No Token");
    int err = System.Runtime.InteropServices.Marshal.GetLastWin32Error();
    if (err == 1008) return null; //There is no token
    throw new System.ComponentModel.Win32Exception(err, "Could not get the user token from session " + sessionID.ToString() + " - Error: " + err.ToString());
    }
    }

    [DllImport("wtsapi32.dll", SetLastError = true)]
    static extern bool WTSQueryUserToken(Int32 sessionId, out IntPtr Token);

    [DllImport("userenv.dll", SetLastError = true)]
    static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit);

    [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    static extern bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, [In] StringBuilder lpCommandLine, IntPtr /*to a SecurityAttributes struct or null*/ lpProcessAttributes, IntPtr /*to a SecurityAttributes struct or null*/ lpThreadAttributes, bool bInheritHandles, CreationFlags creationFlags, IntPtr lpEnvironment, String lpCurrentDirectory, ref StartupInfo lpStartupInfo, out ProcessInformation lpProcessInformation);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool CloseHandle(IntPtr hHandle);

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    struct StartupInfo
    {
    public Int32 cb;
    public String lpReserved;
    public String lpDesktop;
    public String lpTitle;
    public Int32 dwX;
    public Int32 dwY;
    public Int32 dwXSize;
    public Int32 dwYSize;
    public Int32 dwXCountChars;
    public Int32 dwYCountChars;
    public Int32 dwFillAttribute;
    public STARTF dwFlags;
    public ShowWindow wShowWindow;
    public Int16 cbReserved2;
    public IntPtr lpReserved2;
    public IntPtr hStdInput;
    public IntPtr hStdOutput;
    public IntPtr hStdError;
    }

    [StructLayout(LayoutKind.Sequential)]
    internal struct ProcessInformation
    {
    public IntPtr hProcess;
    public IntPtr hThread;
    public int dwProcessId;
    public int dwThreadId;
    }

    ///

    /// The following process creation flags are used by the CreateProcess, CreateProcessAsUser, CreateProcessWithLogonW, and CreateProcessWithTokenW functions. They can be specified in any combination, except as noted.
    ///

    [Flags]
    enum CreationFlags : int
    {
    NONE = 0,
    DEBUG_PROCESS = 0x00000001,
    DEBUG_ONLY_THIS_PROCESS = 0x00000002,
    CREATE_SUSPENDED = 0x00000004,
    DETACHED_PROCESS = 0x00000008,
    CREATE_NEW_CONSOLE = 0x00000010,
    CREATE_NEW_PROCESS_GROUP = 0x00000200,
    CREATE_UNICODE_ENVIRONMENT = 0x00000400,
    CREATE_SEPARATE_WOW_VDM = 0x00000800,
    CREATE_SHARED_WOW_VDM = 0x00001000,
    CREATE_PROTECTED_PROCESS = 0x00040000,
    EXTENDED_STARTUPINFO_PRESENT = 0x00080000,
    CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
    CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000,
    CREATE_DEFAULT_ERROR_MODE = 0x04000000,
    CREATE_NO_WINDOW = 0x08000000,
    }

    [Flags]
    public enum STARTF : uint
    {
    STARTF_USESHOWWINDOW = 0x00000001,
    STARTF_USESIZE = 0x00000002,
    STARTF_USEPOSITION = 0x00000004,
    STARTF_USECOUNTCHARS = 0x00000008,
    STARTF_USEFILLATTRIBUTE = 0x00000010,
    STARTF_RUNFULLSCREEN = 0x00000020, // ignored for non-x86 platforms
    STARTF_FORCEONFEEDBACK = 0x00000040,
    STARTF_FORCEOFFFEEDBACK = 0x00000080,
    STARTF_USESTDHANDLES = 0x00000100,
    }

    public enum ShowWindow : short
    {
    SW_HIDE = 0,
    SW_SHOWNORMAL = 1,
    SW_NORMAL = 1,
    SW_SHOWMINIMIZED = 2,
    SW_SHOWMAXIMIZED = 3,
    SW_MAXIMIZE = 3,
    SW_SHOWNOACTIVATE = 4,
    SW_SHOW = 5,
    SW_MINIMIZE = 6,
    SW_SHOWMINNOACTIVE = 7,
    SW_SHOWNA = 8,
    SW_RESTORE = 9,
    SW_SHOWDEFAULT = 10,
    SW_FORCEMINIMIZE = 11,
    SW_MAX = 11
    }
    }
    }
    "@
    # Using the OutputAssembly and OutputType we can make an executable out of this. It requires the System.ServiceProcess assembly also to inherit the ServiceBase class.
    Add-Type -TypeDefinition $ProgramSource -Language CSharpVersion3 -OutputAssembly "ProcessName.exe" -OutputType ConsoleApplication -ReferencedAssemblies "System.ServiceProcess"
    }

    Edit: Corrected errors in code.

    by selko at 2012-11-20 22:50:56

    Hi z3r0c00l12,

    my first post goes to you 🙂

    I have tried your code and it works.

    [quote]On Reboot, Powershell executes to complete the installation, but the user can't see the progress of the installation.
    I would like to be able to launch a process in the user's session which will display only the progress.[/quote]

    But how can you display progress to the user session when the setup (f.e standard MSI installation with /QB parameters) is called with system account.
    Because the logged on user has no access to install applications. Hope you understand my question 🙂

    Thanks

    by z3r0c00l12 at 2012-11-21 03:46:46

    Hi Selko,

    In our case, the process launched in the user session simply informs them that an installation is in progress, with some information on the application, so the user doesn't need admin rights since the installation is performed by the system account.

    Thank you.

    by selko at 2012-11-21 04:47:29

    Hi z3r0c00l12,

    now i got it 🙂

    Is this a MessageBox where user needs to Click on it? Or a Windows From with Timer which closes itself after a while.

    Thanks

    by z3r0c00l12 at 2012-11-21 14:19:20

    It's a windows form, but the ProcessID is returned by the function above, so when installation completes, I close the process.

You must be logged in to reply to this topic.