用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)。
 

四、笔者的示例程序

笔者自己也写了一点示例程序,源代码参见: https://github.com/FinixLei/QtProjects/tree/master/TestSignalTrigger
 
  • 15
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
好的,这是一个比较复杂的问题,需要分步骤进行回答。 首先,需要使用Qt框架中的QtService创建一个服务程序。QtService类提供了创建Windows服务和Unix守护进程的功能,可以在后台运行程序。以下是创建服务程序的代码示例: ```c++ #include <QtService> int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QtService::setServiceDescription("My service"); QtService::setServiceName("MyService"); MyService service; return QtService::exec(&service); } ``` 在上面的代码中,我们创建了一个名为MyService的服务程序,并设置了服务程序的描述和名称。MyService是自己实现的一个服务程序类。 接下来,我们需要使用Qt中的QTcpServer类创建一个TCP服务器,等待QtSocket的连接请求。以下是创建TCP服务器的代码示例: ```c++ #include <QTcpServer> #include <QtService> class MyService : public QtService<QCoreApplication> { Q_OBJECT public: MyService(int argc, char **argv) : QtService<QCoreApplication>(argc, argv, "MyService") { setServiceDescription("My service"); setServiceFlags(QtServiceBase::CanBeSuspended); m_server = new QTcpServer(this); connect(m_server, &QTcpServer::newConnection, this, &MyService::onNewConnection); if (!m_server->listen(QHostAddress::Any, 1234)) { qCritical() << "Failed to start server:" << m_server->errorString(); } } private slots: void onNewConnection() { QTcpSocket *socket = m_server->nextPendingConnection(); connect(socket, &QTcpSocket::readyRead, this, &MyService::onReadyRead); } void onReadyRead() { QTcpSocket *socket = qobject_cast<QTcpSocket *>(sender()); if (!socket) return; QByteArray data = socket->readAll(); // 处理收到的数据 } private: QTcpServer *m_server; }; ``` 在上面的代码中,我们在MyService类的构造函数中创建了一个QTcpServer对象,并将其绑定到任意IP地址的1234端口。当有新的连接请求时,QTcpServer会发出newConnection信号,我们在onNewConnection槽函数中创建一个QTcpSocket对象,并将其连接到readyRead信号,以便在有新的数据可读时进行处理。 最后,我们需要在onReadyRead槽函数中处理收到的数据。这里需要根据具体需求进行处理,例如将数据发送给其他服务进程进行处理,或者返回一些结果给客户端。 以上就是使用QtService创建一个应用程序,等待QtSocket的连接请求并进行交互的基本步骤。需要注意的是,这只是一个简单的示例,实际应用中可能需要更复杂的逻辑和数据处理方式。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值