/* Copyright (C) 2002-2005 RealVNC Ltd.  All Rights Reserved.
 * 
 * This is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this software; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307,
 * USA.
 */

// -=- WinVNC Version 4.0 Service-Mode implementation

#include <winvnc/VNCServerService.h>
#include <rfb_win32/TsSessions.h>
#include <rfb_win32/ModuleFileName.h>
#include <windows.h>
#include <wtsapi32.h>
#include <tlhelp32.h>

using namespace winvnc;
using namespace rfb;
using namespace win32;

const TCHAR* winvnc::VNCServerService::Name = _T("TigerVNC");

// SendSAS is not available until Windows 7, and missing from MinGW
static HMODULE sasLibrary = NULL;
typedef void WINAPI (*SendSAS_proto)(BOOL AsUser);
static SendSAS_proto _SendSAS = NULL;

VNCServerService::VNCServerService()
  : Service(Name)
  , stopServiceEvent(CreateEvent(0, FALSE, FALSE, 0))
  , sessionEvent(CreateEvent(0, FALSE, FALSE, "Global\\SessionEventTigerVNC"))
  , sessionEventCad(CreateEvent(0, FALSE, FALSE, "Global\\SessionEventTigerVNCCad")) {
  if (sasLibrary == NULL) {
    sasLibrary = LoadLibrary("sas.dll");
    if (sasLibrary != NULL)
      _SendSAS = (SendSAS_proto)GetProcAddress(sasLibrary, "SendSAS");
  }
  // - Set the service-mode logging defaults
  //   These will be overridden by the Log option in the
  //   registry, if present.
  logParams.setParam("*:EventLog:0,Connections:EventLog:100");
}


//////////////////////////////////////////////////////////////////////////////

DWORD GetLogonPid(DWORD dwSessionId)
{
    DWORD dwLogonPid = 0;
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    if (hSnap != INVALID_HANDLE_VALUE)
    {
        PROCESSENTRY32 procEntry;
        procEntry.dwSize = sizeof procEntry;

        if (Process32First(hSnap, &procEntry)) do
        {
            DWORD dwLogonSessionId = 0;
            if (_stricmp(procEntry.szExeFile, "winlogon.exe") == 0 &&
                ProcessIdToSessionId(procEntry.th32ProcessID, &dwLogonSessionId) &&
                dwLogonSessionId == dwSessionId)
            {
                dwLogonPid = procEntry.th32ProcessID;
                break;
            }
        } while (Process32Next(hSnap, &procEntry));
        CloseHandle(hSnap);
    }
    return dwLogonPid;
}

//////////////////////////////////////////////////////////////////////////////
BOOL GetSessionUserTokenWin(OUT LPHANDLE lphUserToken)
{
    BOOL bResult = FALSE;
    ConsoleSessionId ID_session;
    DWORD Id = GetLogonPid(ID_session.id);
    if (HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Id))
    {
        bResult = OpenProcessToken(hProcess, TOKEN_ALL_ACCESS, lphUserToken);
        CloseHandle(hProcess);
    }
    return bResult;
}

//////////////////////////////////////////////////////////////////////////////
// START the app as system 
HANDLE LaunchProcessWin(DWORD dwSessionId)
{
    HANDLE hProcess = NULL;
    HANDLE hToken = NULL;
    if (GetSessionUserTokenWin(&hToken))
    {
        ModuleFileName filename;
        TCharArray cmdLine;
        cmdLine.format("\"%s\" -noconsole -service_run", filename.buf);
        STARTUPINFO si;
        ZeroMemory(&si, sizeof si);
        si.cb = sizeof si;
        si.dwFlags = STARTF_USESHOWWINDOW;
        PROCESS_INFORMATION	pi;
        if (CreateProcessAsUser(hToken, NULL, cmdLine.buf, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi))
        {
            CloseHandle(pi.hThread);
            hProcess = pi.hProcess;
        }
        CloseHandle(hToken);
    }
    return hProcess;
}

DWORD VNCServerService::serviceMain(int argc, TCHAR* argv[])
{
    ConsoleSessionId OlddwSessionId;

    HANDLE hProcess = NULL;
    //We use this event to notify the program that the session has changed
    //The program need to end so the service can restart the program in the correct session
    //wait_for_existing_process();
    HANDLE testevent[2] = { stopServiceEvent, sessionEventCad };
    setStatus(SERVICE_RUNNING);
    while (status.dwCurrentState == SERVICE_RUNNING)
    {
        DWORD dwEvent = WaitForMultipleObjects(2, testevent, FALSE, 1000);
        switch (dwEvent) 
        { 
        //stopServiceEvent, exit while loop
        case WAIT_OBJECT_0 + 0: 
            setStatus(SERVICE_STOP_PENDING);
            break; 

        //cad request
        case WAIT_OBJECT_0 + 1:
            if (_SendSAS != NULL)
                _SendSAS(FALSE);
            break; 

        case WAIT_TIMEOUT:
            {
                ConsoleSessionId dwSessionId;
                if (OlddwSessionId.id != dwSessionId.id)
                {
                    OlddwSessionId.id = dwSessionId.id;
                    SetEvent(sessionEvent);
                }
                DWORD dwExitCode = 0;
                if (hProcess == NULL ||
                    (GetExitCodeProcess(hProcess, &dwExitCode) &&
                    dwExitCode != STILL_ACTIVE &&
                    CloseHandle(hProcess)))
                {
                    hProcess = LaunchProcessWin(dwSessionId.id);
                }
            }
            break;
        }
    }

    SetEvent(sessionEvent);

    if (hProcess)
    {
        WaitForSingleObject(hProcess, 15000);
        CloseHandle(hProcess);
    }
    return 0;
}

void VNCServerService::stop() {
  SetEvent(stopServiceEvent);
  SetEvent(sessionEvent);
}
