App Launcher: Universal RDS Remote Application¶
App Launcher starts a Windows application as an RDS RemoteApp and signs in to it by typing credentials supplied by the 12Port Access Broker. The user never sees or handles the password: the broker injects it into the session, the session is recorded, and user input stays blocked until the sign-in sequence finishes.
A single executable, app-launcher.exe, serves any number of applications. Each application is described by its own section in app-launcher.ini, and each asset selects a section by name. Adding a new application means adding an INI section; nothing needs to be recompiled.
App Launcher drives applications with keystrokes and window detection. It works with any application that accepts keyboard input: desktop clients, terminal emulators such as IBM i Access Client Solutions 5250, and web login pages in Microsoft Edge.
How it works¶
- A user opens an RDS Remote Application asset in 12Port.
- The Access Broker starts
app-launcher.exeon the RDS server and passes the asset fields listed in Remote Arguments. The launcher requests them through the session's control channel; they are not written to disk. - The launcher reads the
Scriptfield and loads the INI section of that name. - It starts the application from the section's
exeandargs. - It runs the section's
script: waits for windows, types field values, presses keys. - It unblocks user input. The user now works in the signed-in application.
- Optionally, it waits for the application to close and then cleans up: it ends leftover processes and logs off the RDS session.
Requirements¶
- Windows Server with the Remote Desktop Session Host role and a RemoteApp collection.
- The target application installed on the RDS server.
- An RDS Host Server asset in 12Port for that server.
- For web portals: Microsoft Edge.
Installation¶
1. Get the files¶
1. Download Required Files
* Executable:
https://bin.12port.com/product/remote-apps/app-launcher/app-launcher.exe
* Configuration File (.ini):
https://bin.12port.com/product/remote-apps/app-launcher/app-launcher.ini
Source file:
https://bin.12port.com/product/remote-apps/ztna.au3
Note: Theztna.au3source file contains shared code used by all RDS Remote Application scripts. This file is required for compiling modified or custom created RDS app scripts. Place it in the parent folder of the script source, for exampleapp-launcher.au3; the source includes it as..\ztna.au3.
* Place both the Executable (.exe) and Configuration (.ini) files in the same directory on the RDS Server where the Remote Application will be published. For example: C:\12Port-RDSApps\app-launcher.
2. Place the files¶
Copy both files into the same folder on every RDS server in the collection, for example:
The launcher always reads the INI from its own folder.
Protect the folder. The INI decides which programs run in user sessions. Grant write access to administrators only; users need read and execute access.
3. Publish the RemoteApp¶
In an elevated PowerShell session on the RD Connection Broker, replacing QuickSessionCollection with your RDS collection name:
New-RDRemoteApp -CollectionName "QuickSessionCollection" `
-Alias "app-launcher" `
-DisplayName "app-launcher" `
-FilePath "C:\12Port-RDSApps\app-launcher\app-launcher.exe" `
-ShowInWebAccess $true `
-CommandLineSetting Allow
-CommandLineSetting Allow is required: it lets the Access Broker pass connection parameters to the launcher. Make sure -FilePath matches where you copied the executable.
You publish the launcher once. Every application in the INI is reached through this one RemoteApp.
Configuring applications¶
INI structure¶
[launcher]
; global settings, also defaults for every application section
[my-app]
; one application
[my-app-variant]
base=my-app
; a variation of my-app; only what differs is listed here
[launcher]is reserved for global settings. It can't be used as an application name.- Every other section is an application. Its name is what the asset's
Scriptfield contains. (The asset fieldScriptpicks a section; thescript=key inside that section holds the sign-in sequence.) base=<section>copies every key the section doesn't set itself from another section. A base section can have its ownbase, up to 10 levels deep.- Lookup order for any key: the application section, then its
basesections, then[launcher], then the built-in default.
Application settings¶
| Key | Required | Description |
|---|---|---|
exe | Yes | Program to start: a full path, or a bare name such as msedge.exe, which is looked up in the Windows App Paths registry. |
args | No | Command-line arguments. {var:Field} is replaced with the asset field value (see Arguments). |
script | No | Sign-in sequence (see Script reference). |
workdir | No | Working directory. Defaults to the folder of exe. |
kill-existing | No | Process name, such as msedge.exe, to close in the current user's session before starting. Processes in other users' sessions are never touched. |
exit-watch | No | Window to watch after sign-in. When no window matching it is left, exit-action runs. See Session exit. |
exit-action | No | kill, logoff, or kill,logoff. Applies only with exit-watch. |
disconnect-wait | No | Milliseconds a session may stay disconnected before exit-action runs. Default 0: act immediately. See Session exit. |
Global settings and timing¶
These go in [launcher] and can be overridden in any application section.
| Key | Default | Description |
|---|---|---|
debug | false | Shows the received parameters at startup (secret fields masked) and sends each script step to the 12Port server log. |
winwait-timeout | 60000 | Default timeout, in milliseconds, for {winwait}, {ifwin} and {abortif}. |
focus-wait | 500 | Pause, in milliseconds, after a window is activated. |
send-key-delay | 40 | Delay, in milliseconds, between typed characters. Increase it for applications that drop characters. |
send-key-down-delay | 5 | How long, in milliseconds, each key is held down. |
Arguments¶
args can reference any asset field passed in Remote Arguments:
For security, a value substituted into args must not contain spaces or double quotes. Otherwise the launcher refuses to start and shows an error. This stops an asset field from adding extra command-line switches. URLs must therefore be URL-encoded, for example %20 instead of a space.
Quote a placeholder in args when the program expects a quoted value: --new-window "{var:Host}".
Script reference¶
A script is a list of actions. Each action is enclosed in braces, and actions are separated by commas. They run left to right.
Actions¶
| Action | Description |
|---|---|
{var:Field} | Types the value of an asset field exactly as stored; special characters are typed literally. |
{text:literal} | Types fixed text exactly as written, for example a menu choice: {text:21}. Commas are allowed inside: {text:a, b}. |
{TAB}, {ENTER}, {ESC}, {SPACE}, {BACKSPACE}, {DEL}, {INS}, {HOME}, {END}, {PGUP}, {PGDN}, {UP}, {DOWN}, {LEFT}, {RIGHT}, {F1} … {F24} | Presses the key. Add a count to repeat: {TAB 3}. |
{send:keys} | Sends keys in AutoIt Send notation, for key combinations: {send:+{TAB}} is Shift+Tab and {send:^a} is Ctrl+A. |
{wait:ms} | Pauses for the given number of milliseconds. |
{winwait:window} or {winwait:window,ms} | Waits until the window exists, then activates it. Stops the script if it doesn't appear in time. |
{winmax:window} or {winmax:window,ms} | Waits for the window like {winwait}, then maximizes it. In a RemoteApp session the window fills the user's screen. Use it instead of a launch option: many applications, including Java applications and Edge, ignore the "start maximized" request and restore their own saved size. |
{winactivate:window} | Brings an existing window to the front. Stops the script if the window isn't found within 2 seconds. |
{ifwin:window,ms} … {endif} | Runs the enclosed actions only if the window appears within the time; otherwise skips to {endif}. If it appears, it is activated. Blocks can be nested. |
{abortif:window,ms} | Waits up to ms. If the window appears, the script stops; otherwise it behaves like {wait:ms}. |
Field names in {var:} are case-sensitive and must match the asset field names exactly.
Window specifications¶
Wherever an action takes a window, you can use:
| Form | Matches |
|---|---|
Sign in | Any window whose title contains this text |
[CLASS:SunAwtFrame] | Windows of this window class |
[REGEXPTITLE:^A - .*] | Title matching a regular expression |
Window classes are more reliable than titles when the title changes, as with browser tabs. To find a window's title and class, run AutoIt Window Info (Au3Info.exe, included with AutoIt) on the RDS server and point it at the window.
Writing reliable scripts¶
- Wait for windows instead of sleeping.
{winwait:[CLASS:...]}continues as soon as the window is there and fails clearly when it isn't. A fixed{wait:20000}is slow when the application is fast and too short when it is slow. - Use
{abortif}after every submit. It replaces the pause you would put there anyway. If sign-in is rejected, the script stops instead of typing the next field, possibly a password, into an error dialog or an unrelated window. - Use
{ifwin}for steps that don't always appear, such as a sign-on dialog shown only when no credentials are cached. - Keep a
{wait}where nothing on the screen can be detected. Terminal emulators draw sign-on screens and menus inside one window, so the screen content can't be watched.{wait}is the only option there.
When a script stops¶
If an action fails (window timeout, missing field, unknown action, or {abortif} triggered), the launcher:
- stops typing,
- writes the reason to the 12Port server log,
- unblocks user input, so the user can finish signing in manually,
- continues with session exit handling.
Session exit¶
Without exit-watch, the launcher exits as soon as the script finishes, and the application runs on by itself. This suits most applications.
With exit-watch, the launcher stays in the background. It notes which processes own the matching windows. It runs exit-action when either of these happens:
- no window matching
exit-watchis left, or - the RDS session is disconnected, for example because the user closed the remote session window without closing the application, and stays disconnected for
disconnect-waitmilliseconds (default0, immediately).
Acting on disconnect matters for security. A disconnected session keeps running, and the next connection by the same account reattaches to it. Without this, the next user of that account would find the application still signed in. Set disconnect-wait, for example to 60000, only if users must be able to resume after short network interruptions.
The actions are:
killends the processes that owned the watched windows and the originally started process, together with their child processes.logoffforces the RDS session to log off, even if an application refuses or hangs.
Use this for applications that leave background processes running after their window closes, so the RDS session doesn't stay open. Some Java applications behave this way. With logoff, a disconnected session is logged off rather than left waiting.
Disconnect handling works only while the launcher runs, that is, only with exit-watch. As a server-wide safeguard, also set the Remote Desktop Session Host policy Set time limit for disconnected sessions, so Windows logs off disconnected sessions even when no launcher is watching.
Watch the main window, not a dialog. The launcher acts only once the main window is gone, so a user who cancels an "Are you sure?" prompt keeps working.
Asset configuration¶
- Create an asset of type RDS Remote Application.
- Set Remote App to
||app-launcher, the published alias prefixed with||. - The asset type has a text field named Script, hidden by default. Unhide it and set it to the INI section name, for example
acs-5250. - Add any other fields the section uses. Secrets such as passwords, TOTP secrets or secondary credentials go in fields marked Secured. For TOTP, add a field of type TOTP with Algorithm SHA1.
- Set Remote Arguments to the comma-separated list of fields the section uses, including
Script, for exampleScript,Host,Port,User,Password. Only listed fields reach the launcher. - Under Member Assets, add the RDS Host Server asset with the Member role. Sessions can't start without it.
Never put credentials in the INI file. Every credential the script types must come from a secured asset field through
{var:...}. The INI is a plain text file on the RDS server, readable by its users.
Examples¶
Web login page in Microsoft Edge¶
Remote Arguments: Script,Host,User,Password,TOTP, where Host is the login page URL.
[web-portal]
exe=msedge.exe
args=--new-window "{var:Host}" --no-first-run --disable-features=RestoreSessionState
kill-existing=msedge.exe
script={winwait:[CLASS:Chrome_WidgetWin_1]},{wait:2000},{var:User},{wait:1000},{TAB},{wait:1000},{var:Password},{wait:1000},{ENTER},{wait:1000},{var:TOTP},{wait:1000},{ENTER}
[web-portal-no-mfa]
base=web-portal
script={winwait:[CLASS:Chrome_WidgetWin_1]},{wait:2000},{var:User},{wait:1000},{TAB},{wait:1000},{var:Password},{wait:1000},{ENTER}
Before using Edge, sign in to the RDS server with the Host Server account and turn off: - Show tab search, at edge://settings/appearance, which can take keyboard focus, - Offer to save passwords, which can interrupt the sequence.
IBM i Access Client Solutions 5250¶
Remote Arguments: Script,Host,Port,User,Password
[acs-5250]
exe=C:\Users\Public\IBM\ClientSolutions\Start_Programs\Windows_x86-64\acslaunch_win-64.exe
args=/plugin=5250 /system={var:Host} /port={var:Port} /wide /ssl=1
exit-watch=[CLASS:SunAwtFrame]
exit-action=kill,logoff
script={winwait:[CLASS:SunAwtFrame]},{ifwin:Signon to IBM i,15000},{var:User},{TAB},{var:Password},{ENTER},{abortif:Error Message,6000},{endif},{winactivate:[CLASS:SunAwtFrame]},{var:User},{TAB},{var:Password},{ENTER},{abortif:Error Message,3000}
The script signs in twice: first in the Java Signon to IBM i dialog, if ACS shows it, then on the 5250 sign-on screen.
To use a specific Java runtime, put -vm in front of the other arguments:
Use -vm if ACS fails to start with the system Java. A missing runtime library, for example, shows up as a Java UnsatisfiedLinkError in the debug log.
Variation with a fixed port¶
HMC 5250 console with a second set of credentials¶
Remote Arguments: Script,Host,Port,User,Password,HmcUser,HmcPassword, with HmcPassword as a secured field. Create the HmcUser and HmcPassword fields in the RDS Remote Application asset type.
[acs-5250-hmc]
base=acs-5250
script={winwait:[CLASS:SunAwtFrame]},{wait:20000},{text:21},{ENTER},{wait:500},{var:HmcUser},{TAB},{var:HmcPassword},{ENTER},{abortif:Error Message,1000},{text:1},{ENTER},{wait:1000},{text:1},{ENTER},{wait:1000},{var:User},{TAB},{var:Password},{ENTER}
The {wait} values depend on how quickly the host responds. Tune them per environment.
Troubleshooting¶
Turn on diagnostics¶
- Set
debug=truein[launcher], or only in the affected application section. - In 12Port, set these loggers to Debug:
com.otna.proxy.rdp.pipeline.handler.RemoteAgentHandlerfor sessions through the RDP proxy,com.otna.gw.filter.RemoteAgentFilterfor browser (HTML5) sessions.- Start a session. The launcher shows the received parameters for 10 seconds, and the server log records each script step, for example
step 7: abortif:Error Message,6000. Typed values are never logged.
In the startup window, fields whose names contain pass, totp, secret, token, key or pin are shown as ****. Every other field is shown in clear text, so turn debug off again when you're done.
Common problems¶
| Symptom | Cause and fix |
|---|---|
| "Application '…' is not configured on this server." | The asset's Script field is empty, misspelled, or Script is missing from Remote Arguments. Section names must match exactly. |
| "Cannot start '…': asset field 'X' contains whitespace or quotes" | A value used in args contains a space or ". Fix the field value, or URL-encode it. |
| "Cannot start '…': missing asset field 'X' used in args" | Add X to the asset's Remote Arguments. |
Log: window '…' did not appear | The application started slowly or the window spec doesn't match. Check the title or class with AutoIt Window Info; increase the timeout with {winwait:spec,ms}. |
Log: missing asset field 'X' | A {var:X} field isn't in Remote Arguments, or its name differs in case. |
Log: unknown action '{…}' | Typo in the script, or an unsupported key name. Use {send:…} for key combinations. |
| Characters are missing or out of order | The application can't keep up. Raise send-key-delay, for example to 80. |
| Keystrokes go to the wrong window | Add {winwait} or {winactivate} before typing, and {abortif} after each submit. |
| The RDS session stays open after the user closes the application | Background processes are still running. Add exit-watch for the main window and exit-action=kill,logoff. |
Testing a section without the broker¶
You can run a section directly on the RDS server. Encode the parameters as Base64 Name=Value lines and pass them as the first argument:
$p = "Script=acs-5250`nHost=myhost.example.com`nPort=992`nUser=TESTUSER`nPassword=..."
$b = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($p))
& C:\12Port-RDSApps\app-launcher\app-launcher.exe $b
Base64 is not encryption. Values passed this way can be read in the process list and in your console history. Test only with throwaway accounts, and clear your PowerShell history afterwards.
Limitations¶
- The launcher works through keystrokes and window detection. It can't read text inside a window. On terminal screens and single-window applications, sign-in errors shown inside the window aren't detected; only separate dialog windows are.
- Keystrokes go to the window that has focus. Scripts should activate the target window before typing.
- One script runs per session. The launcher doesn't sign in again after a session times out inside the application.