设计驱动安装程序(一)
Written by 张佩
mail: [email protected]
目录
硬件主导安装
软件主导安装
安装驱动包
安装设备
设备正连接
设备未连接
枚举系统设备
分析INF文件
DrvInst介绍
制作软件安装包
视图介绍
我们的工程
编译执行
上一章讲完了安装文件,在这一章中,笔者要和大家一起设计一个软件,用来安装驱动程序。软件安装,本质是都可以“绿色化”。我们知道,大部分Linux和Mac下的软件,都不需要安装,直接下载即可用。在Windows下需要写多少不等的注册表,将不同文件拷贝至系统目录,这些手动的操作比较繁琐,由安装软件或者一个批文件,代为完成,较为方便。驱动测试的时候,用不着安装程序,只要手动更新即可;一旦发布,就绝对需要了,因为驱动安装对于普通用户来说,是相当复杂的,要他们弄懂什么Inf文件、Inf目录、OEM、驱动库等概念,实在不容易。
但我相信,当读者看完这一章后,将觉得驱动安装程序,原来也容易!
硬件主导的设备安装,是指在安装程序尚未运行,系统尚无设备驱动信息的情况下,用户将设备连接到电脑上,这将导致“硬件主导”的设备安装方式。每当有新设备连接到系统中,系统PNP管理器都会尝试为设备寻找合适的驱动程序,如果能够找到,将尽可能地采用服务器端方式,悄无声息地为设备将驱动安装完毕。否则,将提示用户进行驱动安装指导。我们这一节,要讲述的是第二种情况下的处理。
和硬件主导的安装方式相反,软件主导的安装方式,是先软件安装先于设备接入。但由于软件无法预知自己到底是先还是后,所以他需要在运行之初,自己做准确的判断。一般来说,安装软件在运行之初,需要做一件事:判断当前系统中,PNP管理器是否正在为另一个设备安装忙碌中,如果这样的话,最好应当等待其完毕后,再进行本设备的安装。
可以非常简单地通过CMP_WaitNoPendingInstallEvents函数完成此判断。此函数存在于Win2K以后的系统中,如果要在更老系统中使用,亦可通过GetProcAddress的方法动态从SetupAPI.dll库中获取函数地址。
#Include <cfgmgr32.h>
// 此函数仅仅判断是否有其他的安装进程在运行中,
// 如果返回TRUE,说明有其他安装程序在运行,否则没有或者系统错误。
BOOL IsDeviceInstallInProgress (VOID)
{
return !(CMP_WaitNoPendingInstallEvents(0) == WAIT_OBJECT_0);
}
CMP_WaitNoPendingInstallEvents是配置管理器(CfgMgr)暴露出的API,从名字可以看出,他是在等待事件(Events)。从文档得知,PNP管理器在内部维持一个内核事件对象,当系统中所有的设备安装工作都结束的时候,此事件对象被设置为有信号状态。所以这个函数在内部实则是对WaitForSingleObject的封装。其参数是一个超时值,如果设置为0,就会立刻返回;否则就可能会等待。
驱动安装是有安装文件主导的。所以将安装文件安装到系统中,软件方面的安装就算完成。这一点是很容易实现的。调用SetupCopyOEMInf,将用户目录中的安装文件拷贝到Windows/Inf目录下,并以OEMxx形式命名。
BOOL WINAPI SetupCopyOEMInf(
__in PCTSTR SourceInfFileName, // 源INF文件路径
__in PCTSTR OEMSourceMediaLocation, // 其他驱动文件或源媒体路径
__in DWORD OEMSourceMediaType, // 源媒体类型
__in DWORD CopyStyle, // 拷贝类型,建议NOOVERWRITE
__out_opt PTSTR DestinationInfFileName, // 系统创建的OEMxx.inf路径
__in DWORD DestinationInfFileNameSize, // 上面缓冲区大小
__out_opt PDWORD RequiredSize, // 实际使用大小
__out_opt PTSTR DestinationInfFileNameComponent//下述
);
简单的参数这里不必说了,读者看上面注释即能明白。几个重要的参数详述如下:
OEMSourceMediaLocation 、OEMSourceMediaType
这两个参数同时使用,指定了和inf文件一起被创建的pnf文件中内容。大家知道,Inf文件只是驱动包中众多文件中的一个,其他的诸如sys、dll等文件,将如何处呢?系统为此在创建OEM Inf文件的时候,一道创建了一个称为预编译inf文件的文件,叫做.pnf文件。在pnf文件中指明了驱动包中其他文件的位置等信息。PNF的文件格式并不公开,并且似乎从来也无人专门做过研究,笔者到处找不到解读文档。但起码笔者可以较大家一招,多少能得到许多有用信息:以文本方式打开pnf文件,然后将字符编码格式选择为UTF-16LE,50%的内容将变得可读。特别是文件最后,我发现总是显示了源媒体路径和源安装文件名。
OEMSourceMediaType有三个可选值,一个是无源媒体路径;一个指明本地路径;一个指明URL网络路径。
另,OEMSourceMediaType设定为本地路径时,OEMSourceMediaLocation可设定为NULL。这样源INF文件所在目录将作为源媒体目录。
DestinationInfFileName、DestinationInfFileNameComponent
安装成功,或者失败但错误值为ERROR_FILE_EXISTS的情况下,此字符缓冲区中将返回系统创建的OEMxxx.inf文件全名,包含路径。而DestinationInfFileNameComponent恰好是一个字符指针,指向DestinationInfFileName路径中的文件名部分。例如,DestinationInfFileName为:c:/windows/inf/oem12.inf,则DestinationInfFileNameComponent即指向:oem12.inf。
举一个最简单的例子如下:
char asDesPath[MAX_PATH];
char* asDesName;
int nResult;
if(FALSE == SetupCopyOEMInf(
“c://tmp//cy001.inf”,
NULL, // 和SPOST_PATH一起使用时设为NULL,表示和inf文件同目录即c:/tmp
SPOST_PATH,
SP_COPY_NOOVERWRITE,
asDesPath,
MAX_PATH, NULL,
&asDesName))
{
if(GetLastError() == ERROR_FILE_EXISTS)
nResult = 1; // 已安装
else
nResult = -1; // 失败
}else
{
UDBG(g_InfList[nInfIndex]->asDesPath);
nResult = 0; // 成功
}
讲完了安装后,再提一提卸载。SetupCopyOEMInf有一个反函数SetupUninstallOEMInf。使用颇为简单,唯一的缺点是只能在XP以后使用。用户指定待删除的oem inf文件名,在成功的情况下,将删除inf、pnf以及可能存在的cat文件(cat文件是数字签名文件)。下面是一个简单例子。
if (!SetupUninstallOEMInf(
"oem12.inf", // 指明删除哪个inf文件,必须是INF目录中的文件
SUOI_FORCEDELETE, // 强制删除,即使有设备已经安装了此inf文件。
NULL))
{
if (GetLastError() == ERROR_INF_IN_USE_BY_DEVICES) {
UDBG("正在使用,无法删除");
} else if (GetLastError() == ERROR_NOT_AN_INSTALLED_OEM_INF) {
UDBG("找不到指定的OEM文件");
} else {
UDBG("失败");
}
代码很简单,而可能出现的主要错误也很明白。ERROR_INF_IN_USE_BY_DEVICES表明PNP管理器可能正在使用,无法删除;ERROR_NOT_AN_INSTALLED_OEM_INF表明无法找到对应的oem inf文件。
使用此函数的一个麻烦事是,首先要确定你要删除的oem文件准确的名字。这要费不少脑筋的。最好的办法是到注册表中软件键下读取。本文不详述了。
驱动包被安装成功后,并非万事大吉。驱动必须和设备关联起来,才能最终驱动设备。所以还有重要的一步要走:安装设备。最简单的情况呢,就是设备尚未安装,那么此时只要将设备和设备连接,系统就会自动开始新设备安装过程,并能定位到驱动包。
比较复杂的情况,则是如果设备已经安装过,此时需要更新,改当如何操作。因为设备一旦安装,系统并不会主动重新寻找新驱动(除非是Win7下,如在安装文件中设置了自动更新指令,系统是会定期到网络上检查驱动更新情况的。然其原理亦与此两样也)。分两种情况处理此问题:当前正连接在系统上的设备,当前不连接到系统的设备。
通过UpdateDriverForPlugAndPlayDevices并传入设备ID,通过返回值来判断设备是否已连接:函数返回TRUE;或函数返回FALSE,且GetLastError返回错误值ERROR_NO_MORE_ITEMS。除此两条,可知设备未曾连接。
UpdateDriverForPlugAndPlayDevices函数最大的作用是驱动更新。
BOOL WINAPI UpdateDriverForPlugAndPlayDevices(
IN HWND hwndParent OPTIONAL,
IN LPCTSTR HardwareId, // 更新啥设备?给出设备ID
IN LPCTSTR FullInfPath, // 更新成啥样?给出新的安装文件
IN DWORD InstallFlags, // 有啥要求?强迫还是从权?
OUT PBOOL bRebootRequired OPTIONAL // 返回重启标志
);
这个函数的Flags值需要强调说明,他可以是三种值的组合:
INSTALLFLAG_FORCE
强迫驱动更新,即使系统中当前的驱动,从版本上判断,比用户指定驱动新。这将导致旧驱动覆盖新驱动。但笔者写代码时,大部分时间都是将他填上的。
INSTALLFLAG_READONLY
只读标志。安装文件以及所有的驱动文件,只读,不可写、复制操作。此标志被设置后,系统不会将驱动文件拷贝或安装到系统中,将就地在当前目录完成安装。不建议使用此标志!
INSTALLFLAG_NONINTERACTIVE
无交互标志。如果安装程序不愿意更新过程中,有UI界面显示,可设置此标志。此标志一般用在没办法显示界面的环境下,比如Vista下的服务,没办法显示用户界面。所以一般情况下,不要设置此标志。
再讲bRebootRequired。他用来返回一个布尔量,表示驱动更新操作,是否需要系统重启。如果调用者忽略此值,则系统将在需要的情况下,提示用户重启;否则,仅返回重启标志,让用户决定。
下面是一个例子,通过C:/目录下的CY001.inf文件更新设备ID为“USB/VID_04B4&PID_1009”设备的驱动。
bSuccess = UpdateDriverForPlugAndPlayDevices(NULL,
“USB/VID_04B4&PID_1009”,
“c:/cy001.inf”,
INSTALLFLAG_FORCE,
&reboot);
if(bSuccess && reboot)
UDBG(“系统需要重启以完成更新”);
驱动程序安装好后,如果要更新改怎么做呢?这个小节我们就来讲。我们知道,系统为所有的设备建立了一棵设备树,每个设备都是数上的一个节点,叫DevNode。设备首次连接到系统中,设备节点就被建立了。即使将设备移除,系统为了效率起见,并不会将此设备节点也从设备树上删除,这将利于设备再次插入的时候,快速识别,省略了驱动寻找的过程。这个特点不利于我们这里的更新工作,因为当设备再次插入时,更新信息不会被系统使用。故而安装程序必须要找到这个节点,并为他打上“重新安装”的标记。这样设备再次插入,PNP管理器将为他重新寻找驱动。
1> 首先安装新驱动,调用SetupCopyOEMInf函数将安装文件拷贝到系统中。这是必要的,下面的三个步骤用来修改标记。
2> 找到要更新的设备。调用SetupDiGetClassDevs函数。由于设备未连接到系统,所以调用时要去除DIGCF_PRESENT标志。SetupDiGetClassDevs将返回一个所有符合条件的设备集。此时继续调用SetupDiEnumDeviceInfo对此设备集进行遍历操作,以得到每个设备的信息,通过设备ID或者兼容ID,来判断是否是要找的设备。
3> 检查设备节点DevNode状态。调用配置管理器函数CM_Get_DevNode_Status,并传入在第二步中得到的SP_DRVINFO_DATA结构体中的DevInst变量作为关键参数。不出意外的话,CM_Get_DevNode_Status的返回值应该是CR_NO_SUCH_DEVINST,表明DevNode虽存在设备却已移除。
4> 为DevNode打标记。首先获取现有的配置值,调用SetupDiGetDeviceRegistryProperty,并将属性参数设置为SPDRP_CONFIGFLAGS。将取得的配置值与CONFIGFLAG_REINSTALL(重安装也!)进行或操作,并调用相反的设置函数SetupDiSetDeviceRegistryProperty将新的配置值写入。
软件的更新操作完毕了,此时可以验收成果:插入设备,系统将重新安装驱动。代码如下:
// 更新驱动程序
int UpdateDriver(const char *inf_file)
{
HDEVINFO dev_info;
SP_DEVINFO_DATA dev_info_data;
INFCONTEXT inf_context;
HINF inf_handle = NULL;
DWORD config_flags, problem, status;
BOOL reboot;
char inf_path[MAX_PATH];
char id[MAX_PATH];
char tmp_id[MAX_PATH];
char *p;
int dev_index;
dev_info_data.cbSize = sizeof(SP_DEVINFO_DATA);
__try
{
// 取INF路径
if(!GetFullPathName(inf_file, MAX_PATH, inf_path, NULL))
{
UDBG(".inf文件 %s 未找到", inf_file);
__leave;
}
// 打开INF文件
inf_handle = SetupOpenInfFile(inf_path, NULL, INF_STYLE_WIN4, NULL);
if(inf_handle == INVALID_HANDLE_VALUE)
{
UDBG("打开文件失败:%s", inf_file);
__leave;
}
// 搜索设备描述符
if(!SetupFindFirstLine(inf_handle, "Devices", NULL, &inf_context))
{
UDBG("没有可用设备描述符");
__leave;
}
do {
// 一行行搜索
if(!SetupGetStringField(&inf_context, 2, id, sizeof(id), NULL))
{
continue;
}
strlwr(id);
reboot = FALSE;
// 拷贝INF文件
SetupCopyOEMInf(inf_path, NULL, SPOST_PATH, 0, NULL, 0, NULL, NULL);
// 为当前连接到系统的所有设备更新驱动
// 下面再搜索那些未连接的设备,更新他们的驱动。
UpdateDriverForPlugAndPlayDevices(NULL, id, inf_path, INSTALLFLAG_FORCE,
&reboot);
// 寻找所有设备,设置Flag值为DIGCF_ALLCLASSES。
dev_info = SetupDiGetClassDevs(NULL, "USB", NULL, DIGCF_ALLCLASSES);
if(dev_info == INVALID_HANDLE_VALUE)
{
break;
}
dev_index = 0;
// 枚举设备
while(SetupDiEnumDeviceInfo(dev_info, dev_index, &dev_info_data))
{
// 先取设备ID,判断是否是当前更新设备
if(SetupDiGetDeviceRegistryProperty(dev_info, &dev_info_data,
SPDRP_HARDWAREID, NULL,
(BYTE *)tmp_id,
sizeof(tmp_id), NULL))
{
//获得的是一个字符串列表,故而需要遍历
for(p = tmp_id; *p; p += (strlen(p) + 1))
{
strlwr(p);
if(strstr(p, id))
{
// 判断此设备是不是当前未连接在系统
if(CM_Get_DevNode_Status(&status,
&problem,
dev_info_data.DevInst,
0) == CR_NO_SUCH_DEVINST)
{
// 取当前配置值
if(SetupDiGetDeviceRegistryProperty(dev_info,
&dev_info_data,
SPDRP_CONFIGFLAGS,
NULL,
(BYTE *)&config_flags,
sizeof(config_flags),
NULL))
{
// 与CONFIGFLAG_REINSTALL或
config_flags |= CONFIGFLAG_REINSTALL;
// 将新的配置值写入
SetupDiSetDeviceRegistryProperty(dev_info,
&dev_info_data,
SPDRP_CONFIGFLAGS,
(BYTE *)&config_flags,
sizeof(config_flags));
}
}
break;
}
}
}
dev_index++;
}
SetupDiDestroyDeviceInfoList(dev_info);
// 获取下一个设备ID
} while(SetupFindNextLine(&inf_context, &inf_context));
}
__finally
{
if(inf_handle)
SetupCloseInfFile(inf_handle);
}
return 0;
}
上面代码中,涉及到了另外两个技术要点,这里略述如下: 枚举系统设备
技术关键是:调用SetupDiGetClassDevs获取系统设备信息集合(Device Information Set),然后调用SetupDiEnumDeviceInfo枚举出集合中的每一个设备。设备信息集合是一个未文档化的结构体,用句柄HDEVINFO表示;集合内的每个设备成员,以结构体SP_DEVINFO_DATA描述,则是个文档化结构体;此外,设备成员内部还包含多个设备接口,以SP_DEVICE_INTERFACE_DATA描述。下面是这两个结构体的定义:
typedef struct _SP_DEVINFO_DATA {
DWORD cbSize;
GUID ClassGuid; // 设备类GUID
DWORD DevInst; // DevNode句柄
ULONG_PTR Reserved;
} SP_DEVINFO_DATA, *PSP_DEVINFO_DATA;
和
typedef struct _SP_DEVICE_INTERFACE_DATA {
DWORD cbSize;
GUID InterfaceClassGuid; // 设备接口GUID
DWORD Flags; // 接口标志:活动、移除、默认
ULONG_PTR Reserved;
} SP_DEVICE_INTERFACE_DATA, *PSP_DEVICE_INTERFACE_DATA;
下图是笔者从MSDN中截取的一张设备信息集合和设备成员的示意图,笔者做了一定的整理,相信会有助于大家对这些API的理解。
图X 设备信息集合
下面是对这三个函数的讲解:
HDEVINFO SetupDiGetClassDevs(
IN LPGUID ClassGuid, OPTIONAL // 类GUID
IN PCTSTR Enumerator, OPTIONAL // 枚举子
IN HWND hwndParent, OPTIONAL // 父设备
IN DWORD Flags // 标志位,下详
);