본문으로 바로가기

윈도우 서비스 프로그램에서 메세지 BOX

category S/Service 2017. 10. 22. 16:53
서비스의 윈도우는 로그온 한 윈도우와 다르기 때문에 사용자들은 화면을 볼 수 없다. 서비스에 서 구동 되는 UI가 보여 지려면 로그인 윈도우를 가져와야 한다.

서비스프로그램 화면 이해 보기로그온 화면을 관리하는 winlogon desktop, 화면보호기를 담당하는 screen saver desktop, 평상시 윈도우 화면을 보여주는 interactive desktop 3가지로 나눠진다. 사용자가 로그온을 하게 되면 WinSta0\Winlogon 에서 smss.exe(세션메니져), winlogon.exe, msgina.dll 를 통해 일련의 logon 작업을 거쳐 최종적으로 Winst0\default 라는 interactive window station을 생성한다. 그러므로 사용자는 Winsta0의 station에서만이 UI 및 키입력을 받을 수 있다. 서비스 프로그램에서 Message Box도 보여지지 않는다. 백그라운드에서 동작하기 때문에 서비스 프로그램의 메세지도 확인 할 수 없다.

서빗스 옵션에서 '서비스와 데스크톱 상호작용허용' 이 적용 되었을 때, 서비스에 메제지가 발생하며 '메시지 표시' 버튼을 눌러야만 서비스 화면을 볼 수 있는 것이다. '서비스와 데스크톱 상호작용허용'이 되어 있지 않으면 서비스 화면은 볼 수가 없다.

 

WinApi 를 이용해서 Winst0\default Sation으로 전환 코드예 보기

아래의 코드는 서비스 프로그램에서 로그인되어 있는 윈도우 Station의 응용프로그램을 실행시키고 UI를 보여 줄 수 있도록 만들어진 것이며 실제 정상 동작하는 것을 확인한 코드이다. 전체가 소개되지 않고 실제 동작에 해당하는 일부 코드만 표시 했다

public bool ExcuteProcess()
{
            if (!IsReady()) return true;

            WriteLog("ExcuteProcess");
            /***********/
            String AppName = FullExeName;
            //String AppName = "notepad.exe";
            WInAPI.PROCESS_INFORMATION procInfo;

            WInAPI.StartProcessAndBypassUAC(AppName, out procInfo);    
}
public static bool StartProcessAndBypassUAC(String applicationName, out PROCESS_INFORMATION procInfo)
{
            uint winlogonPid = 0;
            IntPtr hUserTokenDup = IntPtr.Zero, hPToken = IntPtr.Zero, hProcess = IntPtr.Zero;            
            procInfo = new PROCESS_INFORMATION();

            // obtain the currently active session id; every logged on user in the system has a unique session id
            uint dwSessionId = WTSGetActiveConsoleSessionId();

            // obtain the process id of the winlogon process that is running within the currently active session
            Process[] processes = Process.GetProcessesByName("winlogon");
            foreach (Process p in processes)
            {
                if ((uint)p.SessionId == dwSessionId)
                {
                    winlogonPid = (uint)p.Id;
                }
            }

            // obtain a handle to the winlogon process
            hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);
            if (hProcess.ToInt32()!=0)//0이면 서비스 모드가 아니기 때문에 
            {
                // obtain a handle to the access token of the winlogon process
                if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, ref hPToken))
                {
                    CloseHandle(hProcess);
                    return false;
                }
            }
            // Security attibute structure used in DuplicateTokenEx and CreateProcessAsUser
            // I would prefer to not have to use a security attribute variable and to just 
            // simply pass null and inherit (by default) the security attributes
            // of the existing token. However, in C# structures are value types and therefore
            // cannot be assigned the null value.
            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
            sa.Length = Marshal.SizeOf(sa);
            if (hPToken.ToInt32() != 0)
            {
                // copy the access token of the winlogon process; the newly created token will be a primary token
                if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref hUserTokenDup))
                {
                    CloseHandle(hProcess);
                    CloseHandle(hPToken);
                    return false;
                }
            }
            // By default CreateProcessAsUser creates a process on a non-interactive window station, meaning
            // the window station has a desktop that is invisible and the process is incapable of receiving
            // user input. To remedy this we set the lpDesktop parameter to indicate we want to enable user 
            // interaction with the new process.
            STARTUPINFO si = new STARTUPINFO();
            si.cb = (int)Marshal.SizeOf(si);
            si.lpDesktop = @"winsta0\default"; // interactive window station parameter; basically this indicates that the process created can display a GUI on the desktop

            // flags that specify the priority and creation method of the process
            int dwCreationFlags = NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

            // create a new process in the current user's logon session
            bool result = CreateProcessAsUser(hUserTokenDup,        // client's access token
                                            null,                   // file to execute
                                            applicationName,        // command line
                                            ref sa,                 // pointer to process SECURITY_ATTRIBUTES
                                            ref sa,                 // pointer to thread SECURITY_ATTRIBUTES
                                            false,                  // handles are not inheritable
                                            dwCreationFlags,        // creation flags
                                            IntPtr.Zero,            // pointer to new environment block 
                                            null,                   // name of current directory 
                                            ref si,                 // pointer to STARTUPINFO structure
                                            out procInfo            // receives information about new process
                                            );

            // invalidate the handles
            CloseHandle(hProcess);
            CloseHandle(hPToken);
            CloseHandle(hUserTokenDup);

            return result; // return the result
 }

앞의 과정을 검토 하고 정상적으로 서비스프로그램에서 로그온한 사용자의 윈도우 Station에 UI를 보이게 하더라도 메세지 박스는 역시 LocalSystem의 백그라운드에서 동작 하므로 메세지 박스가 보이지 않는다. 이럴때 API를 이이용해서 WTSGetActiveConsoleSessionId();를 구해서 WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, (int)currentSessId.... 를 실행하면 MessageBox를 출력 할 수가 있다.
public static IntPtr WTS_CURRENT_SERVER_HANDLE = IntPtr.Zero;
public static int WTS_CURRENT_SESSION = -1;
public void Test()
{
  bool result = false;
  string title = "Hello";
  int tlen = title.Length;
  string msg = "Terminal Service!";
  int mlen = msg.Length;
  int resp = 0;
  uint currentSessId = WInAPI.WTSGetActiveConsoleSessionId();
  result = WInAPI.WTSSendMessage(WTS_CURRENT_SERVER_HANDLE, (int)currentSessId, title, tlen, msg, mlen, 0, 0, out resp, true);
  int err = Marshal.GetLastWin32Error();         
}