1. 程式人生 > >用QT建立一個Windows Service以及踩到的若干坑

用QT建立一個Windows Service以及踩到的若干坑

因為專案需要,做一個Tech Spike,看看用QT如何建立一個Windows Service,並實現觸控某硬體而彈出某應用程式的功能。

一、自然的思路

為實現“觸控某硬體而彈出某應用程式”,首先想到的是,這個觸控動作觸發了一個特定的signal,而QObject的connect()函式就將這個signal與載入應用程式的動作連線起來,這樣就實現了此功能。而這一切又是實現在Windows的service裡面的。於是一切看起來就很順暢自然了。現在,我們來看看如何用QT建立一個Windows的Service吧。

二、用QT建立Windows的Service

本來,建立一個Windows的Service是一件挺複雜的事情。如果QT這種跨平臺的類庫已經實現了這一功能,豈不是省去了開發者很多麻煩?!筆者也不能算QT專家,於是少不了Google一把。結論是:QT本身並不支援建立Windows的Service或Unix的daemon,但是有第三方的QT庫支援!這個庫就是:

https://github.com/qtproject/qt-solutions/tree/master/qtservice

那麼這個庫是啥License呢?沒有找到專門描述License的檔案,但從所有原始碼檔案來看,應該是BSD License. 而在README.txt中又提到了LGPL. 筆者去了解個大概:BSD是一種比較寬鬆的License,而LGPL要求使用者只要不修改其原始碼就可以用於商業產品。OK!可以用了!

怎麼使用呢?有2種方式:一是編譯成dll後使用,二是直接使用原始碼再配合自己的程式碼進行編譯。好像是廢話。。。

具體咋用呢?看其自帶的examples:在examples這個目錄下,提供了3個示例。第一個是controller,筆者沒有細看,大致是寫了一個類似於Windows的sc.exe的功能吧;第二個是interactive service,這個筆者簡單編譯運行了一下,執行時崩潰了,因為開始認為和筆者的目標不太吻合,就沒有細究原因,但是到最後回頭來看,發現卻是和最後一個大坑一樣的原因,這個暫且按下不表;最後一個示例就是一個一般的service,筆者略作了一下研究,發現其實它只有一個main.cpp檔案,裡面有一個service類和一個幹活的類,而兩個類基本都要實現start(或run)方法、pause、resume、terminate等方法。所以,開發者只要把這個main.cpp稍加修改,就是用QT建立自己的Windows Service了。

具體操作Service的方法如下:

1. 開啟一個Administrator級別的Terminal視窗,注意,必須是Administrator許可權的;

2. 假設編譯出的可執行檔案叫做XXX.exe, 在其目錄下執行:XXX.exe -i  這是代表安裝了XXX.exe作為Windows的服務了。此時你可以按Windows鍵+R鍵,再執行services.msc來檢視這個新安裝的服務。

注意:直接用-i選項安裝的是一個"LocalSystem"帳號的服務,而用 -i account password 選項則是安裝的一個當前使用者帳號的服務。

3. 繼續執行:XXX.exe -s  這是表示啟動了這個Service. 此時,在系統的Service檢視視窗按F5重新整理,可以看到這個服務的執行狀態已經變成Running了。

4. 此後,可以用-p、-r、-t、-u選項分別來暫停、繼續、停止和解除安裝這個服務。而-e選項是如普通程式般執行此exe程式,即不以Service來執行,這是為了Debug方便。

以上就是用QT建立和執行Windows Service的介紹了。

三、踩到的幾個坑

1. log檔案到哪裡去了? 原本的qtservice庫提供了logMessage()來記錄log,但是記錄的地址並不是一般的log檔案,而是要在Windows的Event Viewer裡面才能看到的系統事件。使用起來多有不便。所以,還是自己寫一個qInstallMessageHandler吧! 可是問題來了,如果不指定絕對路徑,只寫xxxx.log,那麼這個log檔案竟然找不到了 -- 並不是和exe檔案在同一個目錄下! 那麼它在哪裡呢?到底寫了log沒有呢? 寫了log,位置就在C:\Windows\System32目錄下。 這裡要注意的一點是,無論這是LocalSystem的服務,還是當前使用者的服務,log都會被寫到system32目錄下。 2. 配置檔案該放在哪裡? 假設程式中使用了QSetting來讀寫配置檔案,那麼這個配置檔案該放哪兒呢? 沒錯,有了上面的經驗,我們不難發現,配置檔案也應該放在system32目錄去。 這裡要注意的一點是,無論是配置檔案還是log,如果執行的-e選項,那麼就仍然是和exe檔案在同一個目錄去尋找,而不是system32目錄了。 3. 該如何啟動一個新的process? 其實這個坑和Service無關。啟動一個新的process大致有如下幾個方法: a. QProcess::start()     // 非靜態方法 b. QProcess::execute()    // 靜態方法 c. QProcess::startDetached()   // 靜態方法 d. system()    // #include <cstdlib> 這裡的坑在於,使用方法a、b、d都會導致父程序suspend,也就是父程序的不會繼續往下執行了,只有子程序結束,父程序才會繼續;而只有使用方法c才是真正的啟動了一個新程序後,父程序繼續自己的工作。 4. 為何看不見新啟動的程序的GUI? 最開始我以為這是因為新啟動的程序是屬於LocalSystem帳號的,所以當前使用者看不見。但後來以當前使用者來啟動Service,再觸發產生新程序,仍然看不到這個新程序的GUI。一般,這裡用來做實驗的新程序都是notepad.exe.  經過一番研究,筆者發現了以下2篇文章: 簡要介紹一下它們的內容吧: a. 自從Windows Vista後,Windows Service就不支援啟動GUI application了,這是為了安全的考慮。其實,這個application是被啟動了,在工作管理員裡能看到,但是無法看到GUI.  b. 即使用LocalSystem帳號啟動service,並且在Service的屬性的LogOn標籤頁勾選了"Interactive with desktop"選項,使用者也仍然是看不見GUI的. 之所以看不見GUI的原因,就是Service和其所啟動的application都被放到了一個叫做session 0的地方,而這裡是無法看到GUI的。 c. 如果一定要有GUI該怎麼辦呢?官方的說法是,自己寫一個帶GUI的application,然後通過管道或Socket,與後臺Service進行通訊。 最後,筆者又回來看了下前面提到的第二個example “Interactive service”裡面的註釋,發現差不多也是同樣的意思。 但是真的不能從Windows Service啟動一個GUI了嗎?答案是否定的。請看下面的示例程式碼,它解決了這一難題。
#ifdef Q_OS_WIN

#include <Windows.h>
#include <WinBase.h>
#include <WtsApi32.h>
#include <UserEnv.h>
#include <tchar.h>

BOOL launchGUIApplication(std::wstring app, std::vector<std::wstring>params)
{
    BOOL bResult = FALSE;

    DWORD dwSessionId = WTSGetActiveConsoleSessionId();
    if (dwSessionId == 0xFFFFFFFF)
    {
        return FALSE;
    }

    HANDLE hUserToken = NULL;
    if (WTSQueryUserToken(dwSessionId, &hUserToken) == FALSE)
    {
        return FALSE;
    }

    HANDLE hTheToken = NULL;
    if (DuplicateTokenEx(hUserToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, 0, SecurityImpersonation, TokenPrimary, &hTheToken) == TRUE)
    {

        if (ImpersonateLoggedOnUser(hTheToken) == TRUE)
        {
            DWORD dwCreationFlags = HIGH_PRIORITY_CLASS | CREATE_NEW_CONSOLE;

            STARTUPINFO si = { sizeof(si) };
            PROCESS_INFORMATION pi;
            SECURITY_ATTRIBUTES Security1 = { sizeof(Security1) };
            SECURITY_ATTRIBUTES Security2 = { sizeof(Security2) };

            LPVOID pEnv = NULL;
            if (CreateEnvironmentBlock(&pEnv, hTheToken, TRUE) == TRUE)
            {
                dwCreationFlags |= CREATE_UNICODE_ENVIRONMENT;
            }

            TCHAR path[MAX_PATH];
            _tcscpy_s(path, MAX_PATH, app.c_str());

            TCHAR commandLine[MAX_PATH];
            _tcscpy_s(commandLine, MAX_PATH, L" ");
            for (auto item : params) {
                _tcscat_s(commandLine, MAX_PATH, item.c_str());
            }

            // Launch the process in the client's logon session.
            bResult = CreateProcessAsUser(
                hTheToken,
                (LPWSTR)(path),
                (LPWSTR)(commandLine),
                &Security1,
                &Security2,
                FALSE,
                dwCreationFlags,
                pEnv,
                NULL,
                &si,
                &pi
                );

            RevertToSelf();

            if (pEnv)
            {
                DestroyEnvironmentBlock(pEnv);
            }
        }
        CloseHandle(hTheToken);
    }
    CloseHandle(hUserToken);

    return bResult;
}

#endif

#ifdef Q_OS_WIN
    std::wstring app = L"notepad.exe";
    std::vector<std::wstring> params = {};
    if (launchGUIApplication(app, params) == FALSE) {
        qDebug() << "Failed to launch " << app.c_str();
    }
#endif

以上程式碼中,起到關鍵作用的是CreateProcessAsUser()函式,但是,必須要有DuplicateTokenEX()函式的配合。沒有這個函式的配合,是仍能啟動notepad.exe,但是卻會無法看到筆記本的GUI. DuplicateTokenEX()函式的作用就是,建立一個訪問令牌(access token),它是複製了一個已存在的令牌的。該函式或者建立一個主令牌(Primary Token),或者建立一個模擬令牌(ImpersonateToken)。

四、筆者的示例程式