标签存档: Win32

REALbasic 中使用结构体作为 Win32 API 的参数及使用 Win32 API 停止服务

在之前,我使用 ShellExecute 这个 API 来执行命令(《REALbasic 中使用 ShellExecute 执行命令》),然后通过这个方法来停止某个服务,但是今天想加服务运行状态检测,这样就可以在服务没有运行的情况下不再询问用户是否需要停止某个服务。

为了省事,我一开始决定同样使用 cmd 去执行一个命令,将服务状态输出到一个临时文件中,再通过读取这个临时文件,查找特征字符串来判断服务是否运行:

// 伪代码
ShellExecute("cmd.exe /c sc query service_name > tmpfile")
dim serviceStatus as string
serviceStatus = TextInputStream.Open(tmpfile).ReadAll()
if instr(serviceStatus, "STOPPED") < 1 then
  // 提示用户是否停止服务
end if

但是这样做不太靠谱,因为 sc 这个命令执行是要时间的,而 ShellExecute 是异步的,这就导致了在调用 ShellExecute 执行完 sc query 之后,临时文件 tmpfile 里并不是马上就有服务状态的内容了。

为了防止这个情况,我再加了一个临时文件内容的检测,如果为空的话,sleep 100ms,再继续读取,如果超过 10 次仍没有内容,直接当作服务正在运行来对待。

// 伪代码
ShellExecute("cmd.exe /c sc query service_name > tmpfile")
dim serviceStatus as string
dim tryCount as integer
while tryCount < 10 and serviceStatus = ""
  tryCount = tryCount + 1
  App.SleepCurrentThread(100)
  serviceStatus = TextInputStream.Open(tmpfile).ReadAll()
 
wend
if instr(serviceStatus, "STOPPED") < 1 then
  // 提示用户是否停止服务
end if

但是这样仍然不能百分百保证正确,于是想到可不可以继续用 win32 api 来做这些,Google 了一下,找到一篇博客[1],看了之后发现如果只需要停止服务的话,还是蛮简单的,决定使用 win32 api 来实现我想要的功能了。

在停止服务这个过程中需要用到的 win32 api 有:OpenSCManagerW、OpenServiceW、QueryServiceStatus、ControlService,还有一个结构体:SERVICE_STATUS。

win32 api 的参数都很好对应,数值、句柄类型的参数直接使用 Integer 即可,字符串类型使用 CString,而 SERVICE_STATUS 这个结构体也可以直接通过工程菜单添加。

在传参的时候需要注意两点:

  1. 如果是字符串类型的参数并且调用的接口是 Unicode 类型的,那么需要在传入参数之前,将参数值转换为 Unicode 编码。可以通过 ConvertEncoding(text, Encodings.UTF16) 来转换。
  2. 如果参数是结构体,那么参数前的传参方式就要写 ByRef,也就是按引用传值,字符串和数值类型的参数可以不写或者写 ByVal。

以下是 4 个 win32 api 的 REALbasic 定义:

soft declare function OpenSCManagerW Lib "advapi32.dll" _
(ByVal lpMachineName As CString, ByVal lpDatabaseName As CString, _
ByVal dwDesiredAccess As Integer) As Integer
 
soft declare function OpenServiceW lib "advapi32.dll" _
(ByVal hSCManager As Integer, ByVal lpServiceName As CString, _
ByVal dwDesiredAccess As Integer) As Integer
 
soft declare function QueryServiceStatus lib "advapi32.dll" _
(byval hService as integer, byref lpServiceStatus as SERVICE_STATUS) _
as boolean
 
soft declare function ControlService lib "advapi32.dll" _
(byval hService as integer, byval dwControl as integer, _
byref lpServiceStatus as SERVICE_STATUS) as boolean

有了 api 定义之后,就可以直接根据上面提到的 vc++ 流程来停止某个服务了。

完整的服务操作 api 可以参阅微软的 MSDN。

这样看来,REALbasic 要写出支持 Windows 7 特性的应用程序也不是难事:)

参数资料

  1. VC++启动和停止服务, huangchonghai
  2. QueryServiceStatus, MSDN

REALbasic 中使用 ShellExecute 执行命令

在 REALbasic 中,如果需要执行 cmd 命令,可以直接使用 Shell 类,但是这样的话,编译成 Windows 程序时会额外需要一个 Shell.dll 的动态链接库,这对于我这样的 1exe 爱好者是不能忍受的。但是对于 Mac OS X 和 Linux 的生成目标来说,是不存在这个问题的,因为 Mac OS X 的应用程序本身就是一个文件夹,而 Linux 的目标不会生成额外的链接库。因此,需要针对 Windows 进行特殊处理。于是在网上搜索解决方案,找到了 VB 中执行程序的几种方法:

1. 使用 CreateProcess

通过 CreateProcess 以及使用管道,可以执行外部程序并获取输出,但是这个方法过于烦琐,并且我也不需要外部进程执行完毕后的输出结果,因此不采用。

2. 使用 Shell 方法

VB 里有一个 Shell 方法,但是在 RB 中并没有,所以此路不通。

3. 使用 ShellExecute

这个方法同样是一个系统 API,可以直接通过 RB 的 declare 声明并调用它,在测试之后,使用 declare 来使用系统 API 不会生成额外的 dll,正是我需要的。

首先在 RB 的某个模块中添加一个方法 ShellExecute,用来封装对系统 API 的请求:

function ShellExecute(hWnd as Integer, lpOperation as String, _
lpFile as String, lpParameters as String, lpDirectory as String, nShowCmd as Integer)

注意,在 RB 中添加方法时,参数列表中的 _ 需要去掉,这里是为了排版的需要而加上的。

这个 API 是定义在 shell32.dll 中的,在 ShellExecute 方法中先需要声明:

soft declare function ShellExecuteA Lib “shell32.dll” _
(ByVal hWnd As Integer, ByVal lpOperation As CString, _
ByVal lpFile As CString, ByVal lpParameters As CString, _
ByVal lpDirectory As CString, ByVal nShowCmd As Integer) As Integer

soft declare function ShellExecuteW Lib “shell32.dll” _
(ByVal hWnd As Integer, ByVal lpOperation As CString, _
ByVal lpFile As CString, ByVal lpParameters As CString, _
ByVal lpDirectory As CString, ByVal nShowCmd As Integer) As Integer

然后来调用这个 API:

  dim ret as Integer
    Try
      ret = ShellExecuteW(hWnd, ConvertEncoding(lpOperation, Encodings.UTF16), _
      ConvertEncoding(lpFile, Encodings.UTF16), _
      ConvertEncoding(lpParameters, Encodings.UTF16), _
      ConvertEncoding(lpDirectory, Encodings.UTF16), nShowCmd)
    Catch
      ret = ShellExecuteA(hWnd, lpOperation, lpFile, lpParameters, lpDirectory, nShowCmd)
    End
    return ret

把 ShellExecuteW 放在 Try … Catch 中是为了兼容的需要,因为像 Windows 98 这样比较老的系统中,系统内部编码还不是 Unicode,还没有 ShellExecuteW 这样结尾是 W 的 API。

另外,因为 RB 内部是使用的 UTF-8 编码,而系统 API ShellExecuteW 使用的编码是 Unicode,所以参数在传递前需要进行转码,将字符串参数值转换为 Unicode 编码,否则在执行一个明明正确的命令时,会出现“找不到文件”的错误。

好了,这样就可以使用 ShellExecute 方法来执行外部程序了,例如:

  #if TargetWin32
    dim ret as Integer
    ret = Win32Helper.ShellExecute(hWnd, "open", "cmd.exe", _
    "/c shutdown -t 10", "", 0)
  #endif

上面的代码就调用 cmd.exe 执行了命令 shutdown -s -t 10,也就是 10 秒后关机。

其实这个方法也可以用来打开文档或者网址,具体的用法可以去 MSDN 找一找。

当然,如果不在乎生成的 Windows 目标还有额外的 dll 的话,完全可以使用 RB 自带的 Shell 类,功能也强上不少。