Hi guys,
I have a question.
I'm working on a Server Client app via UDP socket.
This is a 3 way program; Server, Client and Tray (originally written in .NET framework 4.8, and I'm upgrading them to .NET9, so I decided to re-write a few functions)
The Server is a Windows Form app that sits on a computer
The Client is a Windows Service.
The Tray is a window-less app that just has a Tray Icon, and can do a few things.
Situation:
The Client (service app) starts at Windows load (with a delay).
The Tray loads after a user logs in.
The old version (.NET 4.8) loaded the Tray with a Task Schedule entry that is triggered when a user logs in and when a user is unlocked.
New, in the new version (.NET9), I want the Client (service) to handle the Tray loading (because every time the user logs in or unlocks the computer there is a flash of blue window from the Powershell script).
Problem:
Since the Client is a service, it doesn't interact with the user interface (GUI).
I spent a lot of time with ChatGPT on this issue.
We managed to get the Tray app running, but there is no GUI aspect to it (the Tray Icon is not showing) and I can't interact with it.
What is going on here is the Client is running the Tray Process in the background and not in the user session, we tried to specify the session ID, and try to force it to run under the users GUI, but no luck.
Here is one of the codes we tried:
Code:
Imports System.Runtime.InteropServices
Public Class ProcessHelper
' Define STARTUPINFO structure
<StructLayout(LayoutKind.Sequential, CharSet:=CharSet.Auto)>
Public Structure STARTUPINFO
Public cb As UInteger
Public lpReserved As String
Public lpDesktop As String
Public lpTitle As String
Public dwX As UInteger
Public dwY As UInteger
Public dwXSize As UInteger
Public dwYSize As UInteger
Public dwXCountChars As UInteger
Public dwYCountChars As UInteger
Public dwFillAttribute As UInteger
Public dwFlags As UInteger
Public wShowWindow As Short
Public cbReserved2 As Short
Public lpReserved2 As IntPtr
Public hStdInput As IntPtr
Public hStdOutput As IntPtr
Public hStdError As IntPtr
End Structure
' Define PROCESS_INFORMATION structure
<StructLayout(LayoutKind.Sequential)>
Public Structure PROCESS_INFORMATION
Public hProcess As IntPtr
Public hThread As IntPtr
Public dwProcessId As UInteger
Public dwThreadId As UInteger
End Structure
' Define CreateProcess function from kernel32.dll
<DllImport("kernel32.dll", SetLastError:=True, CharSet:=CharSet.Auto)>
Public Shared Function CreateProcess(
lpApplicationName As String,
lpCommandLine As String,
lpProcessAttributes As IntPtr,
lpThreadAttributes As IntPtr,
bInheritHandles As Boolean,
dwCreationFlags As UInteger,
lpEnvironment As IntPtr,
lpCurrentDirectory As String,
lpStartupInfo As STARTUPINFO,
ByRef lpProcessInformation As PROCESS_INFORMATION) As Boolean
End Function
' Method to get the last error code
<DllImport("kernel32.dll", SetLastError:=True)>
Public Shared Function GetLastError() As Integer
End Function
Public Shared Sub StartTray*****ingCreateProcess(trayAppPath As String)
Dim startupInfo As New STARTUPINFO()
startupInfo.cb = CUInt(Marshal.SizeOf(startupInfo))
startupInfo.lpDesktop = "winsta0\default" ' Ensure running on the interactive desktop
Dim processInfo As New PROCESS_INFORMATION()
' Start the process
Dim result As Boolean = CreateProcess(trayAppPath, Nothing, IntPtr.Zero, IntPtr.Zero, False, 0, IntPtr.Zero, IO.Path.GetDirectoryName(trayAppPath), startupInfo, processInfo)
If result Then
CreateLog($"Started tray application (PID: {processInfo.dwProcessId})")
Else
Dim lastError As Integer = GetLastError()
CreateLog($"Error starting tray application. Last Error: {lastError}")
End If
End Sub
Private Shared Sub CreateLog(logMessage As String)
Dim logFilePath As String = "D:\Logs\log.txt" ' Replace with your log file path
Dim logEntry As String = $"{DateTime.Now}: {logMessage}"
System.IO.File.AppendAllText(logFilePath, logEntry & Environment.NewLine & Environment.NewLine)
End Sub
End Class
Here is another version:
Code:
Imports System
Imports System.Diagnostics
Imports System.Runtime.InteropServices
Imports System.Text
Public Class ProcessHelper
<DllImport("Wtsapi32.dll", SetLastError:=True)>
Private Shared Function WTSQueryUserToken(sessionId As Integer, ByRef tokenHandle As IntPtr) As Boolean
End Function
<DllImport("Advapi32.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function DuplicateTokenEx(
existingToken As IntPtr,
desiredAccess As UInteger,
tokenAttributes As IntPtr,
impersonationLevel As Integer,
tokenType As Integer,
ByRef newToken As IntPtr) As Boolean
End Function
<DllImport("Kernel32.dll", SetLastError:=True)>
Private Shared Function WTSGetActiveConsoleSessionId() As Integer
End Function
<DllImport("Userenv.dll", SetLastError:=True, CharSet:=CharSet.Unicode)>
Private Shared Function CreateEnvironmentBlock(
ByRef lpEnvironment As IntPtr,
hToken As IntPtr,
bInherit As Boolean) As Boolean
End Function
<DllImport("Userenv.dll", SetLastError:=True)>
Private Shared Function DestroyEnvironmentBlock(lpEnvironment As IntPtr) As Boolean
End Function
<DllImport("Kernel32.dll", SetLastError:=True)>
Private Shared Function CloseHandle(hObject As IntPtr) As Boolean
End Function
Public Shared Sub StartProcessAsUser(applicationPath As String)
Dim sessionId As Integer = WTSGetActiveConsoleSessionId()
If sessionId = 0 Then
PCMonitor_Client.CreateLog("No active console session found.")
Return
End If
Dim userToken As IntPtr = IntPtr.Zero
Dim duplicateToken As IntPtr = IntPtr.Zero
Dim environmentBlock As IntPtr = IntPtr.Zero
Try
' Get user token for active session
If Not WTSQueryUserToken(sessionId, userToken) Then
PCMonitor_Client.CreateLog($"Failed to get user token. Error: {Marshal.GetLastWin32Error()}")
Return
End If
' Duplicate the token to create a primary token
If Not DuplicateTokenEx(userToken, &H10000000UI, IntPtr.Zero, 2, 1, duplicateToken) Then
PCMonitor_Client.CreateLog($"Failed to duplicate token. Error: {Marshal.GetLastWin32Error()}")
Return
End If
' Create environment block
If Not CreateEnvironmentBlock(environmentBlock, duplicateToken, False) Then
PCMonitor_Client.CreateLog($"Failed to create environment block. Error: {Marshal.GetLastWin32Error()}")
environmentBlock = IntPtr.Zero ' Ensure it's null if creation fails
End If
' Set process startup info
Dim si As New ProcessStartInfo With {
.FileName = applicationPath,
.UseShellExecute = False,
.RedirectStandardOutput = True,
.RedirectStandardError = True,
.CreateNoWindow = False,
.WorkingDirectory = IO.Path.GetDirectoryName(applicationPath)
}
' Set environment variables to ensure it runs on the correct desktop
si.EnvironmentVariables("SESSIONNAME") = "Console"
si.EnvironmentVariables("USERPROFILE") = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
si.EnvironmentVariables("SystemRoot") = Environment.GetEnvironmentVariable("SystemRoot")
si.EnvironmentVariables("TEMP") = Environment.GetEnvironmentVariable("TEMP")
' Set the desktop to the interactive user desktop (Winsta0\Default)
si.EnvironmentVariables("USERDOMAIN") = Environment.UserDomainName
' Start process
Using process As New Process()
process.StartInfo = si
process.Start()
PCMonitor_Client.CreateLog($"Started {applicationPath} (PID: {process.Id}) for Session {sessionId}")
' Optional: Bring the window to the front (if needed)
If process.MainWindowHandle <> IntPtr.Zero Then
SetForegroundWindow(process.MainWindowHandle)
End If
End Using
Catch ex As Exception
PCMonitor_Client.CreateLog($"Exception while starting process: {ex.Message}")
Finally
' Clean up
If environmentBlock <> IntPtr.Zero Then DestroyEnvironmentBlock(environmentBlock)
If duplicateToken <> IntPtr.Zero Then CloseHandle(duplicateToken)
If userToken <> IntPtr.Zero Then CloseHandle(userToken)
End Try
End Sub
Public Shared Sub StartTrayAppInUserSession(applicationPath As String)
Dim sessionId As Integer = WTSGetActiveConsoleSessionId()
If sessionId = 0 Then
PCMonitor_Client.CreateLog("No active console session found.")
Return
End If
' User token and environment setup
Dim userToken As IntPtr = IntPtr.Zero
Dim duplicateToken As IntPtr = IntPtr.Zero
Dim environmentBlock As IntPtr = IntPtr.Zero
Try
' Get user token for the active session
If Not WTSQueryUserToken(sessionId, userToken) Then
PCMonitor_Client.CreateLog($"Failed to get user token. Error: {Marshal.GetLastWin32Error()}")
Return
End If
' Duplicate the token to create a primary token
If Not DuplicateTokenEx(userToken, &H10000000UI, IntPtr.Zero, 2, 1, duplicateToken) Then
PCMonitor_Client.CreateLog($"Failed to duplicate token. Error: {Marshal.GetLastWin32Error()}")
Return
End If
' Create environment block
If Not CreateEnvironmentBlock(environmentBlock, duplicateToken, False) Then
PCMonitor_Client.CreateLog($"Failed to create environment block. Error: {Marshal.GetLastWin32Error()}")
environmentBlock = IntPtr.Zero ' Ensure it's null if creation fails
End If
' Set process startup info
Dim si As New ProcessStartInfo With {
.FileName = applicationPath,
.UseShellExecute = False,
.RedirectStandardOutput = True,
.RedirectStandardError = True,
.CreateNoWindow = False,
.WorkingDirectory = IO.Path.GetDirectoryName(applicationPath)
}
' Start process
Using process As New Process()
process.StartInfo = si
process.Start()
PCMonitor_Client.CreateLog($"Started {applicationPath} (PID: {process.Id}) for Session {sessionId}")
' Ensure tray application is brought to the front if necessary
If process.MainWindowHandle <> IntPtr.Zero Then
SetForegroundWindow(process.MainWindowHandle)
End If
End Using
Catch ex As Exception
PCMonitor_Client.CreateLog($"Exception while starting tray app: {ex.Message}")
Finally
' Clean up
If environmentBlock <> IntPtr.Zero Then DestroyEnvironmentBlock(environmentBlock)
If duplicateToken <> IntPtr.Zero Then CloseHandle(duplicateToken)
If userToken <> IntPtr.Zero Then CloseHandle(userToken)
End Try
End Sub
' Add this method for bringing the window to the foreground (optional)
<DllImport("user32.dll", SetLastError:=True)>
Private Shared Function SetForegroundWindow(hWnd As IntPtr) As Boolean
End Function
End Class
After a lot of tests, ChatGPT just goes in circles...
So I'm asking for help from the community.
Is there a way to load the Tray app from the Client (service) and get the GUI to work?
Thanks