Triggering WinCC Logoff via Windows Event Log with VBScript

David Krause18 min read
SiemensTutorial / How-toWinCC
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

1. Problem Statement and Operational Context

When a WinCC Runtime station is operated from a remote location via Remote Desktop (RDP), the Windows session lifecycle is decoupled from the operator's physical presence. On a Windows server configured with the default Terminal Services policy, a disconnected RDP session is kept alive on the host indefinitely; see the guidance in Troubleshoot unexpected reboots using system event logs for how session state is recorded in the system event logs. The consequence for a WinCC HMI station is that the previously authenticated Windows user remains logged on, and the WinCC operator account remains active, even though no human is at the console. Most plant security policies require the active WinCC user account to be terminated as soon as the physical console is unattended.

The recommended signal source for the trigger is the Windows Event Log, exposed by the Event Viewer (see Event Viewer - Microsoft Learn). Two distinct mechanisms are documented in this article:

  1. A VBScript inside a WinCC Global Script action that polls the Microsoft-Windows-TerminalServices-LocalSessionManager/Operational channel through the WMI class Win32_NTLogEvent on a 1-second timer. This is the shortest path and is supported on every WinCC V7 installation without a compiler.
  2. A C action that subscribes to the same channel through the native EvtSubscribe API (Windows Event Log API introduced in Vista) and pushes the event into WinCC through an internal tag. This drops the latency from approximately 1 s to under 100 ms at the cost of a compiled DLL.

Both approaches are interoperable with the WinCC Runtime Process Mode (WinCC V7.0 SP3 and later) and the WinCC Runtime Professional (TIA Portal V13 and later).

2. Architecture Overview

The data flow from the RDP event to the WinCC logoff action is summarized in the diagram below. The RDP client (mstsc.exe) closes its window on the remote workstation; Windows writes a record to the LocalSessionManager log; the WinCC Global Script detects it; an internal trigger tag is set; the UserAdmin control bound to that tag executes the standard WinCC logoff routine.

WinCC Logoff driven by RDP disconnect event RDP Client mstsc.exe WinCC Runtime Station LocalSessionManager Log Event ID 26 written Global Script VBS Win32_NTLogEvent query 1 s cyclic trigger TriggerLogoff tag + UserAdmin event WinCC logoff routine WinCC operator account terminated

3. Prerequisites

  • Siemens WinCC V7.0 SP3 or later, or WinCC Runtime Professional (TIA Portal V13 or later) with the Global Script option licensed.
  • Microsoft Windows Server 2012 R2 / 2016 / 2019 / 2022, or Windows 10 / 11. The WinCC installation must be on a Windows version supported by the installed WinCC release; consult the WinCC Installation Notes for the exact matrix.
  • Local administrator rights on the WinCC station to enable the LocalSessionManager operational log and to grant the WinCC service read permission on the channel.
  • A working RDP listener. Confirm with netstat -ano | findstr :3389 on the station; the listener must show LISTENING.
  • An internal WinCC tag of type Binary Tag named TriggerLogoff defined in the tag management of the active project.
  • An internal WinCC tag of type Text Tag 16 named RDP_LastSeenOffset used as a high-water mark for the polling script.

4. Windows Event Log Channels Relevant to RDP

The Windows Event Viewer (see Event Viewer - Microsoft Learn) exposes the following channels that are useful for detecting RDP lifecycle events. Only the first two are normally read from inside WinCC.

Channel Provider name When records are written Default state
Microsoft-Windows-TerminalServices-LocalSessionManager/Operational Microsoft-Windows-TerminalServices-LocalSessionManager RDP session create, disconnect, reconnect, logoff at the host level Enabled on Windows Server; disabled on Windows 10 / 11
Security Microsoft-Windows-Eventlog Logon / logoff / credential validation Enabled
System Service Control Manager Service state transitions; used as a secondary signal if TerminalServices-LocalSessionManager is disabled Enabled
Application Various WinCC Runtime and HMIRuntime application-level events Enabled
Note: Reading the Security channel from a service account requires the Audit Policy > Logon/Logoff categories to be enabled and the WinCC Runtime service account to be a member of the local Event Log Readers group. The LocalSessionManager operational channel requires only read access to the channel SD, which the default WinCC Runtime service identity already holds on Windows Server.

5. RDP Event ID Reference

The Microsoft-Windows-TerminalServices-LocalSessionManager/Operational channel is the canonical source. Each RDP lifecycle transition writes exactly one of the Event IDs below.

Event ID Written on Should WinCC act? Recommended action
21 Session logon succeeded (RDP connection accepted) Optional Log only; do not log off
22 Session logoff succeeded No No-op: the user has already terminated their session
23 Session lock Optional Lock the WinCC screen instead of logging off
24 Session unlock Optional Require re-authentication
25 Session reconnection succeeded No No-op
26 Session reconnection disconnected (RDP client closed without logoff) Yes Trigger WinCC logoff on the operator whose console session is active
27 Session reconnection failed No Audit-only; route to the WinCC audit log if present
4778 (Security) A session was reconnected to a WinStation Optional Backup signal if LocalSessionManager is disabled
4779 (Security) A session was disconnected from a WinStation Optional Backup signal
Why Event 26 is the trigger: When the operator closes the RDP window without explicitly logging off, Windows writes Event 26, not Event 22. Event 26 is the only signal that uniquely identifies the situation "operator has left, but the Windows session is still alive on the server". Hooking Event 22 alone misses the most common scenario.

6. Enabling the LocalSessionManager Channel

On Windows 10 / 11 the operational channel is disabled by default. Confirm with eventvwr.msc > Applications and Services Logs > Microsoft > Windows > TerminalServices-LocalSessionManager > Operational. If the channel is disabled, enable it from an elevated command prompt:

wevtutil set-log "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational" /enabled:true

Verify that records are now being written by establishing a test RDP connection from a second PC and inspecting the channel. If the records still do not appear, ensure that the Remote Desktop Services service is running and that the Network Policy Server is not filtering the connection.

7. Reading the Channel with Win32_NTLogEvent (VBScript)

Win32_NTLogEvent is a WMI class that exposes every record in the classical (pre-Vista) channels and, with the appropriate Logfile parameter, every record in the Vista+ operational channels. The schema fields used by the script below are:

Field Type Meaning
EventCode String The numeric Event ID, returned as a string (compare with '26', not 26)
Logfile String Channel name. For Vista+ channels pass the full path
TimeWritten String UTC timestamp in YYYYMMDDHHMMSS.XXXXXX+ZZZ format
Message String Rendered localized event message
RecordNumber UInt32 Record sequence within the channel
SourceName String Provider name

Add the following project function in the WinCC Global Script editor (menu Global Script > Project Functions). It returns the most recent matching event as an array of three elements: EventCode, TimeWritten, Message.

' --- BEGIN ReadLastRDPSessionEvent (project function) -----------------
Option Explicit

Function ReadLastRDPSessionEvent()
    Dim objWMIService, colEvents, objEvent
    Dim strComputer, strLog, strQuery
    Dim dtmLastTime, objLastEvent

    strComputer = "."
    strLog = "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational"

    strQuery = "Select * From Win32_NTLogEvent Where " & _
               "Logfile='" & strLog & "' And " & _
               "(EventCode='21' Or EventCode='22' Or EventCode='26')"

    Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate,(Security)}!\\\\" & _
                                strComputer & "\\root\\cimv2")
    Set colEvents = objWMIService.ExecQuery(strQuery)

    dtmLastTime = "00000000000000.000000+000"
    Set objLastEvent = Nothing

    For Each objEvent In colEvents
        If CDate(Mid(objEvent.TimeWritten, 1, 14)) > _
           CDate(Mid(dtmLastTime, 1, 14)) Then
            Set objLastEvent = objEvent
            dtmLastTime = objEvent.TimeWritten
        End If
    Next

    If Not objLastEvent Is Nothing Then
        ReadLastRDPSessionEvent = Array(CStr(objLastEvent.EventCode), _
                                        CStr(objLastEvent.TimeWritten), _
                                        CStr(objLastEvent.Message))
    Else
        ReadLastRDPSessionEvent = Array("", "", "")
    End If
End Function
' --- END --------------------------------------------------------------

Typical execution time on a local query against the LocalSessionManager channel is 80 - 250 ms on a quad-core WinCC station. If the channel holds tens of thousands of records, query time grows linearly; on stations that have run for several years, consider increasing the WMI repository size or pruning the channel with wevtutil cl Microsoft-Windows-TerminalServices-LocalSessionManager/Operational during a planned maintenance window.

8. Native Vista+ API from a C Action (Latency-Sensitive Variant)

When the 1-second polling latency is too long, replace the WMI call with a native subscription through EvtSubscribe. The required headers and library are wevtapi.h and wevtapi.lib, both shipped with the Windows SDK. The C snippet below is suitable for inclusion in a WinCC ODK external function.

// --- BEGIN EvtSubscribe from a WinCC C action -----------------------
#include <windows.h>
#include <winevt.h>
#pragma comment(lib, "wevtapi.lib")

static EVT_HANDLE g_hSubscription = NULL;
static DWORD WINAPI SubscriptionCallback(EVT_SUBSCRIBE_CALLBACK_FLAG flags,
                                          void* ctx, EVT_HANDLE hEvent) {
    PEVT_VARIANT pProps = NULL;
    DWORD dwCount = 0, dwBuf = 0;
    EvtRender(hEvent, EvtRenderEventXml, 0, NULL, &dwBuf, NULL);
    // allocate buffer of dwBuf bytes, EvtRender again, parse <EventID>26</EventID>
    // when matched, post a message to WinCC through an internal tag
    return 0;
}

extern "C" __declspec(dllexport) void __stdcall StartRDPSubscription() {
    LPCWSTR szChannel =
      L"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational";
    LPCWSTR szQuery   = L"*[System[EventID=26]]";
    g_hSubscription = EvtSubscribe(NULL, NULL, szChannel, szQuery,
                                   NULL, NULL,
                                   (EVT_SUBSCRIBE_CALLBACK)SubscriptionCallback,
                                   EvtSubscribeStartAtOldestRecord);
}

extern "C" __declspec(dllexport) void __stdcall StopRDPSubscription() {
    if (g_hSubscription) { EvtClose(g_hSubscription); g_hSubscription = NULL; }
}
// --- END -------------------------------------------------------------

Compile as a 32-bit DLL on a 32-bit WinCC station or as a 64-bit DLL on a 64-bit WinCC Professional station, then register it as an ODK external function. The push path keeps end-to-end latency from RDP disconnect to WinCC logoff under 100 ms.

9. Building the WinCC Logoff Action

There is no public WinCC VBScript method that logs off the current user directly. The supported pattern is to flip an internal tag that is bound to the UserAdmin control's logoff routine. The required objects in the WinCC project are:

  1. An internal binary tag TriggerLogoff with initial value 0.
  2. A UserAdmin control (Siemens WinCC option, installed by default) placed on the start screen, configured with the Logoff button visible and its Click event bound to a Global Script action called PerformLogoff.
  3. A second internal binary tag TriggerLogoffAck that the action writes back to 0 after the logoff completes, to prevent re-entry.
' --- BEGIN PerformLogoff (Global Script action) -----------------------
Sub PerformLogoff()
    On Error Resume Next

    HMIRuntime.Trace "RDP disconnect detected at " & Now & vbCrLf

    ' Pulse the trigger tag; the UserAdmin control logoff routine reacts
    ' to the rising edge.
    HMIRuntime.Tags("TriggerLogoff").Write 1

    ' Acknowledge after a short delay so the next event can fire again.
    Dim dteAck
    dteAck = DateAdd("s", 2, Now)
    Do While Now < dteAck
        DoEvents
    Loop
    HMIRuntime.Tags("TriggerLogoff").Write 0

    HMIRuntime.Trace "Logoff routine returned at " & Now & vbCrLf
End Sub
' --- END --------------------------------------------------------------
Warning: Do not call HMIRuntime.Stop from the logoff action. Stop terminates WinCC Runtime entirely and will corrupt any open archive segments that have not yet been flushed. Use only the user-level logoff mechanism described above. A full WinCC Runtime shutdown must be performed from the operator's exit sequence, not from an unattended disconnect handler.

10. Combining Polling and Logoff in a Cyclic Action

Open the Global Script editor, right-click Actions, and create a new action of trigger type On time. Set the trigger interval to 1000 ms and assign the project function CheckRDPSession. The action maintains a high-water mark in the tag RDP_LastSeenOffset so that the same event is never processed twice.

' --- BEGIN CheckRDPSession (cyclic, 1 s trigger) ----------------------
Sub CheckRDPSession()
    Dim arrEvent, intCode, strTime

    arrEvent = ReadLastRDPSessionEvent()
    intCode  = arrEvent(0)
    strTime  = arrEvent(1)

    If intCode = "" Then Exit Sub

    ' Compare against the cached timestamp; ignore events we have already seen.
    If StrComp(strTime, HMIRuntime.Tags("RDP_LastSeenOffset").Read, _
               vbBinaryCompare) = 0 Then Exit Sub

    HMIRuntime.Tags("RDP_LastSeenOffset").Write strTime

    Select Case CLng(intCode)
        Case 26
            HMIRuntime.Trace "Event 26 (RDP disconnect) - triggering logoff" & vbCrLf
            PerformLogoff()
        Case 22
            HMIRuntime.Trace "Event 22 (RDP logoff) - no action" & vbCrLf
        Case 21
            HMIRuntime.Trace "Event 21 (RDP logon)   - no action" & vbCrLf
    End Select
End Sub
' --- END --------------------------------------------------------------

The 1-second trigger interval is configurable. Values as low as 250 ms are practical; below that, the WMI query overhead starts to dominate the polling cycle and the alternative push subscription (Section 8) should be considered.

11. State Diagram: RDP Session vs. WinCC User

The interaction between the RDP session state and the WinCC user state is shown below. The terminal node "WinCC logged off" is reachable only through Event 26 (RDP disconnect) or Event 22 (explicit RDP logoff) handled by the Global Script.

Start RDP Session Active Event 21 / 25 RDP Disconnected Event 26 WinCC User Logged Off via TriggerLogoff WinCC User Active Operator at console Stop

12. Edge Case: Multiple Concurrent RDP Sessions

A WinCC engineering station frequently has more than one RDP connection open at a time (one for the operator, one for the engineer). If the engineer disconnects while the operator is still working at the console, Event 26 fires for the engineer's session only and the operator must not be logged off. Filter the query by user name to ensure that only the operator's disconnect triggers the action.

strOperatorUser = CreateObject("WScript.Shell").ExpandEnvironmentStrings("%USERNAME%")
strQuery = "Select * From Win32_NTLogEvent Where " & _
           "Logfile='" & strLog & "' And " & _
           "EventCode='26' And " & _
           "Message LIKE '%" & strOperatorUser & "%'"

Substitute strOperatorUser at runtime with the Windows account that owns the physical console session. The variable expands to the user under which the WinCC Runtime service is running, which is normally the same as the interactive console user on a dedicated HMI station.

13. Edge Case: Channel Permission for the WinCC Service Account

When the WinCC Runtime service is configured to run under a domain account (a recommended hardening for production stations), that account must hold read access to the operational channel. The default security descriptor on the channel grants read to BUILTIN\\Administrators, BUILTIN\\Event Log Readers, and the local SYSTEM account only. Add the service account to the local Event Log Readers group, or modify the channel SD directly with wevtutil:

wevtutil sl Microsoft-Windows-TerminalServices-LocalSessionManager/Operational /ca:O:BAG:SYD:(A;;0x1;;;S-1-5-32-573)

SID S-1-5-32-573 is the well-known SID for the Event Log Readers built-in group. Restart the WinCC Runtime service after modifying the channel SD so that the cached access token is refreshed.

14. Archive and Recipe Safety Considerations

The archive writer inside WinCC Runtime runs under the WinCC Runtime service identity, not under the logged-in user. A user logoff does not interrupt the archive writer and does not require any special handling. The following scenarios, however, do require care:

  • Recipe in progress. If the operator is in the middle of writing a recipe dataset when the RDP disconnect fires, the logoff routine will discard the in-progress recipe. To prevent data loss, bind the recipe write to a Commit event that writes a snapshot to a process tag and reads it back on the next logon.
  • Long-running batch step. If a batch step has been started and is waiting for operator confirmation, the logoff routine should mark the step as Interrupted by Logoff rather than Aborted. This is achieved by writing the batch ID to an internal tag before the logoff is triggered.
  • Pending alarm acknowledgement. Pending alarms that have not been acknowledged must remain pending after the logoff; the alarm logging subsystem does not depend on the logged-in user.
Archive integrity: Do not call HMIRuntime.Stop or any function that triggers a Runtime shutdown from the disconnect handler. The WinCC archive writer must always be terminated through the dedicated WinCC exit sequence, which flushes the active archive segments in a controlled manner. A premature shutdown will leave the most recent archive segment in a state that requires a manual rebuild on the next start.

15. Field Commissioning Procedure

Use the following checklist during commissioning. Each item must be verified before the station is handed over to operations.

  1. Confirm the LocalSessionManager operational channel is enabled and that records are visible in eventvwr.msc.
  2. Define the two internal tags TriggerLogoff and RDP_LastSeenOffset in the active WinCC project.
  3. Place a UserAdmin control on the start screen and bind the Logoff button's Click event to the PerformLogoff Global Script action.
  4. Add the two project functions (ReadLastRDPSessionEvent, PerformLogoff) and one cyclic action (CheckRDPSession) to the project.
  5. Activate the project on the Runtime station.
  6. Open a second RDP session from a remote workstation and log on with any Windows account.
  7. Verify in the WinCC diagnostic log (C:\ProgramData\Siemens\Automation\WinCC\diagnose\WinCC_Sys_<n>.log) that RDP logon - no action appears within 2 seconds of the connection being established.
  8. Close the RDP window without logging off. Verify that Event 26 (RDP disconnect) - triggering logoff appears in the diagnostic log within 2 seconds and that the WinCC screen returns to the standard Logon dialog.
  9. Confirm via User Administration that the user that was active at the console is no longer listed.
  10. Repeat steps 6 - 9 with the RDP window open for 30 minutes, then closed. Verify that no spurious logoff is triggered.
  11. Repeat steps 6 - 9 with two concurrent RDP sessions (one from the operator, one from the engineer). Verify that only the operator's disconnect triggers the logoff.

16. Verification Checks

The following checks confirm correct operation without disturbing the active Runtime. They are suitable for inclusion in a periodic maintenance procedure.

Check Expected result How to verify
Event 26 is logged on RDP disconnect Record visible in eventvwr.msc within 1 s Open the LocalSessionManager operational log and inspect the latest record
Polling script runs at the configured interval Trace line written every 1 s when an event is detected Tail the diagnostic log during a test disconnect
TriggerLogoff tag toggles Tag value transitions 0 -> 1 -> 0 on each event Open the tag online view in WinCC Explorer
WinCC logoff completes Logon dialog appears within 3 s of disconnect Visual inspection during the test disconnect
Archive segments remain intact No archive rebuild required on next start Stop and start the Runtime; confirm no alarm about corrupt archives
Multiple RDP sessions only log off the operator Engineer's disconnect does not trigger the operator's logoff Filter the WMI query by user name (Section 12)

17. Troubleshooting Matrix

Symptom Likely cause Remediation
Action runs but no event is returned LocalSessionManager channel disabled Enable with wevtutil sl Microsoft-Windows-TerminalServices-LocalSessionManager/Operational /e:true
Action returns Access Denied WinCC service account not in Event Log Readers Add the account to the local Event Log Readers group
Logoff triggers on every operator action TriggerLogoff tag not reset to 0 Confirm TriggerLogoff is written back to 0 inside PerformLogoff
Logoff never triggers 1-s timer not started, or project function not assigned to the action Open the action properties and confirm the trigger interval and the project function name
WMI query takes > 2 s Channel contains millions of records; WMI repository rebuild Limit query to LocalSessionManager only, or archive and clear the channel
Event 26 fires but WinCC user remains UserAdmin control logoff routine did not run Confirm that the UserAdmin control is on the active start screen and that its Logoff button event is bound
Operator logged off when engineer disconnects Multi-session filter not applied Add the Message LIKE '%<operator>%' predicate (Section 12)
Logoff fires twice on a single disconnect Polling script reads the same event twice because the timestamp cache is wrong Cache TimeWritten, not RecordNumber, because record numbers rotate after channel clearing
Logoff not visible to operators Active project differs from the one the action is bound to Confirm that the WinCC project loaded at Runtime contains the action

18. Alternative: PowerShell Sidecar Service

On stations where the WinCC Global Script editor is restricted (e.g., lockdown images), a small PowerShell script running as a Windows service can be used as a sidecar that pushes the trigger into WinCC through the OPC UA server of WinCC. The script uses the same Microsoft-Windows-TerminalServices-LocalSessionManager/Operational channel, this time through the Get-WinEvent cmdlet:

$filter = @{
    LogName = 'Microsoft-Windows-TerminalServices-LocalSessionManager/Operational'
    Id      = 26
}
Register-WinEvent -SourceIdentifier RDPDisconnect
 -Action {
     $payload = @{ trigger = 1 } | ConvertTo-Json
     Invoke-RestMethod -Uri "http://localhost:48020/api/v1/tags/TriggerLogoff" `
                       -Method Put -Body $payload -ContentType 'application/json'
 } -FilterHashtable $filter

The Invoke-RestMethod call targets the WinCC REST interface introduced in WinCC V7.4 SP1, which exposes every internal tag at /api/v1/tags/<name>. The sidecar approach is useful when the polling script inside WinCC cannot be edited because the project is signed or supplied by a vendor.

19. Frequently Asked Questions

Which Windows Event ID most reliably indicates that the physical console is unattended?

Event ID 26 from the Microsoft-Windows-TerminalServices-LocalSessionManager/Operational log. It is written every time an RDP session is disconnected from the client side without a Windows logoff. Pair it with Event ID 22 if you also want to log off the WinCC user when an explicit Windows logoff is performed from inside the RDP window.

Can I read the Event Log without polling on a 1-second timer?

Yes. Subscribe to the channel through the EvtSubscribe API from a C action and post the event payload back to a Global Script callback through an internal tag. The end-to-end latency drops to under 100 ms. The polling approach, however, has the advantage that no C compiler, no DLL registration, and no WinCC ODK project is required.

Does the WinCC Runtime service have permission to read the LocalSessionManager log?

On Windows Server with the default service account LocalSystem, read permission is granted. On hardened installations where the WinCC Runtime service runs under a domain account, add that account to the local Event Log Readers group, or grant the read ACE directly through the channel SD using wevtutil sl Microsoft-Windows-TerminalServices-LocalSessionManager/Operational /ca:O:BAG:SYD:(A;;0x1;;;S-1-5-32-573).

What happens to WinCC Runtime archives if the user is logged off in the middle of a recipe step?

The archive write is performed by the WinCC Runtime service, not by the logged-in user. A user logoff does not interrupt the archive writer. A WinCC Runtime shutdown, on the other hand, does flush and close every archive segment and must therefore be triggered explicitly and only from the WinCC exit sequence, never from the RDP disconnect handler.

Is there a way to distinguish a remote disconnect from a local console lock?

Yes. Inspect the SessionID field inside the Event Data section of the rendered XML. If the SessionID matches the active console session (typically 1 on a dedicated HMI station), the event was triggered by the physical console. Otherwise, the event was triggered by an RDP session and should be used as the logoff trigger only when filtered against the operator's Windows account name.

Back to blog