设计驱动安装程序(一)

设计驱动安装程序(一)

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                      // 标志位,下详

    );

你可能感兴趣的:(设计驱动安装程序(一))