// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package winutil
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"unicode/utf16"
"unsafe"
"github.com/dblohm7/wingoes"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
var (
// ErrDefunctProcess is returned by (*UniqueProcess).AsRestartableProcess
// when the process no longer exists.
ErrDefunctProcess = errors . New ( "process is defunct" )
// ErrProcessNotRestartable is returned by (*UniqueProcess).AsRestartableProcess
// when the process has previously indicated that it must not be restarted
// during a patch/upgrade.
ErrProcessNotRestartable = errors . New ( "process is not restartable" )
)
// Implementation note: the code in this file will be invoked from within
// MSI custom actions, so please try to return windows.Errno error codes
// whenever possible; this makes the action return more accurate errors to
// the installer engine.
const (
_RESTART_NO_CRASH = 1
_RESTART_NO_HANG = 2
_RESTART_NO_PATCH = 4
_RESTART_NO_REBOOT = 8
)
func registerForRestart ( opts RegisterForRestartOpts ) error {
var flags uint32
if ! opts . RestartOnCrash {
flags |= _RESTART_NO_CRASH
}
if ! opts . RestartOnHang {
flags |= _RESTART_NO_HANG
}
if ! opts . RestartOnUpgrade {
flags |= _RESTART_NO_PATCH
}
if ! opts . RestartOnReboot {
flags |= _RESTART_NO_REBOOT
}
var cmdLine * uint16
if opts . UseCmdLineArgs {
if len ( opts . CmdLineArgs ) == 0 {
// re-use our current args, excluding the exe name itself
opts . CmdLineArgs = os . Args [ 1 : ]
}
var b strings . Builder
for _ , arg := range opts . CmdLineArgs {
if b . Len ( ) > 0 {
b . WriteByte ( ' ' )
}
b . WriteString ( windows . EscapeArg ( arg ) )
}
if b . Len ( ) > 0 {
var err error
cmdLine , err = windows . UTF16PtrFromString ( b . String ( ) )
if err != nil {
return err
}
}
}
hr := registerApplicationRestart ( cmdLine , flags )
if e := wingoes . ErrorFromHRESULT ( hr ) ; e . Failed ( ) {
return e
}
return nil
}
type _RMHANDLE uint32
// See https://web.archive.org/web/20231128212837/https://learn.microsoft.com/en-us/windows/win32/rstmgr/using-restart-manager-with-a-secondary-installer
const _INVALID_RMHANDLE = ^ _RMHANDLE ( 0 )
type _RM_UNIQUE_PROCESS struct {
PID uint32
ProcessStartTime windows . Filetime
}
type _RM_APP_TYPE int32
const (
_RmUnknownApp _RM_APP_TYPE = 0
_RmMainWindow _RM_APP_TYPE = 1
_RmOtherWindow _RM_APP_TYPE = 2
_RmService _RM_APP_TYPE = 3
_RmExplorer _RM_APP_TYPE = 4
_RmConsole _RM_APP_TYPE = 5
_RmCritical _RM_APP_TYPE = 1000
)
type _RM_APP_STATUS uint32
const (
//lint:ignore U1000 maps to a win32 API
_RmStatusUnknown _RM_APP_STATUS = 0x0
_RmStatusRunning _RM_APP_STATUS = 0x1
_RmStatusStopped _RM_APP_STATUS = 0x2
_RmStatusStoppedOther _RM_APP_STATUS = 0x4
_RmStatusRestarted _RM_APP_STATUS = 0x8
_RmStatusErrorOnStop _RM_APP_STATUS = 0x10
_RmStatusErrorOnRestart _RM_APP_STATUS = 0x20
_RmStatusShutdownMasked _RM_APP_STATUS = 0x40
_RmStatusRestartMasked _RM_APP_STATUS = 0x80
)
type _RM_PROCESS_INFO struct {
Process _RM_UNIQUE_PROCESS
AppName [ 256 ] uint16
ServiceShortName [ 64 ] uint16
AppType _RM_APP_TYPE
AppStatus _RM_APP_STATUS
TSSessionID uint32
Restartable int32 // Win32 BOOL
}
// RestartManagerSession represents an open Restart Manager session.
type RestartManagerSession interface {
io . Closer
// AddPaths adds the fully-qualified paths in fqPaths to the set of binaries
// that will be monitored by this restart manager session. NOTE: This
// method is expensive to call, so it is better to make a single call with
// a larger slice than to make multiple calls with smaller slices.
AddPaths ( fqPaths [ ] string ) error
// AffectedProcesses returns the UniqueProcess information for all running
// processes that utilize the binaries previously specified by calls to
// AddPaths.
AffectedProcesses ( ) ( [ ] UniqueProcess , error )
// Key returns the session key associated with this instance.
Key ( ) string
}
// rmSession encapsulates the necessary information to represent an open
// restart manager session.
//
// Implementation note: rmSession methods that return errors should use
// windows.Errno codes whenever possible, as we call them from the custom
// action DLL. MSI custom actions are expected to return windows.Errno values;
// to ensure our compliance with this expectation, we should also use those
// values. Failure to do so will result in a generic windows.Errno being
// returned to the Windows Installer, which obviously is less than ideal.
type rmSession struct {
session _RMHANDLE
key string
logf logger . Logf
}
const _CCH_RM_SESSION_KEY = 32 // (excludes NUL terminator)
// NewRestartManagerSession creates a new RestartManagerSession that utilizes
// logf for logging.
func NewRestartManagerSession ( logf logger . Logf ) ( RestartManagerSession , error ) {
var sessionKeyBuf [ _CCH_RM_SESSION_KEY + 1 ] uint16
result := rmSession {
logf : logf ,
}
if err := rmStartSession ( & result . session , 0 , & sessionKeyBuf [ 0 ] ) ; err != nil {
return nil , err
}
result . key = windows . UTF16ToString ( sessionKeyBuf [ : _CCH_RM_SESSION_KEY ] )
return & result , nil
}
// AttachRestartManagerSession opens a connection to an existing session
// specified by sessionKey, using logf for logging.
func AttachRestartManagerSession ( logf logger . Logf , sessionKey string ) ( RestartManagerSession , error ) {
sessionKey16 , err := windows . UTF16PtrFromString ( sessionKey )
if err != nil {
return nil , err
}
result := rmSession {
key : sessionKey ,
logf : logf ,
}
if err := rmJoinSession ( & result . session , sessionKey16 ) ; err != nil {
return nil , err
}
return & result , nil
}
func ( rms * rmSession ) Close ( ) error {
if rms == nil || rms . session == _INVALID_RMHANDLE {
return nil
}
if err := rmEndSession ( rms . session ) ; err != nil {
return err
}
rms . session = _INVALID_RMHANDLE
return nil
}
func ( rms * rmSession ) Key ( ) string {
return rms . key
}
func ( rms * rmSession ) AffectedProcesses ( ) ( [ ] UniqueProcess , error ) {
infos , err := rms . processList ( )
if err != nil {
return nil , err
}
result := make ( [ ] UniqueProcess , 0 , len ( infos ) )
for _ , info := range infos {
result = append ( result , UniqueProcess {
_RM_UNIQUE_PROCESS : info . Process ,
CanReceiveGUIMsgs : info . AppType == _RmMainWindow || info . AppType == _RmOtherWindow ,
} )
}
return result , nil
}
func ( rms * rmSession ) processList ( ) ( [ ] _RM_PROCESS_INFO , error ) {
const maxAttempts = 5
var avail , rebootReasons uint32
needed := uint32 ( 1 )
var buf [ ] _RM_PROCESS_INFO
err := error ( windows . ERROR_MORE_DATA )
numAttempts := 0
for err == windows . ERROR_MORE_DATA && numAttempts < maxAttempts {
numAttempts ++
buf = make ( [ ] _RM_PROCESS_INFO , needed )
avail = needed
err = rmGetList ( rms . session , & needed , & avail , unsafe . SliceData ( buf ) , & rebootReasons )
}
if err != nil {
if err == windows . ERROR_SESSION_CREDENTIAL_CONFLICT {
// Add some more context about the meaning of this error.
err = fmt . Errorf ( "%w (the Restart Manager does not permit calling RmGetList from a process that did not originally create the session)" , err )
}
return nil , err
}
return buf [ : avail ] , nil
}
func ( rms * rmSession ) AddPaths ( fqPaths [ ] string ) error {
if len ( fqPaths ) == 0 {
return nil
}
fqPaths16 := make ( [ ] * uint16 , 0 , len ( fqPaths ) )
for _ , fqPath := range fqPaths {
if ! filepath . IsAbs ( fqPath ) {
return fmt . Errorf ( "%w: paths must be fully-qualified" , windows . ERROR_BAD_PATHNAME )
}
fqPath16 , err := windows . UTF16PtrFromString ( fqPath )
if err != nil {
return err
}
fqPaths16 = append ( fqPaths16 , fqPath16 )
}
return rmRegisterResources ( rms . session , uint32 ( len ( fqPaths16 ) ) , unsafe . SliceData ( fqPaths16 ) , 0 , nil , 0 , nil )
}
// UniqueProcess contains the necessary information to uniquely identify a
// process in the face of potential PID reuse.
type UniqueProcess struct {
_RM_UNIQUE_PROCESS
// CanReceiveGUIMsgs is true when the process has open top-level windows.
CanReceiveGUIMsgs bool
}
// AsRestartableProcess obtains a RestartableProcess populated using the
// information obtained from up.
func ( up * UniqueProcess ) AsRestartableProcess ( ) ( * RestartableProcess , error ) {
// We need PROCESS_QUERY_INFORMATION instead of PROCESS_QUERY_LIMITED_INFORMATION
// in order for ProcessImageName to be able to work from within a privileged
// Windows Installer process.
// We need PROCESS_VM_READ for GetApplicationRestartSettings.
// We need PROCESS_TERMINATE and SYNCHRONIZE to terminate the process and
// to be able to wait for the terminated process's handle to signal.
access := uint32 ( windows . PROCESS_QUERY_INFORMATION | windows . PROCESS_TERMINATE | windows . PROCESS_VM_READ | windows . SYNCHRONIZE )
h , err := windows . OpenProcess ( access , false , up . PID )
if err != nil {
return nil , fmt . Errorf ( "OpenProcess(%d[%#X]): %w" , up . PID , up . PID , err )
}
defer func ( ) {
if h == 0 {
return
}
windows . CloseHandle ( h )
} ( )
var creationTime , exitTime , kernelTime , userTime windows . Filetime
if err := windows . GetProcessTimes ( h , & creationTime , & exitTime , & kernelTime , & userTime ) ; err != nil {
return nil , fmt . Errorf ( "GetProcessTimes: %w" , err )
}
if creationTime != up . ProcessStartTime {
// The PID has been reused and does not actually reference the original process.
return nil , ErrDefunctProcess
}
var tok windows . Token
if err := windows . OpenProcessToken ( h , windows . TOKEN_QUERY , & tok ) ; err != nil {
return nil , fmt . Errorf ( "OpenProcessToken: %w" , err )
}
defer tok . Close ( )
tsSessionID , err := TSSessionID ( tok )
if err != nil {
return nil , fmt . Errorf ( "TSSessionID: %w" , err )
}
logonSessionID , err := LogonSessionID ( tok )
if err != nil {
return nil , fmt . Errorf ( "LogonSessionID: %w" , err )
}
img , err := ProcessImageName ( h )
if err != nil {
return nil , fmt . Errorf ( "ProcessImageName: %w" , err )
}
const _RESTART_MAX_CMD_LINE = 1024
var cmdLine [ _RESTART_MAX_CMD_LINE ] uint16
cmdLineLen := uint32 ( len ( cmdLine ) )
var rmFlags uint32
hr := getApplicationRestartSettings ( h , & cmdLine [ 0 ] , & cmdLineLen , & rmFlags )
// Not found is not an error; it just means that the app never set any restart settings.
if e := wingoes . ErrorFromHRESULT ( hr ) ; e . Failed ( ) && e != wingoes . ErrorFromErrno ( windows . ERROR_NOT_FOUND ) {
return nil , fmt . Errorf ( "GetApplicationRestartSettings: %w" , error ( e ) )
}
if ( rmFlags & _RESTART_NO_PATCH ) != 0 {
// The application explicitly stated that it cannot be restarted during
// an upgrade.
return nil , ErrProcessNotRestartable
}
var logonSID string
// Non-fatal, so we'll proceed with best-effort.
if tokenGroups , err := tok . GetTokenGroups ( ) ; err == nil {
for _ , group := range tokenGroups . AllGroups ( ) {
if ( group . Attributes & windows . SE_GROUP_LOGON_ID ) != 0 {
logonSID = group . Sid . String ( )
break
}
}
}
var userSID string
// Non-fatal, so we'll proceed with best-effort.
if tokenUser , err := tok . GetTokenUser ( ) ; err == nil {
// Save the user's SID so that we can later check it against the currently
// logged-in Tailscale profile.
userSID = tokenUser . User . Sid . String ( )
}
result := & RestartableProcess {
Process : * up ,
SessionInfo : SessionID {
LogonSession : logonSessionID ,
TSSession : tsSessionID ,
} ,
CommandLineInfo : CommandLineInfo {
ExePath : img ,
Args : windows . UTF16ToString ( cmdLine [ : cmdLineLen ] ) ,
} ,
LogonSID : logonSID ,
UserSID : userSID ,
handle : h ,
}
runtime . SetFinalizer ( result , func ( rp * RestartableProcess ) { rp . Close ( ) } )
h = 0
return result , nil
}
// RestartableProcess contains the necessary information to uniquely identify
// an existing process, as well as the necessary information to be able to
// terminate it and later start a new instance in the identical logon session
// to the previous instance.
type RestartableProcess struct {
// Process uniquely identifies the existing process.
Process UniqueProcess
// SessionInfo uniquely identifies the Terminal Services (RDP) and logon
// sessions the existing process is running under.
SessionInfo SessionID
// CommandLineInfo contains the command line information necessary for restarting.
CommandLineInfo CommandLineInfo
// LogonSID contains the stringified SID of the existing process's token's logon session.
LogonSID string
// UserSID contains the stringified SID of the existing process's token's user.
UserSID string
// handle specifies the Win32 HANDLE associated with the existing process.
// When non-zero, it includes access rights for querying, terminating, and synchronizing.
handle windows . Handle
// hasExitCode is true when the exitCode field is valid.
hasExitCode bool
// exitCode contains exit code returned by this RestartableProcess once
// its termination has been recorded by (RestartableProcesses).Terminate.
// It is only valid when hasExitCode == true.
exitCode uint32
}
func ( rp * RestartableProcess ) Close ( ) error {
if rp . handle == 0 {
return nil
}
windows . CloseHandle ( rp . handle )
runtime . SetFinalizer ( rp , nil )
rp . handle = 0
return nil
}
// RestartableProcesses is a map of PID to *RestartableProcess instance.
type RestartableProcesses map [ uint32 ] * RestartableProcess
// NewRestartableProcesses instantiates a new RestartableProcesses.
func NewRestartableProcesses ( ) RestartableProcesses {
return make ( RestartableProcesses )
}
// Add inserts rp into rps.
func ( rps RestartableProcesses ) Add ( rp * RestartableProcess ) {
if rp != nil {
rps [ rp . Process . PID ] = rp
}
}
// Delete removes rp from rps.
func ( rps RestartableProcesses ) Delete ( rp * RestartableProcess ) {
if rp != nil {
delete ( rps , rp . Process . PID )
}
}
// Close invokes (*RestartableProcess).Close on every value in rps, and then
// clears rps.
func ( rps RestartableProcesses ) Close ( ) error {
for _ , v := range rps {
v . Close ( )
}
clear ( rps )
return nil
}
// _MAXIMUM_WAIT_OBJECTS is the Win32 constant for the maximum number of
// handles that a call to WaitForMultipleObjects may receive at once.
const _MAXIMUM_WAIT_OBJECTS = 64
// Terminate forcibly terminates all processes in rps using exitCode, and then
// waits for their process handles to signal, up to timeout.
func ( rps RestartableProcesses ) Terminate ( logf logger . Logf , exitCode uint32 , timeout time . Duration ) error {
if len ( rps ) == 0 {
return nil
}
millis , err := wingoes . DurationToTimeoutMilliseconds ( timeout )
if err != nil {
return err
}
errs := make ( [ ] error , 0 , len ( rps ) )
procs := make ( [ ] * RestartableProcess , 0 , len ( rps ) )
handles := make ( [ ] windows . Handle , 0 , len ( rps ) )
for _ , v := range rps {
if err := windows . TerminateProcess ( v . handle , exitCode ) ; err != nil {
if err == windows . ERROR_ACCESS_DENIED {
// If v terminated before we attempted to terminate, we'll receive
// ERROR_ACCESS_DENIED, which is not really an error worth reporting in
// our use case. Just obtain the exit code and then close the process.
if err := windows . GetExitCodeProcess ( v . handle , & v . exitCode ) ; err != nil {
logf ( "GetExitCodeProcess failed: %v" , err )
} else {
v . hasExitCode = true
}
v . Close ( )
} else {
errs = append ( errs , & terminationError { rp : v , err : err } )
}
continue
}
procs = append ( procs , v )
handles = append ( handles , v . handle )
}
for len ( handles ) > 0 {
// WaitForMultipleObjects can only wait on _MAXIMUM_WAIT_OBJECTS handles per
// call, so we batch them as necessary.
count := uint32 ( min ( len ( handles ) , _MAXIMUM_WAIT_OBJECTS ) )
waitCode , err := windows . WaitForMultipleObjects ( handles [ : count ] , true , millis )
if err != nil {
errs = append ( errs , fmt . Errorf ( "waiting on terminated process handles: %w" , err ) )
break
}
if e := windows . Errno ( waitCode ) ; e == windows . WAIT_TIMEOUT {
errs = append ( errs , fmt . Errorf ( "waiting on terminated process handles: %w" , error ( e ) ) )
break
}
if waitCode >= windows . WAIT_OBJECT_0 && waitCode < ( windows . WAIT_OBJECT_0 + count ) {
// The first count process handles have all been signaled. Close them out.
for _ , proc := range procs [ : count ] {
if err := windows . GetExitCodeProcess ( proc . handle , & proc . exitCode ) ; err != nil {
logf ( "GetExitCodeProcess failed: %v" , err )
} else {
proc . hasExitCode = true
}
proc . Close ( )
}
procs = procs [ count : ]
handles = handles [ count : ]
continue
}
// We really shouldn't be reaching this point
panic ( fmt . Sprintf ( "unexpected state from WaitForMultipleObjects: %d" , waitCode ) )
}
if len ( errs ) != 0 {
return multierr . New ( errs ... )
}
return nil
}
type terminationError struct {
rp * RestartableProcess
err error
}
func ( te * terminationError ) Error ( ) string {
pid := te . rp . Process . PID
return fmt . Sprintf ( "terminating process %d (%#X): %v" , pid , pid , te . err )
}
func ( te * terminationError ) Unwrap ( ) error {
return te . err
}
// SessionID encapsulates the necessary information for uniquely identifying
// sessions. In particular, SessionID contains enough information to detect
// reuse of Terminal Service session IDs.
type SessionID struct {
// LogonSession is the NT logon session ID.
LogonSession windows . LUID
// TSSession is the terminal services session ID.
TSSession uint32
}
// OpenToken obtains the security token associated with sessID.
func ( sessID * SessionID ) OpenToken ( ) ( windows . Token , error ) {
var token windows . Token
if err := windows . WTSQueryUserToken ( sessID . TSSession , & token ) ; err != nil {
return 0 , err
}
var err error
defer func ( ) {
if err != nil {
token . Close ( )
}
} ( )
tokenLogonSession , err := LogonSessionID ( token )
if err != nil {
return 0 , err
}
if tokenLogonSession != sessID . LogonSession {
err = windows . ERROR_NO_SUCH_LOGON_SESSION
return 0 , err
}
return token , nil
}
// ContainsToken determines whether token is contained within sessID.
func ( sessID * SessionID ) ContainsToken ( token windows . Token ) ( bool , error ) {
tokenTSSessionID , err := TSSessionID ( token )
if err != nil {
return false , err
}
if tokenTSSessionID != sessID . TSSession {
return false , nil
}
tokenLogonSession , err := LogonSessionID ( token )
if err != nil {
return false , err
}
return tokenLogonSession == sessID . LogonSession , nil
}
// This is the Window Station and Desktop within a particular session that must
// be specified for interactive processes: "Winsta0\\default\x00"
var defaultDesktop = unsafe . SliceData ( [ ] uint16 { 'W' , 'i' , 'n' , 's' , 't' , 'a' , '0' , '\\' , 'd' , 'e' , 'f' , 'a' , 'u' , 'l' , 't' , 0 } )
// CommandLineInfo manages the necessary information for creating a Win32
// process using a specific command line.
type CommandLineInfo struct {
// ExePath must be a fully-qualified path to a Windows executable binary.
ExePath string
// Args must be any arguments supplied to the process, excluding the
// path to the binary itself. Args must be properly quoted according to
// Windows path rules. To create a properly quoted Args from scratch, call the
// SetArgs method instead.
Args string ` json:",omitempty" `
}
// SetArgs converts args to a string quoted as necessary to satisfy the rules
// for Win32 command lines, and sets cli.Args to that string.
func ( cli * CommandLineInfo ) SetArgs ( args [ ] string ) {
var buf strings . Builder
for _ , arg := range args {
if buf . Len ( ) > 0 {
buf . WriteByte ( ' ' )
}
buf . WriteString ( windows . EscapeArg ( arg ) )
}
cli . Args = buf . String ( )
}
// Validate ensures that cli.ExePath contains an absolute path.
func ( cli * CommandLineInfo ) Validate ( ) error {
if cli == nil {
return windows . ERROR_INVALID_PARAMETER
}
if ! filepath . IsAbs ( cli . ExePath ) {
return fmt . Errorf ( "%w: CommandLineInfo requires absolute ExePath" , windows . ERROR_BAD_PATHNAME )
}
return nil
}
// Resolve converts the information in cli to a format compatible with the Win32
// CreateProcess* family of APIs, as pointers to C-style UTF-16 strings. It also
// returns the full command line as a Go string for logging purposes.
func ( cli * CommandLineInfo ) Resolve ( ) ( exePath * uint16 , cmdLine * uint16 , cmdLineStr string , err error ) {
// Resolve cmdLine first since that also does a Validate.
cmdLineStr , cmdLine , err = cli . resolveArgsAsUTF16Ptr ( )
if err != nil {
return nil , nil , "" , err
}
exePath , err = windows . UTF16PtrFromString ( cli . ExePath )
if err != nil {
return nil , nil , "" , err
}
return exePath , cmdLine , cmdLineStr , nil
}
// resolveArgs quotes cli.ExePath as necessary, appends Args, and returns the result.
func ( cli * CommandLineInfo ) resolveArgs ( ) ( string , error ) {
if err := cli . Validate ( ) ; err != nil {
return "" , err
}
var cmdLineBuf strings . Builder
cmdLineBuf . WriteString ( windows . EscapeArg ( cli . ExePath ) )
if args := cli . Args ; args != "" {
cmdLineBuf . WriteByte ( ' ' )
cmdLineBuf . WriteString ( args )
}
return cmdLineBuf . String ( ) , nil
}
func ( cli * CommandLineInfo ) resolveArgsAsUTF16Ptr ( ) ( string , * uint16 , error ) {
s , err := cli . resolveArgs ( )
if err != nil {
return "" , nil , err
}
s16 , err := windows . UTF16PtrFromString ( s )
if err != nil {
return "" , nil , err
}
return s , s16 , nil
}
// StartProcessInSession creates a new process using cmdLineInfo that will
// reside inside the session identified by sessID, with the security token whose
// logon is associated with sessID. The child process's environment will be
// inherited from the session token's environment.
func StartProcessInSession ( sessID SessionID , cmdLineInfo CommandLineInfo ) error {
return StartProcessInSessionWithHandler ( sessID , cmdLineInfo , nil )
}
// PostCreateProcessHandler is a function that is invoked by
// StartProcessInSessionWithHandler when the child process has been successfully
// created. It is the responsibility of the handler to close the pi.Thread and
// pi.Process handles.
type PostCreateProcessHandler func ( pi * windows . ProcessInformation )
// StartProcessInSessionWithHandler creates a new process using cmdLineInfo that
// will reside inside the session identified by sessID, with the security token
// whose logon is associated with sessID. The child process's environment will be
// inherited from the session token's environment. When the child process has
// been successfully created, handler is invoked with the windows.ProcessInformation
// that was returned by the OS.
func StartProcessInSessionWithHandler ( sessID SessionID , cmdLineInfo CommandLineInfo , handler PostCreateProcessHandler ) error {
pi , err := startProcessInSessionInternal ( sessID , cmdLineInfo , 0 )
if err != nil {
return err
}
if handler != nil {
handler ( pi )
return nil
}
windows . CloseHandle ( pi . Process )
windows . CloseHandle ( pi . Thread )
return nil
}
// RunProcessInSession creates a new process and waits up to timeout for that
// child process to complete its execution. The process is created using
// cmdLineInfo and will reside inside the session identified by sessID, with the
// security token whose logon is associated with sessID. The child process's
// environment will be inherited from the session token's environment.
func RunProcessInSession ( sessID SessionID , cmdLineInfo CommandLineInfo , timeout time . Duration ) ( uint32 , error ) {
timeoutMillis , err := wingoes . DurationToTimeoutMilliseconds ( timeout )
if err != nil {
return 1 , err
}
pi , err := startProcessInSessionInternal ( sessID , cmdLineInfo , 0 )
if err != nil {
return 1 , err
}
windows . CloseHandle ( pi . Thread )
defer windows . CloseHandle ( pi . Process )
waitCode , err := windows . WaitForSingleObject ( pi . Process , timeoutMillis )
if err != nil {
return 1 , fmt . Errorf ( "WaitForSingleObject: %w" , err )
}
if e := windows . Errno ( waitCode ) ; e == windows . WAIT_TIMEOUT {
return 1 , e
}
if waitCode != windows . WAIT_OBJECT_0 {
// This should not be possible; log
return 1 , fmt . Errorf ( "unexpected state from WaitForSingleObject: %d" , waitCode )
}
var exitCode uint32
if err := windows . GetExitCodeProcess ( pi . Process , & exitCode ) ; err != nil {
return 1 , err
}
return exitCode , nil
}
func startProcessInSessionInternal ( sessID SessionID , cmdLineInfo CommandLineInfo , extraFlags uint32 ) ( * windows . ProcessInformation , error ) {
if err := cmdLineInfo . Validate ( ) ; err != nil {
return nil , err
}
token , err := sessID . OpenToken ( )
if err != nil {
return nil , fmt . Errorf ( "(*SessionID).OpenToken: %w" , err )
}
defer token . Close ( )
exePath16 , commandLine16 , _ , err := cmdLineInfo . Resolve ( )
if err != nil {
return nil , fmt . Errorf ( "(*CommandLineInfo).Resolve(): %w" , err )
}
wd16 , err := windows . UTF16PtrFromString ( filepath . Dir ( cmdLineInfo . ExePath ) )
if err != nil {
return nil , fmt . Errorf ( "UTF16PtrFromString(wd): %w" , err )
}
env , err := token . Environ ( false )
if err != nil {
return nil , fmt . Errorf ( "token environment: %w" , err )
}
env16 := newEnvBlock ( env )
// The privileges in privNames are required for CreateProcessAsUser to be
// able to start processes as other users in other logon sessions.
privNames := [ ] string {
"SeAssignPrimaryTokenPrivilege" ,
"SeIncreaseQuotaPrivilege" ,
}
dropPrivs , err := EnableCurrentThreadPrivileges ( privNames )
if err != nil {
return nil , fmt . Errorf ( "EnableCurrentThreadPrivileges(%#v): %w" , privNames , err )
}
defer dropPrivs ( )
createFlags := extraFlags | windows . CREATE_UNICODE_ENVIRONMENT | windows . DETACHED_PROCESS
si := windows . StartupInfo {
Cb : uint32 ( unsafe . Sizeof ( windows . StartupInfo { } ) ) ,
Desktop : defaultDesktop ,
}
var pi windows . ProcessInformation
if err := windows . CreateProcessAsUser ( token , exePath16 , commandLine16 , nil , nil ,
false , createFlags , env16 , wd16 , & si , & pi ) ; err != nil {
return nil , fmt . Errorf ( "CreateProcessAsUser: %w" , err )
}
return & pi , nil
}
func newEnvBlock ( env [ ] string ) * uint16 {
// Intentionally using bytes.Buffer here because we're writing nul bytes (the standard library does this too).
var buf bytes . Buffer
for _ , v := range env {
buf . WriteString ( v )
buf . WriteByte ( 0 )
}
if buf . Len ( ) == 0 {
// So that we end with a double-null in the empty env case
buf . WriteByte ( 0 )
}
buf . WriteByte ( 0 )
return unsafe . SliceData ( utf16 . Encode ( [ ] rune ( string ( buf . Bytes ( ) ) ) ) )
}