用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
这里的坑在于,使用方法a、b、d都会导致父进程suspend,也就是父进程的不会继续往下执行了,只有子进程结束,父进程才会继续;而只有使用方法c才是真正的启动了一个新进程后,父进程继续自己的工作。

4. 为何看不见新启动的进程的GUI?
最开始我以为这是因为新启动的进程是属于LocalSystem帐号的,所以当前用户看不见。但后来以当前用户来启动Service,再触发产生新进程,仍然看不到这个新进程的GUI。一般,这里用来做实验的新进程都是notepad.exe. 
经过一番研究,笔者发现了以下2篇文章:
http://stackoverflow.com/questions/5063731/is-there-any-way-to-start-a-gui-application-from-a-windows-service-on-windows-7
https://msdn.microsoft.com/en-us/library/windows/desktop/ms683502(v=vs.85).aspx
简要介绍一下它们的内容吧:
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 
#include 
#include 
#include 
#include 

BOOL launchGUIApplication(std::wstring app, std::vectorparams)
{
    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 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)。

四、笔者的示例程序

笔者自己也写了一点示例程序,源代码参见: https://github.com/FinixLei/QtProjects/tree/master/TestSignalTrigger

你可能感兴趣的:(技术综合,QT,疑难杂症)