Skip to content

PsfLauncher

Tim Mangan (MVP) edited this page Oct 8, 2022 · 15 revisions

The PsfLauncher is an executable that is provided in the PSF to act as a wrapper for any containerized application entry-point that needs the PSF. The most common use of PsfLauncher is to act at the target of Start Menu items. The PsfLauncher uses configuration from the config.json file to know what to do, which will include what target application to actually launch inside the container.

PsfLauncher is more than just for those typical start menu entry-points. It is used for:

  • Start Menu entry-points to exes in the package.
  • Start Menu entry-points to exes not in the package but must run in the container for configuration and/or plug-ins.
  • Start Menu entry-points to non exe targets, like scripts and documents and Web URLs.
  • File Type Association entry-points.
  • Protocol Handler entry-points.

32 and 64 Bit

PsfLauncher is available in both 32-bit and 64-bit varieties. The bitness of the application (Microsoft uses the overloaded term 'Architecture' for this) must match the bitness of the PsfLauncher used. This bitness rule also applies to all of the dlls (both PsfRuntime dll and Fixup dlls).

Only one Application entry in the AppXManifest per exe.

One of the limitations of the MSIX AppXManifest file is that you must have an independent Application element in the manifest for each entry-point, and no two Application elements can have the same exe. To make this work, we often make multiple copies of PsfLauncher in the package and give them different names. We recommend starting that name with the 'PsfLauncher' string to make debugging easier. Some tools just call them PsfLauncher1.exe, PsfLauncher2.exe, and so on.

High-level Overview of PsfLauncher.

The following drawing walks through the typical way that a traditional application is started from a Start Menu shortcut, and how it works with Psflauncher:

PsfLauncher

In the traditional application, the installer creates a shortcut (lnk file) and deposits into one of the start menu folders. In windows 10, this is detected and an entry in the Windows 10 Start Menu is added (along with other lnk files and entries from UAP/MSIX packages). This entry points to the target application and it is normally run outside of the container and/or PSF.

The bottom part of the image shows what happens when the app is containerized with the PSF.

The MSIX package will have an applicaiton entry-point that is added to the start menu. This will now point to a copy of the PsfLaunncher application. Then:

  1. PsfLauncher automatically loads in PsfRuntime dll, and this dll finds and reads the Application section of the config.json file to find the matching entry for the name of our PsfLauncher file.
  2. That configuration will cause the runtime to start the idenfied target application and
  3. inject another copy of PsfRuntime into that new process.
  4. The copy of PsfRuntime in the new process will also locate and read the config.json file, but in this case looking for a matching entry from the Process section of that file, and
  5. The PsfRutime in that process will inject and configure any additional Fixup dlls defined in that Process configuration.

Identifying the correct Application in the config.json

The Applications section of the json identifies applications by matching to the Application Id field of an application in the AppXManifest file and not necessarily the actual name of the exe process. While other third party tooling may use different naming schemes for the creation of the Id field in the AppXManifest file, the Microsoft MSIX Packaging Tool uses a munging of the process name that you can see in this partial AppXManifest file:

<Package ...>
   ...
   <Applications>
    <Application Id="PSFLAUNCHEROne" Executable="PsfLauncher1.exe" EntryPoint="Windows.FullTrustApplication">
      <uap:VisualElements BackgroundColor="transparent" DisplayName="7-Zip File Manager" Square150x150Logo="Assets\SevenZFM-Square150x150Logo.png" Square44x44Logo="Assets\SevenZFM-Square44x44Logo.png" Description="7-Zip File Manager">
        <uap:DefaultTile Wide310x150Logo="Assets\SevenZFM-Wide310x150Logo.png" Square310x310Logo="Assets\SevenZFM-Square310x310Logo.png" Square71x71Logo="Assets\SevenZFM-Square71x71Logo.png" />
      </uap:VisualElements>
      ...
    </Application>
    <Application Id="PSFLAUNCHERTwo" Executable="PsfLauncher2.exe"  EntryPoint="Windows.FullTrustApplication">
      ...
    </Application
  </Applications>
</Package>

The config.json file for this package might have an applications section such as this:

"applications": [
    {
      "id": "PSFLAUNCHEROne",
      "executable": "VFS\\ProgramFilesX64\\7-Zip\\7zFM.exe",
      "arguments": "",
      "workingDirectory": "VFS\\ProgramFilesX64\\7-Zip"
    },
    {
      "id": "PSFLAUNCHERTwo",
      "executable": "VFS\\ProgramFilesX64\\7-Zip\\7-zip.chm",
      "arguments": "",
      "workingDirectory": ""
    }
  ],

Although Start Menu items in Windows 10 do not currently support arguments, Microsoft is considering adding them. Until then, the launcher is the only way to specify command line arguments to the target application, and this is done in the arguments field of the json configuration of our launcher.

Similarly, the Start Menu items in Windows 10 do not support specification of a working directory of the target application and will always start the app with the System32 folder as the working directory. This may be overridden in the workingDirectory field of the json configuration for our launcher. To make this work like the native lnk files, if the workingDirectory item of the json configuration is left blank, the folder that contains the ultimate target file will be used.

Support for Process launch and Shell launch

Although Windows 10 Start Menu items may not be files, by using PsfLauncher we can use the shell launch method to get an appropriate client side application to open the file. This is shown in the previous json for PSFLAUNCHERTWO. When the launcher sees that the executable file is not an exe file, it will look up the default program for that file type on the end-user system. It will then use a technique to perform a shell launch and force the resulting program to run inside this container, if possible. Some of the shell launch apps might be AppX based, so starting those inside this container won't be possible for those and the app will be externally started for the file instead. This might not always work, but it is the best we can do.

When the target executable file in the config.json configuration for the application is not an exe, you must add one of two special powershell script files that act as a secondary helper launcher for the associated command that will be shell launched using the most appropriate FTA for that file type.

File Types Required supporting script
exe None
cmd or bat StartMenuCmdWrapper.ps1
all other non exe files StartMenuShellLaunchWrapper.ps1

These scripts should be placed wherever the PsfLauncher is placed, typically in the root folder of the package. They will ensure that the resulting process will run inside the container and will be eligible for PSF intervention (dependent on the process section of the file). Keep in mind that if the other applications in your package are 32 bit and the package is run on an x64 system, the fta related process might be 64 bit (will always be 64 bit for the cmd/bat file types), thus the 64-bit version of PsfRuntime and any fixup dlls would be needed as well.

About PsfLauncher and Processes needing elevation

When the target application contains an internal or external manifest file requesting elevation (such as RequireAdministrator or HighestAvailable), the PSFLauncher must be elevated first. The PsfLauncher does not include an internal manifest, but you may add an external manifest file in the package to request that elevation. See the "About PsfLauncher and Processes needing elevation" section of PsfLauncher Developer Documentation for more information.

Although often not required, you are supposed to mark the MSIX package with the runElevated capabilities setting in the AppXManifest.

Configuring PsfLauncher to start a Monitor

A 'monitor' is a program to be started by the launcher before the target program is started. The most common use of this launcher feature is to start PsfMonitor because the target process will be using PsfTraceFixup. But this monitor program may be any secondary program that you need to start. The launcher starts this program and waits a few seconds for it to initialize before starting the target app.

Here is an example of the PsfLauncher configuration for that use:

"applications": [
  {
      "id": "PSFLAUNCHERSixFour",
      "executable": "PrimaryApp.exe",
      "arguments": "/AddedArg"
      "workingDirectory": "",
      "monitor": {
          "executable": "PsfMonitor.exe",
          "arguments": "",
          "asadmin": true,
      }
  }
],

Scripting

The PsfLauncher also supports the configuration of scripts that may be run in conjunction of the target application. There are two triggers that are available to run a script, and up to one script may be run at each trigger. These events are:

  • StartScript - runs prior to starting the target application.
  • EndScript - runs after the target application exits.

The script to be run must be a PowerShell PS1 file, and it will be run by a PSF provided wrapper PowerShell script so that the requested PS1 script will be run inside the container.

If a start script is requested, it will begin before the target application. The script configuration may request that the application start immediately ( "wait"=false ) or it can wait for the script to complete ( "wait"=true ). If a wait is requested, a maximum wait time field, in milliseconds may be given. Note that setting the wait with a timeout of 0 means to wait for 0 milliseconds, not wait forever. If a wait is requested, there is an additional rollback field set to true means that the target application will not be started unless the script returns with a exit code of 0. There is no list for alternative exit codes.

If an end script it requested, it will begin after the target application exits. No wait is possible on this script and an error code for rollback is not supported.

Both scripts may also have arguments, and an option to show or hide the script window is also provided. While the json syntax also supports a field to specify if the script should run inside or outside the container, this field is currently ignored, and all scripts will run inside the container.

Here is an example of an application configuration that uses scripts.

"applications": [
    {
      "id": "PSFLAUNCHEROne",
      "executable": "Sample.exe",
      "workingDirectory": "",
      "stopOnScriptError": false,
      "scriptExecutionMode": "-ExecutionPolicy Bypass",
	  "startScript":
	  {
		"waitForScriptToFinish": true,
		"timeout": 30000,
		"runOnce": true,
		"showWindow": false,
		"scriptPath": "PackageStartScript.ps1",
		"waitForScriptToFinish": true
		"scriptArguments": "%MsixWritablePackageRoot%\\VFS\\LocalAppData\\Vendor",
	  },
	  "endScript":
	  {
		"scriptPath": "\\server\\scriptshare\\RunMeAfter.ps1",
		"scriptArguments": "ThisIsMe.txt"
	  }
    }
  ],

Special Variables available in the PsfLauncher configuration

It is sometimes necessary to specify file paths in the various fields of the application record that configures PsfLauncher. This can include the arguments field for the target application, argument field for the monitor, or argument field of scripts.

The two special variables are:

  • %MsixPackageRoot% - which resolves to the location of the package root (typically "C:\Program Files\WindowsApps\PackageFamilyName").
  • %MsixWritablePackageRoot% - which resolves to the location that the FileRedirectionFramework will redirect for copy-on-access and copy-on-write (typically "%LocalAppData%\packages\PackageFamilyName\LocalCache\Microsoft\WritablePackageRoot").

Controlling process injections

It is the PsfRuntime component that intercepts the CreateProcess functions to control the start of new processes and (in most cases) inject the PsfRuntime into child processes that need it. But there are cases where this shouldn't be done:

  • PsfRuntime is automatically loaded anytime a PsfLauncher is run, so we don't never need to perform an injection into one of those processes.
  • Child processes that are scheduled to run outside of the container can't work with the PSF to we don't want the launcher injected into those.
  • Certain console exe processes seem to be incompatible with the PSF as well. We don't (yet) know why.

After PsfRuntime is injected and the new child process starts, this injected runtime uses the Processes section of the config.json file to determine if any fixups are required, and if so it loads them up.

Although the original intent of the PSF was to be very prescriptive and specifically list everything that is proved to be required but nothing more, this approach proved to difficult and the more common approach is to define the json with a process configuration for the package using the RegEx wildcard string for the executable field which contains what we generally want to happen, and also add specific process names where we list different options needed. As the processes are matched serially, this means that the wildcard process match must be the last one in the list.

Below is an example of the a typical configuration whereby any child process except for the ones listed have the FRF. The three named processes are being excluded for the reasons stated in the bullets above. The CreateProcess intercept will not inject a PsfRuntime into the new child process if no fixups are listed for the process.

 {
     "applications" [
         ...
     ],
 
    "processes": [
        {
          "executable": "^PsfLauncher.*"
        },
        {
          "executable": "^[Pp]ower[Ss]hell.*"
        },
        {
          "executable": "^notme.exe$"
        },
        {
            "executable": ".*",
            "fixups": [
                {
                    "dll": "FileRedirectionFixup.dll",
                    config {
                        ...
                    }
                }
            ]
        }
    ]
}

Debugging PsfLauncher

Unlike the fixup dlls, using the release build of PsfLauncher, as well as PsfRuntime, will always result in debugging output tbug builhat may be used to help you understand what is wrong with launching issues. You may view this detail in any tool that captures the debug port, but for the IT Pro the easiest to use is probably the Microsoft SysInternals tool DbgView.

By selecting the Debug build of PsfLauncher and/or PsfRuntime you can enable the additional details available. These additional details tend to be more helpful for someone debugging the PsfLauncher itself rather than debugging their package.

For the IT Pro that has coding skills, You can also enable the waitfordebugger option in the PsfLauncher config.json application entry. This option (which does not require including the WairForDebugger dll file in the package) will place a break point at the start of PsfLaunher such that PsfLauncher/PsfRuntime code debugging may be performed. (The WaitForDebugger.dll file is only required to be added to the package if the dll is listed as a fixup in the process section of the config.json.

The following is an example of a simple application launch that used the debug builds:

Line# Timestamp [ProcessId] Details
0001 0.00000000 [18908] PsfRuntime: In DllMain Pid=18908 Tid=12188
0002 0.00206120 [18908] g_PackageFullName=NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e
0003 0.00208790 [18908] g_PackageFamilyName=NotePadPlusPlus_xwfzvwzp69w2e
0004 0.00212760 [18908] g_ApplicationUserModelId=NotePadPlusPlus_xwfzvwzp69w2e!PSFLAUNCHEROne
0005 0.00214830 [18908] g_ApplicationId=PSFLAUNCHEROne
0006 0.00219470 [18908] g_PackageRootPath=C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e
0007 0.00245140 [18908] g_FinalPackageRootPath=\?\C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e
0008 0.00266270 [18908] g_CurrentExecutable=C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e\VFS\ProgramFilesX64\Notepad++\PsfLauncher1.exe
0009 0.00285510 [18908] Config.json not found in root of package C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e, look elsewhere.
0010 0.00293260 [18908] Config.json found in executable folder of package C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e
0011 0.00345260 [18908] Processes config match=^PsfLauncher.*
0012 0.00424960 [18908] PSFLauncher started.
0013 0.00461780 [18908] Json Application match against id=PSFLAUNCHEROne
0014 0.00777510 [18908] Arguments Original=
0015 0.00780940 [18908] Arguments Devariablized=
0016 0.00783460 [18908] Arguments after ArgumentVirtualization=
0017 0.00785460 [18908] Process Launch: =C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e\VFS\ProgramFilesX64\Notepad++\notepad++.exe
0018 0.00787580 [18908] Arguments: =
0019 0.00789770 [18908] Working Directory: =C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e\VFS\ProgramFilesX64\Notepad++
0020 0.00794210 [18908] [10001] CreateProcessFixup: commandline="notepad++.exe"
0021 0.01650760 [18908] [10001] CreateProcessFixup: Attempt injection into 24836 using C:\Program Files\WindowsApps\NotePadPlusPlus_8.1.2.0_x64__xwfzvwzp69w2e\VFS\ProgramFilesX64\Notepad++\PsfRuntime64.dll
0022 0.01675500 [18908] [10001] CreateProcessFixup: Injected PsfRuntime64.dll into PID=24836
0023 0.02718130 [24836] PsfRuntime: In DllMain Pid=24836 Tid=5660

Information about the fields in the debug output:

  • Line Num is the line number of the debug output capture.
  • Timestamp is the offset time relative to when DebugView capture was enabled.
  • [ProcessId] is the process Id of the process that emitted the debugging output. In the example above, the [18909] value represents the PsfLauncher1.exe process and [24836] represents the child Notepad++.exe process.
  • Details contains additional details about the debug event. Often this will start with further identifying details. For example:
  • A Square bracket number at the start of the details in an API Intercept event identifier. PsfRuntime incident identifiers start at 10001 on the first instance of PsfRuntime intercepting a secondary process launch request (this identifier would increment if the same process later tried to start another process. Each module has its own starting point that is a multiple of 10000. PsfLauncher and the rest of PsfRuntime do not emit these event identifiers.
  • A second square bracket number appears during FindNextFile operations. When this appears, the second number is the new incident number, and the first number is the incident number of the FindFirstFile or FindFirstFileEx that is related.
  • The module name might be indicated.
  • The remaining part of the details are specific to the debug situation.

Some additional details about the sample debug output shown above:

  • Lines 1-8 are emitted by PsfRuntime initialization, which occurs at the beginning of PsfLauncher starting. The g_ entries are variables obtained from the container which are used throughout the PSF.
  • Lines 9-10 are emitted by PsfRuntime initialization as it tries to find the config.json file. It will search the entire package for the file if necessary.
  • Line 11 is the PsfRuntime initialization reading the config.json processes section to determine if it should be loading any additional Psf dlls. We typically include an entry in the processes section for all PsfLaunchers to prevent additional dll loading. If fixup dlls were requested, we would now see them being loaded.
  • Line 12 is the start of PsfLauncher code after dll initialization.
  • Lines 13-19 are the PsfLauncher reading the config.json applications section to determine the configuration details for the child process to be launched. Potentially, although not in this example, there may also be a monitor program to start and a start script.
  • Lines 20-22 are the PsfRuntime intercept of a process launch for the target child process (note the identifying event id). Note that it starts the child processes suspended, allowing this PsfRuntime to inject a copy of PsfRuntime and potentially fixup dlls into the new process before letting the child process resume running.
  • Line 23 is the start of the new process initializing the injected copy of PsfRuntime.

More Information

More Information on PsfLauncher may be found in the PsfLauncher Developer Documentation page.