2006年12月19日

shift键妙用如下:


 

当你面对一大堆窗口,却要一个一个把它们关掉时。是不是很烦啊。只要你按shift 键在单击关闭按扭,所以的窗口就会全部关掉。

 

 

在输入大小写字母时,按shift 键,就可以改变其大小写!


 

当安装了某个新软件,有时要从新启动计算机才有用,只要先按shift 键,就可以跳过计算机的自检节省了大量的时间!

 

 

选择文件时,先按shift 键,在选最后一个文件,可以选中一大批。

 

 

删除文件时,按shift 键可以直接删除。不经过回收站。

 

 

放光碟时,连按数下shift 键,可以跳过自动播放!

 

 

shift +F10可以代替鼠标右键。

 

 

打开文件时,如果你不想用默认方式打开,按shift 键,在单击右键,在右边的菜单上就多出了打开方式下面的你就自己去做吧!

 

 

shift 键,点击超级连接,可以打开新窗口。

当你用QQ和别人聊天时,是不是有时信息发送的特别慢呀,不要紧,只要你发信息时按shift 键信息就会很快的发送出去的!

shift键妙用如下:


 

当你面对一大堆窗口,却要一个一个把它们关掉时。是不是很烦啊。只要你按shift 键在单击关闭按扭,所以的窗口就会全部关掉。

 

 

在输入大小写字母时,按shift 键,就可以改变其大小写!


 

当安装了某个新软件,有时要从新启动计算机才有用,只要先按shift 键,就可以跳过计算机的自检节省了大量的时间!

 

 

选择文件时,先按shift 键,在选最后一个文件,可以选中一大批。

 

 

删除文件时,按shift 键可以直接删除。不经过回收站。

 

 

放光碟时,连按数下shift 键,可以跳过自动播放!

 

 

shift +F10可以代替鼠标右键。

 

 

打开文件时,如果你不想用默认方式打开,按shift 键,在单击右键,在右边的菜单上就多出了打开方式下面的你就自己去做吧!

 

 

shift 键,点击超级连接,可以打开新窗口。

当你用QQ和别人聊天时,是不是有时信息发送的特别慢呀,不要紧,只要你发信息时按shift 键信息就会很快的发送出去的!

2006年09月16日

                                        

 

  使用五笔一定要坚持下来,不出一个月就会很熟练了。开始可以借助“五笔打字员(点击此处下载)”练习,然后试着用五笔和别人聊天,建议使用“智能陈桥五笔(点击此处下载)”输入法,当遇到不会拆的字,可以在“智能陈桥五笔”里用拼音输入,它会提示你该字的字根的。  

  11G   王旁青头戋五一
  12F   土士二干十寸雨
  13D   大犬三(羊)古石厂   (“羊”指羊字底)
  14S   木丁西
  15A   工戈草头右框七        (“右框”即“匚”)

  21H   目具上止卜虎皮   (“具”指具字的上部)
  22J   日早两竖与虫依
  23K   口与川,字根稀
  24L   田甲方框四车力   (“方框”即“囗”)
  25M   山由贝,下框几

  31T   禾竹一撇双人立,反文条头共三一  (“条头”即“夂”)
  32R   白手看头三二斤
  33E   月彡(衫)乃用家衣底
  34W   人和八,三四里      
  35Q   金勺缺点无尾鱼,犬旁留儿一点夕,氏无七(妻)

  41Y   言文方广在四一,高头一捺谁人去
  42U   立辛两点六门病(疒)
  43I   水旁兴头小倒立
  44O   火业头,四点米   
  45P   之宝盖,摘 礻(示)衤(衣)

  51N   已半巳满不出己,左框折尸心和羽
  52B   子耳了也框向上
  53V   女刀九臼山朝西(彐)
  54C   又巴马,丢矢矣(厶)
  55X   慈母无心弓和匕,幼无力(幺)

2006年08月07日

我们知道NT以后的系统中有一个这样的服务:Protected Storage。他是用来储存本地密码和网上服务密码的服务,包括填表时的“自动完成”功能以及OUTLOOK对应的帐号信息。相应的我们就可以通过该服务来获取保存的帐号信息。 源代码如下:

///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////
// //
//Protected Storage Explorer //
// By Hirosh //
//www.hirosh.net //
// //
// //
//No CopyRights- Feel Free to Cut & Paste //
// //
// //
///////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////

#include "stdafx.h"
#include <commctrl.h>
#include "resource.h"
#import "pstorec.dll" no_namespace har SavingFname[MAX_PATH];
HWND hwndlistview;
BOOL iS9x=FALSE;
typedef struct TOOUTDATA{
char POPuser[100];
char POPpass[100];
char POPserver[100];
} OOUTDATA;
OOUTDATA OutlookData[50];
int oIndex=0;

void EnumOutlookAccounts()
{
ZeroMemory(OutlookData,sizeof(OutlookData));
HKEY hkeyresult ,hkeyresult1;
long l,i;
char name[200],skey[200];
DWORD dw2;
FILETIME f;
lstrcpy(skey,"Software\\Microsoft\\Internet Account Manager\\Accounts");
LONG lResult=RegOpenKeyEx(HKEY_CURRENT_USER, ( LPCTSTR ) skey,0,KEY_ALL_ACCESS, &hkeyresult1 );
if(ERROR_SUCCESS != lResult)
return ;
i=0;l=0;
BYTE Data[150];
BYTE Data1[150];
DWORD size;
int j;
j=0;
DWORD type=REG_BINARY;
while(l!=ERROR_NO_MORE_ITEMS){
dw2=200;
l=RegEnumKeyEx(hkeyresult1,i,name,&dw2,NULL,NULL,NULL,&f);
lstrcpy(skey,"Software\\Microsoft\\Internet Account Manager\\Accounts");
lstrcat(skey,"\\");
lstrcat(skey,name);
RegOpenKeyEx(HKEY_CURRENT_USER, ( LPCTSTR )skey ,0,KEY_ALL_ACCESS, &hkeyresult );
size=sizeof(Data);
if(RegQueryValueEx ( hkeyresult, ( LPCTSTR )"HTTPMail User Name" , 0, &type, Data, &size )==ERROR_SUCCESS)
{
lstrcpy(OutlookData[oIndex].POPuser,(char *)Data);
ZeroMemory(Data,sizeof(Data));
lstrcpy(OutlookData[oIndex].POPserver,"Hotmail");
size=sizeof(Data);
if(RegQueryValueEx ( hkeyresult, ( LPCTSTR )"HTTPMail Password2" , 0, &type, Data1, &size ) ==ERROR_SUCCESS){
int totnopass=0;
char mess[100];
for(int i=2;i<size;i++)
if(IsCharAlphaNumeric(Data1[i])||(Data1[i]==’(‘)||(Data1[i]==’)')||(Data1[i]==’.')||(Data1[i]==’ ‘)||(Data1[i]==’-')){
OutlookData[oIndex].POPpass[totnopass]=Data1[i];
totnopass++;
}
OutlookData[oIndex].POPpass[totnopass]=0;
}
ZeroMemory(Data1,sizeof(Data));
oIndex++;
}
else if(RegQueryValueEx ( hkeyresult, ( LPCTSTR )"POP3 User Name" , 0, &type, Data, &size )==ERROR_SUCCESS)
{
lstrcpy(OutlookData[oIndex].POPuser,(char *)Data);
ZeroMemory(Data,sizeof(Data));
size=sizeof(Data);
RegQueryValueEx ( hkeyresult, ( LPCTSTR )"POP3 Server" , 0, &type, Data, &size ) ;
lstrcpy(OutlookData[oIndex].POPserver,(char *)Data);
ZeroMemory(Data,sizeof(Data));
size=sizeof(Data);
if(RegQueryValueEx ( hkeyresult, ( LPCTSTR )"POP3 Password2" , 0, &type, Data1, &size ) ==ERROR_SUCCESS){
int totnopass=0;
char mess[100];
for(int i=2;i<size;i++)
if(IsCharAlphaNumeric(Data1[i])||(Data1[i]==’(‘)||(Data1[i]==’)')||(Data1[i]==’.')||(Data1[i]==’ ‘)||(Data1[i]==’-')){
OutlookData[oIndex].POPpass[totnopass]=Data1[i];
totnopass++;
}
OutlookData[oIndex].POPpass[totnopass]=0;
}
ZeroMemory(Data1,sizeof(Data1));
oIndex++;
}
j++;i++;
}
}

void SaveToDisk(char *buf){
DWORD dwBytes;
HANDLE hf = CreateFile (SavingFname, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
SetFilePointer(hf,0 ,NULL,FILE_END);
WriteFile (hf, (LPVOID)buf,strlen(buf), &dwBytes, NULL);
CloseHandle(hf);
}

BOOL AddItemm(BOOL Save,char *resname,char *restype,char *usrname,char *pass)
{
if(!Save){
LVITEM lvi;
lvi.mask = LVIF_TEXT;
lvi.state = LVIS_SELECTED ;
lvi.stateMask = 0;

lvi.iItem = 10000;
lvi.iSubItem = 0;
lvi.pszText = "";

int i = ListView_InsertItem(hwndlistview, &lvi);
if(!iS9x){
ListView_SetItemText(hwndlistview, i, 0, resname);
ListView_SetItemText(hwndlistview, i, 1, restype);
ListView_SetItemText(hwndlistview, i, 2, usrname);
ListView_SetItemText(hwndlistview, i, 3, pass);
}
else{
ListView_SetItemText(hwndlistview, i, 0, usrname);
ListView_SetItemText(hwndlistview, i, 1, pass);
}
SetFocus(hwndlistview);
ListView_SetItemState (hwndlistview,i,LVIS_FOCUSED | LVIS_SELECTED, 0×000F);
ListView_SetSelectionMark(hwndlistview,i);
}
else{
if(!iS9x){
SaveToDisk("\r\n");
SaveToDisk("<tr><td>");
SaveToDisk(resname);
SaveToDisk("</td><td>");
SaveToDisk(restype);
SaveToDisk("</td><td>");
SaveToDisk(usrname);
SaveToDisk("</td><td>");
SaveToDisk(pass);
SaveToDisk("</td></tr>");
SaveToDisk("\r\n");
}
else{
SaveToDisk("\r\n");
SaveToDisk("<tr><td>");
SaveToDisk(usrname);
SaveToDisk("</td><td>");
SaveToDisk(pass);
SaveToDisk("</td></tr>");
SaveToDisk("\r\n");
}
}
return TRUE;
}

void EnumPStorage(BOOL Save){
typedef HRESULT (WINAPI *tPStoreCreateInstance)(IPStore **, DWORD, DWORD, DWORD);
HMODULE hpsDLL;
hpsDLL = LoadLibrary("pstorec.dll");

tPStoreCreateInstance pPStoreCreateInstance;
pPStoreCreateInstance = (tPStoreCreateInstance)GetProcAddress(hpsDLL, "PStoreCreateInstance");

IPStorePtr PStore;
HRESULT hRes = pPStoreCreateInstance(&PStore, 0, 0, 0);

IEnumPStoreTypesPtr EnumPStoreTypes;
hRes = PStore->EnumTypes(0, 0, &EnumPStoreTypes);

if (!FAILED(hRes))
{

GUID TypeGUID;
char szItemName[512];
char szItemData[512];
char szResName[1512];
char szResData[512];
char szItemGUID[50];

while(EnumPStoreTypes->raw_Next(1,&TypeGUID,0) == S_OK){
wsprintf(szItemGUID,"%x",TypeGUID);

IEnumPStoreTypesPtr EnumSubTypes;
hRes = PStore->EnumSubtypes(0, &TypeGUID, 0, &EnumSubTypes);

GUID subTypeGUID;
while(EnumSubTypes->raw_Next(1,&subTypeGUID,0) == S_OK){

IEnumPStoreItemsPtr spEnumItems;
HRESULT hRes = PStore->EnumItems(0, &TypeGUID, &subTypeGUID, 0, &spEnumItems);

LPWSTR itemName;
while(spEnumItems->raw_Next(1,&itemName,0) == S_OK){
wsprintf(szItemName,"%ws",itemName);
char chekingdata[200];
unsigned long psDataLen = 0;
unsigned char *psData = NULL;
_PST_PROMPTINFO *pstiinfo = NULL;
hRes = PStore->ReadItem(0,&TypeGUID,&subTypeGUID,itemName,&psDataLen,&psData,pstiinfo,0);
if(lstrlen((char *)psData)<(psDataLen-1))
{
int i=0;
for(int m=0;m<psDataLen;m+=2){
if(psData[m]==0)
szItemData[i]=’,';
else
szItemData[i]=psData[m];
i++;
}
szItemData[i-1]=0;
}
else {
wsprintf(szItemData,"%s",psData);
}
lstrcpy(szResName,"");
lstrcpy(szResData,"");
//220d5cc1 Outlooks
if(lstrcmp(szItemGUID,"220d5cc1")==0){
BOOL bDeletedOEAccount=TRUE;
for(int i=0;i<oIndex;i++){
if(lstrcmp(OutlookData[i].POPpass,szItemName)==0){
bDeletedOEAccount=FALSE;
AddItemm(Save,OutlookData[i].POPserver,"OutlookExpress",OutlookData[i].POPuser,szItemData);
break;
}
}
if(bDeletedOEAccount)
AddItemm(Save,szItemName,"Deleted OE Account",OutlookData[i].POPuser,szItemData);
}
//5e7e8100 – IE:Password-Protected sites
if(lstrcmp(szItemGUID,"5e7e8100")==0){
lstrcpy(chekingdata,"");
if(strstr(szItemData,":")!=0){
lstrcpy(chekingdata,strstr(szItemData,":")+1);
*(strstr(szItemData,":"))=0;
}
AddItemm(Save,szItemName,"IE:Password-Protected sites",szItemData,chekingdata);
}
// b9819c52 MSN Explorer Signup
if(lstrcmp(szItemGUID,"b9819c52")==0){
char msnid[100];
char msnpass[100];
int i=0;
BOOL first=TRUE;
for(int m=0;m<psDataLen;m+=2){
if(psData[m]==0){
szItemData[i]=’,';
i++;
}
else{
if(IsCharAlphaNumeric(psData[m])||(psData[m]==’@')||(psData[m]==’.')||(psData[m]==’_')){
szItemData[i]=psData[m];
i++;
}
}
}
szItemData[i-1]=0;
char *p;
p=szItemData+2;
//psData[4] – number of msn accounts
for(int ii=0;ii<psData[4];ii++){
lstrcpy(msnid,p+1);
if(strstr(msnid,",")!=0) *strstr(msnid,",")=0;
if(strstr(p+1,",")!=0)
lstrcpy(msnpass,strstr(p+1,",")+2);
if(strstr(msnpass,",")!=0) *strstr(msnpass,",")=0;
p=strstr(p+1,",")+2+lstrlen(msnpass)+7;
AddItemm(Save,msnid,"MSN Explorer Signup",msnid,msnpass);
}

}
//e161255a IE
if(lstrcmp(szItemGUID,"e161255a")==0){
if(strstr(szItemName,"StringIndex")==0){
if(strstr(szItemName,":String")!=0) *strstr(szItemName,":String")=0;
lstrcpyn(chekingdata,szItemName,8);
if((strstr(chekingdata,"http:/")==0)&&(strstr(chekingdata,"https:/")==0))
AddItemm(Save,szItemName,"IE Auto Complete Fields",szItemData,"");

else{
lstrcpy(chekingdata,"");
if(strstr(szItemData,",")!=0){
lstrcpy(chekingdata,strstr(szItemData,",")+1);
*(strstr(szItemData,","))=0;
}
AddItemm(Save,szItemName,"AutoComplete Passwords",szItemData,chekingdata);
}
}}
ZeroMemory(szItemName,sizeof(szItemName));
ZeroMemory(szItemData,sizeof(szItemData));
}
}
}
}

}
//////////////////Cashed PAsses- 9x
struct PASSWORD_CACHE_ENTRY { WORD cbEntry; WORD cbResource; WORD cbPassword; BYTE iEntry;BYTE nType; char abResource[1];};
typedef BOOL (FAR PASCAL *CACHECALLBACK)( struct PASSWORD_CACHE_ENTRY FAR *pce, DWORD dwRefData );
DWORD APIENTRY WNetEnumCachedPasswords(LPSTR pbPrefix,WORD cbPrefix,BYTE nType,CACHECALLBACK pfnCallback,DWORD dwRefData);
typedef DWORD (WINAPI *ENUMPASSWORD)(LPSTR pbPrefix, WORD cbPrefix, BYTE nType, CACHECALLBACK pfnCallback, DWORD dwRefData);
ENUMPASSWORD pWNetEnumCachedPasswords;
typedef struct {char *pBuffer;int nBufLen;int nBufPos;} PASSCACHECALLBACK_DATA;

BOOL PASCAL AddPass(struct PASSWORD_CACHE_ENTRY FAR *pce, DWORD dwRefData)
{
char buff[1024],buff2[1024];
int nCount;
PASSCACHECALLBACK_DATA *dat;
dat = (PASSCACHECALLBACK_DATA *)dwRefData;
nCount=pce->cbResource+1;
if(nCount>1023) nCount=1023;
lstrcpyn(buff, pce->abResource, nCount);
buff[nCount] = 0;
CharToOem(buff, buff2);
if((dat->nBufPos+lstrlen(buff2))>=dat->nBufLen) return FALSE;
lstrcpy(dat->pBuffer+dat->nBufPos,buff2);
dat->nBufPos+=lstrlen(buff2)+1;

nCount=pce->cbPassword+1;
if(nCount>1023) nCount=1023;
lstrcpyn(buff, pce->abResource+pce->cbResource, nCount);
buff[nCount] = 0;
CharToOem(buff, buff2);
if((dat->nBufPos+lstrlen(buff2))>=dat->nBufLen) return FALSE;
lstrcpy(dat->pBuffer+dat->nBufPos,buff2);
dat->nBufPos+=lstrlen(buff2)+1;

return TRUE;
}
void CashedPass(BOOL Save)
{
HMODULE hLib=LoadLibrary("MPR.DLL");

PASSCACHECALLBACK_DATA dat;
dat.pBuffer=(char *)malloc(65536);
dat.nBufLen=65536;
dat.nBufPos=0;
pWNetEnumCachedPasswords = (ENUMPASSWORD)GetProcAddress(hLib, "WNetEnumCachedPasswords");

pWNetEnumCachedPasswords(NULL, 0, 0xff, AddPass, (DWORD) &dat);
char *svStr;
svStr=dat.pBuffer;
do {
char *svRsc=svStr;
svStr+=lstrlen(svStr)+1;
char *svPwd=svStr;
svStr+=lstrlen(svStr)+1;
char szUser[1024];
char szPass[1024];
AddItemm(Save,"","",svRsc,svPwd);
}while(*svStr!=’\0′);

FreeLibrary(hLib);

};
/////////////////////////////////////////
#define TableHeader "<p><b><font color=\"#FF0000\"></font></b></p><table border=\"1\" cellpadding=\"0\" cellspacing=\"0\"style=\"border-collapse: collapse\" bordercolor=\"#111111\" width=\"100%\" id=\"AutoNumber1\">"
#define Table "</table>"
#include <commdlg.h>
LRESULT CALLBACK DLgProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
OPENFILENAME ofn;
char szFile[MAX_PATH];
switch (message){

case WM_INITDIALOG:
SendMessage(hDlg, WM_SETICON, ICON_SMALL, (LPARAM)LoadIcon(GetModuleHandle(0), MAKEINTRESOURCE(IDI_ICON1)));
if(!iS9x)
SetWindowText(hDlg,"Protected Storage www.hirosh.NET");
else SetWindowText(hDlg,"Cashed Passwords www.hirosh.NET");

hwndlistview = GetDlgItem(hDlg, IDC_LIST3);
LVCOLUMN lvcol;
if(!iS9x){
lvcol.mask =LVCF_TEXT;;
lvcol.pszText = "Resource Name";
ListView_InsertColumn(hwndlistview, 0, &lvcol);
ListView_SetColumnWidth(hwndlistview, 0, 160);

lvcol.mask =LVCF_TEXT;
lvcol.pszText = "Resource Type";
ListView_InsertColumn(hwndlistview, 1, &lvcol);
ListView_SetColumnWidth(hwndlistview, 1, 110);

lvcol.mask =LVCF_TEXT;
lvcol.pszText = "User Name/Value";
ListView_InsertColumn(hwndlistview, 2, &lvcol);
ListView_SetColumnWidth(hwndlistview, 2, 200);

lvcol.mask =LVCF_TEXT;
lvcol.pszText = "Password";
ListView_InsertColumn(hwndlistview, 3, &lvcol);
ListView_SetColumnWidth(hwndlistview, 3, 100);
EnumOutlookAccounts();
EnumPStorage(FALSE);
}
else{
lvcol.mask =LVCF_TEXT;
lvcol.pszText = "User Name/Value";
ListView_InsertColumn(hwndlistview, 0, &lvcol);
ListView_SetColumnWidth(hwndlistview, 0, 250);

lvcol.mask =LVCF_TEXT;
lvcol.pszText = "Password";
ListView_InsertColumn(hwndlistview, 1, &lvcol);
ListView_SetColumnWidth(hwndlistview, 1, 150);
CashedPass(FALSE);
}
ListView_SetExtendedListViewStyle(hwndlistview,LVS_EX_FULLROWSELECT);

return TRUE;

case WM_COMMAND:
switch ( LOWORD(wParam) ){

case IDOK:
ZeroMemory(&ofn, sizeof(OPENFILENAME));
ofn.lStructSize = sizeof(OPENFILENAME);
ofn.hwndOwner = hDlg;lstrcpy(szFile,"*.*");
ofn.lpstrFile ="pstectedstorage.htm";ofn.nMaxFile = sizeof(szFile);
ofn.lpstrFilter = "Htm\0*.htm\0";
ofn.nFilterIndex = 1;ofn.lpstrFileTitle = NULL;
ofn.nMaxFileTitle = 0;ofn.lpstrInitialDir = NULL;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST;
if (GetSaveFileName(&ofn)==TRUE) {
lstrcpy(SavingFname,ofn.lpstrFile);
if(strstr(SavingFname,".htm")==0)lstrcat(SavingFname,".htm");
SaveToDisk(TableHeader);
if(!iS9x){
SaveToDisk("<tr><td><b><font color=\"#FF0000\">Resource Name </font></b></td><td><b><font color=\"#FF0000\">Resource Type </font></b></td><td><b><font color=\"#FF0000\">User Name/Value</font></b></td><td><b><font color=\"#FF0000\">Password</font></b></td></tr>");
EnumOutlookAccounts();
EnumPStorage(TRUE);
}
else{
SaveToDisk("<tr><td><b><font color=\"#FF0000\">User Name/Value</font></b></td><td><b><font color=\"#FF0000\">Password</font></b></td></tr>");
CashedPass(TRUE);
}
SaveToDisk(Table);
}
break;
case IDCANCEL:
EndDialog(hDlg, LOWORD(wParam));
ExitProcess(0);
break;

break;
}
}

return FALSE;
}
//
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)

{
if((int)GetVersion() < 0) iS9x=TRUE;
else iS9x=FALSE;
if(lpCmdLine[0]==NULL){
InitCommonControls();
DialogBox(hInstance, (LPCTSTR)IDD_DIALGMAIN, 0, (DLGPROC)DLgProc);
}
else {
lstrcpy(SavingFname,lpCmdLine);
SaveToDisk(TableHeader);
if(!iS9x){
SaveToDisk("<tr><td><b><font color=\"#FF0000\">Resource Name </font></b></td><td><b><font color=\"#FF0000\">Resource Type </font></b></td><td><b><font color=\"#FF0000\">User Name/Value</font></b></td><td><b><font color=\"#FF0000\">Password</font></b></td></tr>");
EnumOutlookAccounts();
EnumPStorage(TRUE);
}
else{
SaveToDisk("<tr><td><b><font color=\"#FF0000\">User Name/Value</font></b></td><td><b><font color=\"#FF0000\">Password</font></b></td></tr>");
CashedPass(TRUE);
}
SaveToDisk(Table);

}

return 0;
}

http://www.msssm.com/?u=28994

 昨天公司让做一个小东东,需求如下:

    一个网页(http://app1.sda.gov.cn/webportal/portal.po?UID=DWV1_WOUID_URL_6646923&amp;&hsearchOption=clientSearch&hsearchSubOption=searchResult)上面有个表单,表单里有个下拉框和一个输入框,method为“POST”。要求用软件实现在网页上输入时相同的效果,并取回结果表格里的内容,保存到MySql里。返回值的网页在当前页面打开,并且Form的action仍为上述地址。图片如下:

提交前:

提交后:
    
    刚开始,我考虑用WebBrowser控件,打开这个网页后,得到其中的下拉框和输入框对象,赋值后提交这个表单。程序里涉及到以下几个接口:
IHTMLDocument2
IHTMLElementCollection
IHTMLElement
IHTMLFormElement
IHTMLInputElement
IHTMLSelectElement

我用 Doc:=WebBrowser1.Document as IHTMLDocument2 得到把打开后的页面做为实现IHTMLDocument2 接口的对象来处理。这是一个与诸多网页内容相关的接口,如 Doc.frames.length 表示网页的框架个数,Doc.body.OuterText可以得到页面的源代码,aElementCollection:=Doc.GetAll返回页面上所有的对象(或元素)等。Dispatch:=aElementCollection.Item()方法以元素名字或索引来引用某个元素(Dispatch为一个IDispatch接口变量)。例如,可以用以下语句
    if SUCCEEDED(Dispatch.QueryInterface(IHTMLFormElement,HTMLFormElement))then…
来检查网页上是否存在表单(HTMLFormElement是一个IHTMLFormElement接口变量)。如果存在,则检查表单的名称是否为要求的值:
    if HTMLFormElement.name=’frmClientSearch’ then …
如果都符合条件,则可用
    HTMLFormElement.submit
方法向服务器提交这个表单。

    实际编写程序时,遇到了两个问题:
    1、当把取得结果页面的源代码的代码放在按钮事件里时,表单处理地址和当前页面地址一样,并且提交后仍然返回当前页面的地址。提交后,WebBrowser1.LocationURL 得到的不再是原来的地址,并且用
   while WebBrowser1.ReadyState <READYSTATE_COMPLETE  do  Application.ProcessMessages
等待页面显示完毕时不起作用,把代码放在WebBrowser1的OnDocumentComplete事件里可以得到。
    2、速度太慢,因为要下载整个网页。

    于是放弃这种方法,考虑采用IdHTTP控件来直接向服务器POST表单数据。
    打开 WSockExpert ,找到刚才用WebBrowser1编写的程序(直接监视IE数据时,由于滚动太快,抓不住POST请求的数据包。显示内容的列表不能向上滚动,而是把最开始的记录覆盖了,估计是软件的bug,毕业用的是beta版本),提交一次,找到记录下来的如下内容:

POST /webportal/portal.po?UID=DWV1_WOUID_URL_6646923&amp;&&&hsearchOption=clientSearch&hsearchSubOption=searchResult HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, application/x-shockwave-flash, */*
Referer: http://app1.sda.gov.cn/webportal/portal.po?UID=DWV1_WOUID_URL_6646923&amp;&&hsearchOption=clientSearch&hsearchSubOption=searchResult
Accept-Language: zh-cn
Content-Type: application/x-www-form-urlencoded
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)
Host: app1.sda.gov.cn
Content-Length: 38
Connection: Keep-Alive
Cache-Control: no-cache

ListSelect=TBL_TABLE283&searchClient=1

其中 ListSelect 即为表单上下拉框的名字,TBL_TABLE283 是其值;searchClient 是输入框的名字,“1”是输入的要查询的内容。
从上面我们看到提交表单后,用POST方式向服务器发送了 ListSelect=TBL_TABLE283&searchClient=1 这么一些信息。于是很自然地用IdHTTP控件向这个地址发送这些内容即可达到目的。
    往窗体上拖一个 IdHTTP 控件,属性不用更改。源代码如下:

unit frmGetInfo;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient,
  IdHTTP, StdCtrls, IdAntiFreezeBase, IdAntiFreeze, ComCtrls, ExtCtrls, DB,
  MemDS, DBAccess, MyAccess;

type
  TProgressInfo=record
    Verify_Code:string;//核对码
    Accept_case_code:string;//受理号
    Corp_name:string;//企业名称
    Transact_State:string;//办理状态
    Drug_Approve_Code:string;//药品批准文号
    State_date:string;//状态开始时间
    Notice_date:string;//通知时间
    Notice_content:string;//通知内容
    Charge_Circs:string;//收费情况
    Charge_date:string;//费用收到日期
    Checkout_report_date:string;//检验报告收到日期
    Criterion_hodometer_date:string;//标准品回执收到日期
  end;

type
  TGetInfo = class(TForm)
    http: TIdHTTP;
    btnSearch: TButton;
    IdAntiFreeze1: TIdAntiFreeze;
    ProgressBar1: TProgressBar;
    GroupBox1: TGroupBox;
    edtVerify_Code: TLabeledEdit;
    edtAccept_case_code: TLabeledEdit;
    edtCorp_name: TLabeledEdit;
    edtTransact_State: TLabeledEdit;
    edtDrug_Approve_Code: TLabeledEdit;
    edtState_date: TLabeledEdit;
    edtNotice_date: TLabeledEdit;
    edtNotice_content: TLabeledEdit;
    edtCharge_Circs: TLabeledEdit;
    edtCharge_date: TLabeledEdit;
    edtCheckout_report_date: TLabeledEdit;
    edtCriterion_hodometer_date: TLabeledEdit;
    btnClose: TButton;
    procedure httpWorkBegin(Sender: TObject; AWorkMode: TWorkMode;
      const AWorkCountMax: Integer);
    procedure httpWork(Sender: TObject; AWorkMode: TWorkMode;
      const AWorkCount: Integer);
    procedure btnSearchClick(Sender: TObject);
    procedure btnCloseClick(Sender: TObject);
  private
    { Private declarations }
    FResultInfo:TProgressInfo;
    FSuccess:boolean;
    procedure DoSetEditValue;
  public
    { Public declarations }
    Verify_Code:string;
    function CheckConnection(sURL:string):boolean;
    function GetProgressInfo(sVerify_Code:string):TProgressInfo;
    function SaveToDb(aMyQuery:TMyQuery):boolean;
  end;

resourcestring
  PostURL=’http://app1.sda.gov.cn/webportal/portal.po?UID=DWV1_WOUID_URL_6646923&amp;&hsearchOption=clientSearch&hsearchSubOption=searchResult’;
  UpdateSql=’update Corp_reg_Config set Accept_case_code=”%s”,Corp_name=”%s”,Transact_State=”%s”,’+
            ‘Drug_Approve_Code=”%s”, State_date=”%s”,Notice_date=”%s”,Notice_content=”%s”,Charge_Circs=”%s”,’+
            ‘Charge_date=”%s”,Checkout_report_date=”%s”,Criterion_hodometer_date=”%s” ‘+
            ‘where Verify_Code=”%s”’;

var
  GetInfo: TGetInfo;

implementation

{$R *.dfm}

{ TGetInfo }

function TGetInfo.CheckConnection(sURL: string): boolean;
begin
  result:=false;
  try
    http.Head(sURL);
    if pos(‘200 OK’, http.Response.ResponseText)>0 then
      result:=true;
  except
  end;
end;

function TGetInfo.GetProgressInfo(sVerify_Code: string): TProgressInfo;
var
  sHead, sResult: TStringStream;
  sResultContent:String;
  Inicial,Final: Integer;
  function GetValue:string; //取得 ‘</SPAN>’ 之前的字符串
  var
    t:integer;
  begin
    delete(sResultContent,1,pos(‘<SPAN id="Content">’,sResultContent)+18);
    result:=”;
    t:=pos(‘</SPAN>’,sResultContent);
    result:=copy(sResultContent,1,t-1);
  end;
begin
  //检查地址是否有效
  if not CheckConnection(PostURL) then
  begin
    application.MessageBox(‘连接服务器失败,请检查网络是否连通!’,'失败’,MB_ICONWARNING);
    exit;
  end;
  FSuccess:=false;
  sHead := TStringStream.Create(”);
  sResult := TStringStream.Create(”);
  sHead.WriteString(‘ListSelect=TBL_TABLE283&searchClient=’+sVerify_Code);
  http.Request.ContentType := ‘application/x-www-form-urlencoded’;
  try
    http.Post(PostURL, sHead, sResult)
  except
    http.Get(http.Response.Location, sResult);
  end;
  Inicial := Pos(‘<TABLE><TR><TD><SPAN id="Message">’,sResult.DataString);    //获得返回结果表格字符串的开头
  Final   := Pos(‘</TD></TR></TABLE></FORM></TD></TR></TABLE></DIV>’,sResult.DataString);  //获得结果表格的结尾
  sResultContent := Copy(sResult.DataString,Inicial,Final-Inicial);
  sResultContent:= Utf8Toansi(sResultContent);
  if Pos(‘<SPAN id="dbDescr">’,sResultContent)>0 then //如果找到
  begin
    with Result do
    begin
      Verify_Code:=sVerify_Code;//核对码
      Accept_case_code:=GetValue; //受理号
      Corp_name:=GetValue; //企业名称
      Transact_State:=GetValue; //办理状态
      Drug_Approve_Code:=GetValue; //药品批准文号
      State_date:=GetValue; //状态开始时间
      Notice_date:=GetValue; //通知时间
      Notice_content:=GetValue; //通知内容
      Charge_Circs:=GetValue; //收费情况
      Charge_date:=GetValue; //费用收到日期
      Checkout_report_date:=GetValue; //检验报告收到日期
      Criterion_hodometer_date:=GetValue; //标准品回执收到日期
    end;
    FSuccess:=true;
  end
  else
  begin
    application.MessageBox(‘没有找到要查询的数据’,'失败’,MB_ICONWARNING);
  end;
end;

procedure TGetInfo.httpWorkBegin(Sender: TObject; AWorkMode: TWorkMode;
  const AWorkCountMax: Integer);
begin
  ProgressBar1.Position :=0;
  ProgressBar1.Max :=AWorkCountMax;
end;

procedure TGetInfo.httpWork(Sender: TObject; AWorkMode: TWorkMode;
  const AWorkCount: Integer);
begin
  ProgressBar1.Position :=AWorkCount;
end;

function TGetInfo.SaveToDb(aMyQuery: TMyQuery): boolean;
begin
  result:=false;
  if FSuccess then
  begin
    with aMyQuery do
    begin
      close;
      sql.Clear;
      with FResultInfo do
        sql.Text :=format(UpdateSql,[Accept_case_code,Corp_name,Transact_State,Drug_Approve_Code,State_date,
                                     Notice_date,Notice_content,Charge_Circs,Charge_date,Checkout_report_date,
                                     Criterion_hodometer_date,Verify_Code]);
      try
        Execute;
        result:=true;
      except
        result:=false;
      end;
    end;
  end
end;

procedure TGetInfo.DoSetEditValue;
begin
  FResultInfo:=GetProgressInfo(Verify_Code);
  with FResultInfo do
  begin
    edtVerify_Code.Text:=Verify_Code;
    edtAccept_case_code.Text:=Accept_case_code;
    edtCorp_name.Text:=Corp_name;
    edtTransact_State.Text:=Transact_State;
    edtDrug_Approve_Code.Text:=Drug_Approve_Code;
    edtState_date.Text:=State_date;
    edtNotice_date.Text:=Notice_date;
    edtNotice_content.Text:=Notice_content;
    edtCharge_Circs.Text:=Charge_Circs;
    edtCharge_date.Text:=Charge_date;
    edtCheckout_report_date.Text:=Checkout_report_date;
    edtCriterion_hodometer_date.Text:=Criterion_hodometer_date;
  end;
end;

procedure TGetInfo.btnSearchClick(Sender: TObject);
begin
  Verify_Code:=’1′; //要查询的值,由外部调用者传进来。
  //从服务器查询
  (Sender as TButton).Enabled :=false;
  try
    DoSetEditValue;
  finally
    (Sender as TButton).Enabled :=true;
  end;
{
  //保存
  if SaveToDb() then
  begin
    application.MessageBox(‘保存数据完成!’,'成功’,MB_ICONINFORMATION);
    close;
  end
  else
  begin
    application.MessageBox(‘保存数据失败!’,'失败’,MB_ICONWARNING);
  end;
}
end;

procedure TGetInfo.btnCloseClick(Sender: TObject);
begin
  close;
end;

end.

程序界面如下:

(显示的汉字是经过加密后的内容,无关紧要。)

2006年07月22日

与同事到外地出差,当地的同事热情好客,当晚便在一特色酒店的包间设宴接风。男男女女十几个人落座后便不停的聊天,只有一个人在点菜。点好了,征求大伙儿意见:“菜点好了,有没有要加的?”
这种情况,我们在北京一般是让小姐把点过的菜名儿报一遍。于是一位北京的哥们儿说:“小姐,报报。”
小姐看了他一眼,没动静。
“小姐,报一下!”哥们儿有点儿急了。
小姐脸涨得通红,还是没动静。
“怎么着?让你报一下没听见?”哥们儿真急了。
一位女同事赶紧打圆场:“小姐,你就赶紧挨个儿报一下吧,啊。”
小姐嗫嚅着问:“那,那……就抱女的,不抱男的行吗?”
“噗!”边上一位女同事刚喝的一大口茶全喷前边人身上了。十几个人笑做一团,小姐更是不知所措。  

上菜了,先上一个拌拉皮儿。一大盘拉皮儿端上来,接着是几碟儿配料、酱汁儿什么的。小姐上菜的时候没留神,一滴酱汁儿洒在一位哥们儿的裤子上了。那哥们儿也是成心逗闷子,假装阴沉着脸问小姐:“怎么办呀?”
小姐很冷静地说:“怎么办都行。”
“那你说怎么办?”
“您想怎么办就怎么办?”
“那你们这儿一般是怎么办的?”
“要不俺帮您办?”
“好呀。”
只见小姐麻利的把几碟儿配料、酱汁儿一股脑倒在拉皮儿上,一手拿筷子,一手拿
勺子,刷刷几下就拌好了。然后对那哥们儿说:“先生,拌好了,可以吃了。”
哥们儿努着眼珠子瞪着那盘子拉皮儿半天没说话,另一位同事替他跟小姐说了声“谢谢”。

上主菜了——烧羊腿,一大盘肉骨头,一碟子椒盐儿。一位北京哥们儿酷爱这口儿,
毫不客气的抓起一羊腿,咔嚓就是一口,呱唧呱唧的大吃起来。小姐一见,说道:
“先生,这个要蘸着吃。”
哥们儿将信将疑的看了看小姐,又看了看当地的同事。当地的同事说:“蘸着吃好吃一些。”
哥们儿于是拿着羊腿站起来,咔嚓又是一口。
小姐赶紧过来问:“先生,您有什么需要吗?”
“啊?没有啊。”
“那请您坐下来吃。”
哥们儿嘀咕着坐下来,看了看大伙儿,茫然若失。小心翼翼的把羊腿拿到嘴边,小心翼翼的咬了一口。
小姐又说:“先生,这个要蘸着吃。”
哥们儿腾地一下站起来,挥舞着羊腿怒气冲冲的嚷:“又要站着吃,又要坐着吃,到底怎么吃!?”

酒菜满席,领导跚跚而来。
满座起身相迎,一片寒喧之声。
旁边侍宴的小姐甚美,新来,经验不丰,颇有些紧张。
众人落座,有人招呼:“小姐,茶!”
小姐忙近前用手指点:“1、2、3、4、5、6、7,共七位!”
众人哂笑,领导补充曰:“倒茶!”
小姐忙又“倒查”了一遍:“7、6、5、4、3、2、1,还是七位。”
有人发问:“你数什么呢?”
小姐犹豫了一下小声答道:“ 我属狗。”
众人怒,急呼:“叫你们经理来!”,经理入,垂手讪笑,问:“诸位, 传我何事?”
领导曰:“别多问,去查查这位小姐年龄属相。”
经理纳闷,依命而行,旋来回复:“18岁,属狗!”
领导大笑,众人大笑。领导海量不做追究,众人雅量不便追究。
小姐、经理如坠五里云雾。

酒过三旬,上来一道菜:“清炖王八!”
众人皆喜,然未忘规矩,有人以箸拨王八头曰:“领导动动,领导动动!”
领导看着被拨得乱颤的鳖头,心中不悦,既不愿谐了此言的尾音又不愿违了众人美意,于是乎持勺酌汤,曰:“好,好!大家请随意。”
又有人奉称曰:“对–王八就该喝汤!”领导气得几乎喷饭。
未几,汤将尽,有物圆圆浮出,问:“小姐,这是什么?”
小姐忙答:“是王八蛋。”众人又惊喜:“领导先吃,领导先吃!”
这此领导没听到“晦气”之言,甚悦,唤小姐:“给大家分分!”
良久,小姐不动,领导怒问:“怎么,这也分不清楚吗?”
小姐为难的说:“七个人,六个王八蛋,您叫我怎么分啊?”
众人听罢,个个伸脖瞪眼,满口美食,难以下咽。

2006年07月09日

  今天看很多未看完的电子书,都提不起大兴趣,于是又溜到我不知道论坛到处翻看,不经意间看到《妩媚》,冲着迷男大大的招牌,下来看看。谁知道却叫这书坏了我一晚的心情。

  书里描写的妩媚无疑是个好女孩,有文才,有能力,又体贴人,不肖说长得那也是美丽可爱了。男主人公却整个一浪子,自有文才也放荡不羁,心有所爱,却非妩媚,妩媚爱才,心仰慕之,辗辗转转,经历了一些无奈与失意,两人走到了一起。妩媚用全心全意来爱他,迁就着他,她多想得到他全部的爱,一相情愿的在他生日那天把自己给了他,一次次的在他做错事后原谅了他(这样的女人,咱咋的就没碰到呢:))都说女子有爱才有欲,是其然也,奈何他对她又有几分爱意呢,须知他的爱以全部放到了另一个女孩—琳的身上了,只要琳的一声呼唤,他必定会毫不犹豫的去的——-最后他是真的这么做了的,明知道这么做会多伤妩媚的心(男人啊,你怎么去面对那幽伤的眼神,你怎么忍心伤害她)。当爱不再,心难寄托,妩媚堕落了自己(看到这个情节时,当我写到这里时,鼻子总不由得一阵阵的发酸,当一个美好的事物被毁灭后,悲剧就产生了),可怜而痴情的女人啊,你可谓遇人不淑,你糟践了自己的青春年华,你得到了什么啊,我想依你的性格,你也不会去后悔把,毕竟也活过,爱过,也恨过,数年的铁窗生涯后,你或会有所改变,那时妩媚将还是妩媚吗?

书看完后,我不禁怀疑这小说仅仅只是小说,莫不是迷男生活片段的艺术加工?我很希望素材是来源于真实的生活,妩媚如是真实的存在,教人情何以堪!

2006年06月23日

(三)对服务的深入讨论之下

  现在我们还剩下一个函数可以在细节上讨论,那就是服务的CtrlHandler函数。

  当调用RegisterServiceCtrlHandler函数时,SCM得到并保存这个回调函数的地址。一个SCP调一个告诉SCM如何去控制服务的Win32函数,现在已经有10个预定义的控制请求:

Control code
Meaning
SERVICE_CONTROL_STOP Requests the service to stop. The hService handle must have SERVICE_STOP access.
SERVICE_CONTROL_PAUSE Requests the service to pause. The hService handle must have SERVICE_PAUSE_CONTINUE access.
SERVICE_CONTROL_CONTINUE Requests the paused service to resume. The hService handle must have SERVICE_PAUSE_CONTINUE access.
SERVICE_CONTROL_INTERROGATE Requests the service to update immediately its current status information to the service control manager. The hService handle must have SERVICE_INTERROGATE access.
SERVICE_CONTROL_SHUTDOWN Requests the service to perform cleanup tasks, because the system is shutting down. For more information, see Remarks.
SERVICE_CONTROL_PARAMCHANGE Windows 2000: Requests the service to reread its startup parameters. The hService handle must have SERVICE_PAUSE_CONTINUE access.
SERVICE_CONTROL_NETBINDCHANGE Windows 2000: Requests the service to update its network binding. The hService handle must have SERVICE_PAUSE_CONTINUE access.
SERVICE_CONTROL_NETBINDREMOVE Windows 2000: Notifies a network service that a component for binding has been removed. The service should reread its binding information and unbind from the removed component.
SERVICE_CONTROL_NETBINDENABLE Windows 2000: Notifies a network service that a disabled binding has been enabled. The service should reread its binding information and add the new binding.
SERVICE_CONTROL_NETBINDDISABLE Windows 2000: Notifies a network service that one of its bindings has been disabled. The service should reread its binding information and remove the binding.

  上表中标有Windows 2000字样的就是2000中新添加的控制代码。除了这些代码之外,服务也可以接受用户定义的,范围在128-255之间的代码。

  当CtrlHandler函数收到一个SERVICE_CONTROL_STOPSERVICE_CONTROL_PAUSESERVICE_CONTROL_CONTINUE控制代码的时候,SetServiceStatus必须被调用去确认这个代码,并指定你认为服务处理这个状态变化所需要的时间。

  例如:你的服务收到了停止请求,首先要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOP_PENDING,这样可以使SCM确定你已经收到了控制代码。当一个服务的暂停或停止操作正在执行的时候,必须指定你认为这种操作所需要的时间:这是因为一个服务也许不能立即改变它的状态,它可能必须等待一个网络请求被完成或者数据被刷新到一个驱动器上。指定时间的方法就像我上一章说的那样,用成员dwCheckPointdwWaitHint来指明它完成状态改变所需要的时间。如果需要,可以用增加dwCheckPoint成员的值和设置dwWaitHint成员的值去指明你期待的服务到达下一步的时间的方式周期性的报告进展情况。

  当整个启动的过程完成之后,要再一次调用SetServiceStatus。这时就要把SERVICE_STATUS结构的dwCurrentState成员设置成SERVICE_STOPPED,当报告状态代码的同时,一定要把成员dwCheckPointdwWaitHint设置为0,因为服务已经完成了它的状态变化。暂停或继续服务的时候方法也一样。

  当CtrlHandler函数收到一个SERVICE_CONTROL_INTERROGATE控制代码的时候,服务将简单的将dwCurrentState成员设置成服务当前的状态,同时,把成员dwCheckPointdwWaitHint设置为0,然后再调用SetServiceStatus就可以了。

  在操作系统关闭的时候,CtrlHandler函数收到一个SERVICE_CONTROL_SHUTDOWN控制代码。服务根本无须回应这个代码,因为系统即将关闭。它将执行保存数据所需要的最小行动集,这是为了确定机器能及时关闭。缺省时系统只给很少的时间去关闭所有的服务,MSDN里面说大概是20秒的时间,不过那可能是Windows NT 4的设置,在我的Windows 2000 Server里这个时间是10秒,你可以手动的修改这个数值,它被记录在HKEY_LOCAL_MACHINESYSTEMCurrentControlSetControl子键里面的WaitToKillServiceTimeout,单位是毫秒。

 

  当CtrlHandler函数收到任何用户定义的代码时,它应该执行期望的用户自定义行动。除非用户自定义的行动要强制服务去暂停、继续或停止,否则不调SetServiceStatus函数。如果用户定义的行动强迫服务的状态发生变化,SetServiceStatus将被调用去设置dwCurrentStatedwCheckPointdwWaitHint,具体控制代码和前面说的一样。

  如果你的CtrlHandler函数需要很长的时间执行操作的话,千万要注意:假如CtrlHandler函数在30秒内没有返回的话,SCM将返回一个错误,这不是我们所期望的。所以如果出现上述情况,最好的办法是再建立一个线程,让它去继续执行操作,以便使得CtrlHandler函数能够迅速的返回。例如,当收到一个SERVICE_CONTROL_STOP请求的时候,就像上面说的一样,服务可能正在等待一个网络请求被完成或者数据被刷新到一个驱动器上,而这些操作所需要的时间是你不能估计的,那么就要建立一个新的线程等待操作完成后执行停止命令,CtrlHandler函数在返回之前仍然要报告SERVICE_STOP_PENDING状态,当新的线程执行完操作之后,再由它将服务的状态设置成SERVICE_STOPPED。如果当前操作的时间可以估计的到就不要这样做,仍然使用前面交待的方法处理。

  CtrlHandler函数我就先讲这些,下面说说服务怎么安装。一个服务程序可以使用CreateService函数将服务的信息添加到SCM的数据库。

SC_HANDLE CreateService( SC_HANDLE hSCManager, // handle to SCM database LPCTSTR lpServiceName, // name of service to start LPCTSTR lpDisplayName, // display name DWORD dwDesiredAccess, // type of access to service DWORD dwServiceType, // type of service DWORD dwStartType, // when to start service DWORD dwErrorControl, // severity of service failure LPCTSTR lpBinaryPathName, // name of binary file LPCTSTR lpLoadOrderGroup, // name of load ordering group LPDWORD lpdwTagId, // tag identifier LPCTSTR lpDependencies, // array of dependency names LPCTSTR lpServiceStartName, // account name LPCTSTR lpPassword // account password );

  hSCManager是一个标示SCM数据库的句柄,可以简单的通过调用OpenSCManager得到。

SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, // computer name LPCTSTR lpDatabaseName, // SCM database name DWORD dwDesiredAccess // access type );

  lpMachineName是目标机器的名字,还记得我在第一章里说过可以在其它的机器上面安装服务吗?这就是实现的方法。对方机器名字必须以“\”开始。如果传递NULL或者一个空的字符串的话就默认是本机。

  lpDatabaseName是目标机器上面SCM数据库的名字,但MSDN里面说这个参数要默认的设置成SERVICES_ACTIVE_DATABASE,如果传递NULL,就默认的打开SERVICES_ACTIVE_DATABASE。所以我还没有真的搞明白这个参数的存在意义,总之使用的时候传递NULL就行了。

  dwDesiredAccessSCM数据库的访问权限,具体值见下表:

Object access
Description
SC_MANAGER_ALL_ACCESS Includes STANDARD_RIGHTS_REQUIRED, in addition to all of the access types listed in this table.
SC_MANAGER_CONNECT Enables connecting to the service control manager.
SC_MANAGER_CREATE_SERVICE Enables calling of the CreateService function to create a service object and add it to the database.
SC_MANAGER_ENUMERATE_SERVICE Enables calling of the EnumServicesStatus function to list the services that are in the database.
SC_MANAGER_LOCK Enables calling of the LockServiceDatabase function to acquire a lock on the database.
SC_MANAGER_QUERY_LOCK_STATUS Enables calling of the QueryServiceLockStatus function to retrieve the lock status information for the database.

  想要获得访问权限的话,似乎没那么复杂。MSDN里面说所有进程都被允许获得对所有SCM数据库的SC_MANAGER_CONNECT, SC_MANAGER_ENUMERATE_SERVICE, and SC_MANAGER_QUERY_LOCK_STATUS权限,这些权限使得你可以连接SCM数据库,枚举目标机器上安装的服务和查询目标数据库是否已被锁住。但如果要创建服务,首先你需要拥有目标机器的管理员权限,一般的传递SC_MANAGER_ALL_ACCESS就可以了。这个函数返回的句柄可以被CloseServiceHandle函数关闭。

  lpServiceName是服务的名字,lpDisplayName是服务在“服务”管理工具里显示的名字。

  dwDesiredAccess也是访问的权限,有一个比上面的还长的多的一个表,各位自己查MSDN吧。我们要安装服务,仍然简单的传递SC_MANAGER_ALL_ACCESS

  dwServiceType是指你的服务是否和其它的进程相关联,一般是SERVICE_WIN32_OWN_PROCESS,表示不和任何进程相关联。如果你确认你的服务需要和某些进程相关联,就设置成SERVICE_WIN32_SHARE_PROCESS。当你的服务要和桌面相关联的时候,需要设置成SERVICE_INTERACTIVE_PROCESS

  dwStartType是服务的启动方式。服务有三种启动方式,分别是“自动(SERVICE_AUTO_START)”“手动(SERVICE_DEMAND_START)”和“禁用(SERVICE_DISABLED)”。在MSDN里还有另外的两种方式,不过是专为驱动程序设置的。

  dwErrorControl决定服务如果在系统启动的时候启动失败的话要怎么办。

意义
SERVICE_ERROR_IGNORE 启动程序记录错误发生,但继续启动。
SERVICE_ERROR_NORMAL 启动程序记录错误发生,并弹出一个消息框,但仍继续启动
SERVICE_ERROR_SEVERE 启动程序记录错误发生,如果是以last-known-good configuration启动的话,启动会继续。否则会以last-known-good configuration重新启动计算机。
SERVICE_ERROR_CRITICAL 启动程序记录错误发生,如果可能的话。如果是以last-known-good configuration启动的话,启动会失败。否则会以last-known-good configuration重新启动计算机。好严重的错误啊。

  lpBinaryPathName是服务程序的路径。MSDN里面特别提到如果服务路径里面有空格的话一定要将路径用引号引起来。例如"d:\my share\myservice.exe"就一定要指定为""d:\my share\myservice.exe""

  lpLoadOrderGroup的意义在于,如果有一组服务要按照一定的顺序启动的话,这个参数用于指定一个组名用于标志这个启动顺序组,不过我还没有用过这个参数。你的服务如果不属于任何启动顺序组,只要传递NULL或者一个空的字符串就行了。

  lpdwTagId是应用了上面的参数之后要指定的值,专用于驱动程序,与本文内容无关。传递NULL

  lpDependencies标示一个字符串数组,用于指明一串服务的名字或者一个启动顺序组。当与一个启动顺序组建立关联的时候,这个参数的含义就是只有你指定的启动顺序组里有至少一个经过对整个组里所有的成员已经全部尝试过启动后,有至少一个成员成功启动,你的服务才能启动。不需要建立依存关系的话,仍是传递NULL或者一个空的字符串。但如果你要指定启动顺序组的话,必须为组名加上SC_GROUP_IDENTIFIER前缀,因为组名和服务名是共享一个命名空间的。

  lpServiceStartName是服务的启动账号,如果你设置你的服务的关联类型是SERVICE_WIN32_OWN_PROCESS的话,你需要以DomainNameUserName的格式指定用户名,如果这个账户在你本机的话,用.UserName就可以指定。如果传递NULL的话,会以本地的系统账户登陆。如果是Win NT 4.0或更早的版本的话,如果你指定了SERVICE_WIN32_SHARE_PROCESS,就必须传递.System指定服务使用本地的系统账户。最后,如果你指定了SERVICE_INTERACTIVE_PROCESS,你必须使服务运行在本机系统账户。

  看名字就知道了,lpPassword是账户的密码。如果指定系统账户的话,传递NULL。如果账户没有密码的话,传递空字符串。

  总之服务的基本原理就是这样子了,到了这里这篇文章似乎可以告一段落了,但实际上还有很多内容必须要讨论,所以我还不能草草收笔,敬请关注下一章

(四)一些问题的讨论

  前面几章的内容都是服务的一些通用的编写原理,但里面隐含着一些问题,编写简单的服务时看不出来,但遇到复杂的应用就会出现一些问题,所以本章就是用来分析、解决这些问题的,适用于高级应用的开发人员。我这一章的内容都是经过实验得到的,很有实际意义。

  我在第一章里面就说过,是由一个服务的主线程执行CtrlHandler函数,它将收到各种控制命令,但是真正处理命令,执行操作的是ServiceMain的线程。现在,当一个SERVICE_CONTROL_STOP到达之后,你作为一个开发者,要怎样停止这个服务?在我看过的一些源代码里,大部分只是简单的调用TerminateThread函数去强行杀掉服务进程。但应该稍稍有点线程编程的常识就应该知道TerminateThread函数是可用的调用中最为糟糕的一个,服务线程将得不到任何机会去做应该的清理工作,诸如清除内存、释放核心对象,Dlls也得不到任何线程已经被毁的通知。

  所以停止服务的适当方法是以某种方式激活服务线程,让它停止继续提供服务功能,然后执行完当前操作和清除工作后返回。这就表示你必须在CtrlHandler线程和ServiceMain线程之间执行适当的线程通信。现在已知的最好的内部线程通信机制是I/O Completion Port(I/O 完成端口),假如你编写的是一个大型的服务,需要同时处理为数众多的请求,并且运行在多处理器系统上面,这个模型就可以提供最佳的系统性能。但也正因为它的复杂性较高,在小规模的应用上面不值得花费很多的时间和精力,这时作为开发者可以适当的选取其它的通信方式,诸如异步过程调用队列、套接字和窗口消息,以适应实际情况。

  开发服务时的另外一个重要问题就是调用SetServiceStatus函数时的所有状态报告问题。很多的服务开发者为了在什么时候调用SetServiceStatus的问题而常常产生争论,一般推荐的方法就是:先调用SetServiceStatus函数,报告SERVICE_STOP_PENDING状态,然后将控制代码传给服务线程或者再建立一个新的线程,让它去继续执行操作,当该线程即将执行完操作之前,再由它将服务的状态设置成SERVICE_STOPPED,然后服务正好停止。

  上面的主意从两个方面来讲还是很不错的。首先服务可以立即确认收到了控制代码,并将在它认为适当的时候进行处理;然后就是因为前面说过的,执行CtrlHandler函数的是主线程,如果按照这种工作方法,CtrlHandler函数可以迅速的返回,不会影响到其它服务可能收到的控制请求,对含有多个服务的程序来说,响应各个服务的控制代码的速度会大大的提高。可是,随之而来的是问题—— race condition 即“竞争条件”的产生。

  摆在下面的就是一个竞争条件的例子,我花了一点时间来修改我的基本服务的代码,意图故意引发“竞争条件”的发生。我添加了一个线程,CtrlHandler函数的线程在收到请求后立刻作出反应,将当前的服务状态设置成“请求正在被处理”即…_PENDING,然后由我添加的线程在睡眠了5秒之后再将服务状态设置成“请求已完成”状态——以模拟服务正在处理一些不可中止的事件,只有处理完成后才会更改服务的状态。一切就绪之后,我尝试在短时间内连续发送两个“暂停”请求,如果“竞争条件”不存在的话应该只有先发送的那个请求能够到达SCM,而另一个则应该返回请求发送失败的信息,天下太平。

  事实上很不幸的,我成功了。当我在两个不同的“命令提示符”窗口分别同样的输入下面的命令:

net pause kservice

  之后在“事件查看器”里面,我找到了我的服务在“应用程序日志”里添加的事件记录,结果是我得到了这样的事件列表:

SERVICE_PAUSE_PENDING
SERVICE_PAUSE_PENDING
SERVICE_PAUSED
SERVICE_PAUSED

  看上去很奇怪是不是?因为服务处于正在暂停状态的时候,它不应该被再次暂停的。但事实摆在眼前,很多服务都曾明确的报告过上面的顺序状态。我曾经认为这时SCM应该说些什么或做些什么,以阻止“竞争状态”的出现,但实验结果告诉我SCM似乎对此无能为力,因为它不能控制状态代码在什么时候被发送。当用户使用“管理工具”里面的“服务”工具来管理服务的状态的时候,在一个“暂停”请求已经发出之后不能再次用这个工具向它发出“暂停”请求,如果正在暂停服务,会有一个对话框出现,阻止你按下它后面的“服务”工具的工具栏上的任何按钮,如果已经暂停,“暂停“按钮将变成灰色。但是这时用命令行工具 net.exe 就可以很顺利地将暂停请求再次送到服务。证据就是我添加的其他事件记录里面记下了SetServiceStatus的调用全都成功了,这更进一步的说明了我提交的两个暂停请求都经过SCM,然后到达了我的服务。

  接下来我又进行了其它的测试,例如先发送“暂停”请求,后发送“停止”请求,和先发送“停止”请求,再发送“暂停”或“停止”请求。前一种情况更加糟糕,先发送的“暂停”请求和后发送的“停止”请求都没有得到什么好下场,虽然SCM老老实实的先暂停了服务,后停止了服务,但 net.exe 的两个实例的调用均告失败。不过在测试先发送停止“请求”的时候,所有的现象都表示这两个请求只有先发送的“停止”到达了SCM,这还算是个好消息…

  为了解决这个问题,当服务得到一个“停止”“暂停”或“继续”请求的时候,应该首先检查服务是否已经在处理另外的一个请求,如果是,就依情况而定:是不调用SetServiceStatus直接返回还是暂时忍耐直到前一个请求动作完成再调用SetServiceStatus,这是你作为一个开发者要自己决定的。

  如果说前面的问题已经足够麻烦了,下面的问题会令你觉得更加怪异。它其实是一种可以解决上面的问题的方法:当CtrlHandler函数的线程收到SERVICE_PAUSE_PENDING请求之后,它调用SetServiceStatus报告服务正在暂停,然后由它自己调用SuspendThread来暂停服务的线程,然后再由它自己调用SetServiceStatus报告服务已经被暂停。这样做的确避免了“竞争条件”的出现,因为所有的工作都是由一个函数来做的。现在需要注意的不是“竞争条件”而是服务本身,挂起服务的线程会不会暂停服务呢?答案是会的。但是暂停服务意味着什么呢?

  假如我的服务是用来处理网络客户的请求,那么暂停对于我的服务来说应该是停止接受新的请求。如果我现在正处在处理请求的过程中,那么我应该怎么办?也许我应该结束它,使客户不至于无限期悬挂。但如果我只是简单的调用SuspendThread,那么不排除服务线程正处于孤立的中间状态的可能,或者正在调用malloc函数去尝试分配内存,如果运行在同一个进程中的另一个服务也调内存分配函数,那么它也会被挂起,这肯定不是我期望的结果。

  还有一个问题:用户认为自己可以被允许去停止一个已经被暂停了的服务吗?我认为是这样的,而且很明显的,微软也这么认为。因为当我们在“服务”管理工具里面选中一个已暂停的服务之后,“停止”按钮是可以被按下的。但我要怎样停止一个由于线程被挂起才处于暂停状态的服务呢?不,不要TerminateThread,请别跟我提起它。

  解决这所有的混乱的最好方法,就是有一个能够把所有事做好的线程,而且它应该是服务线程,而不是CtrlHandler线程。当CtrlHandler函数得到控制代码之后,它要迅速的将控制代码通过线程内部通讯手段送到服务线程中排队,然后CtrlHandler函数就应该返回,它决不应该调SetServiceStatus。这样,服务可以随心所欲的控制每件事情,因为没有什么比它更有发言权的了,没有“竞争条件”。服务决定暂停意味着什么,服务能够允许自己在已经暂停的情况下停止,服务决定什么内部通讯机制是最好的——并且CtrlHandler函数必须简单的与这种机制相一致。

  事情没有完美的,上面的方法也不例外,它仅有一个小缺陷:就是假定当服务收到控制代码后,在较短的时间内就能做出应有的响应。如果服务线程正在忙于处理一个客户的请求,控制代码可能进入等待队列,而且SetServiceStatus可能也无法迅速的被调用。如果真是这样的话,负责发送通知的SCP可能会认为你的服务已经失败,并向用户报告一个消息框。事实上服务并没有失败,而且也不会被终止。

  这种情况够糟糕了,没有用户会去责怪SCP——虽然SCP将他们引导到了错误的状态,他们只会责怪服务的作者——就是我或你…因此,在服务中怎么做才能防止这种问题发生呢?很简单,使服务快速有效的运行,并且总保持一个活动线程等待去处理控制代码。

  说起来好像很容易,但实际做起来就被那么简单了,这也不是我能够向各位解释的了,只有认真的调试自己的服务,才能找出最为适合处理方法。所以我的文章也真的到了该结束的时候了,感谢各位的浏览。如果我有什么地方说的不对,请不吝赐教,谢谢。

  下面是我写的一个服务的源代码,没什么功能,只能启动、停止和安装。

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>

#define SZAPPNAME "basicservice"
#define SZSERVICENAME "KService"
#define SZSERVICEDISPLAYNAME "KService"
#define SZDEPENDENCIES ""

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv);
void InstallService(const char * szServiceName);
void LogEvent(LPCTSTR pFormat, …);
void Start();
void Stop();

SERVICE_STATUS ssStatus;
SERVICE_STATUS_HANDLE sshStatusHandle;

int main(int argc, char * argv[])
{
  if ((argc==2) && (::strcmp(argv[1]+1, "install")==0))
  {
    InstallService("KService");
    return 0;
  }

SERVICE_TABLE_ENTRY   service_table_entry[] =
  {
    { "KService", KServiceMain },
    { NULL, NULL }
  };
  ::StartServiceCtrlDispatcher(service_table_entry);
  return 0;
}

void InstallService(const char * szServiceName)
{
  SC_HANDLE handle = ::OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
  char szFilename[256];
  ::GetModuleFileName(NULL, szFilename, 255);
  SC_HANDLE hService = ::CreateService(handle, szServiceName,
  szServiceName, SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS,
  SERVICE_DEMAND_START, SERVICE_ERROR_IGNORE, szFilename, NULL,
  NULL, NULL, NULL, NULL);
  ::CloseServiceHandle(hService);
  ::CloseServiceHandle(handle);
}

SERVICE_STATUS servicestatus;
SERVICE_STATUS_HANDLE servicestatushandle;

void WINAPI ServiceCtrlHandler(DWORD dwControl)
{
  switch (dwControl)
  {

//下面虽然添加了暂停、继续等请求的处理代码,但没有实际作用
  //这是为什么呢?到了下面的KServiceMain函数里面就明白了…

case SERVICE_CONTROL_PAUSE:
    servicestatus.dwCurrentState = SERVICE_PAUSE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    // This value need to try a lot to confirm
    // …
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to pause the service
    // not called in this service
    // …
    servicestatus.dwCurrentState = SERVICE_PAUSED;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
    break;

  case SERVICE_CONTROL_CONTINUE:
    servicestatus.dwCurrentState = SERVICE_CONTINUE_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to unpause the service
    // not called in this service
    // …
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
    break;

  case SERVICE_CONTROL_STOP:
    servicestatus.dwCurrentState = SERVICE_STOP_PENDING;
    // TODO: add code to set dwCheckPoint & dwWaitHint
    ::SetServiceStatus(servicestatushandle, &servicestatus);
    // TODO: add code to stop the service
    Stop();
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    // TODO: add code to set dwCheckPoint & dwWaitHint to 0
    break;

  case SERVICE_CONTROL_SHUTDOWN:
    // TODO: add code for system shutdown
    // as quick as possible
    break;

  case SERVICE_CONTROL_INTERROGATE:
    // TODO: add code to set the service status
    // …
    servicestatus.dwCurrentState = SERVICE_RUNNING;
    break;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
}

void WINAPI KServiceMain(DWORD argc, LPTSTR * argv)
{
  servicestatus.dwServiceType = SERVICE_WIN32;
  servicestatus.dwCurrentState = SERVICE_START_PENDING;
  servicestatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;//上面的问题的答案就在这里
  servicestatus.dwWin32ExitCode = 0;
  servicestatus.dwServiceSpecificExitCode = 0;
  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;

  servicestatushandle =
  ::RegisterServiceCtrlHandler("KService", ServiceCtrlHandler);
  if (servicestatushandle == (SERVICE_STATUS_HANDLE)0)
  {
    return;
  }

  bool bInitialized = false;
  // Initialize the service
  // …
  Start();

  bInitialized = true;

  servicestatus.dwCheckPoint = 0;
  servicestatus.dwWaitHint = 0;
  if (!bInitialized {
    servicestatus.dwCurrentState = SERVICE_STOPPED;
    servicestatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
    servicestatus.dwServiceSpecificExitCode = 1;
  }
  else
  {
    servicestatus.dwCurrentState = SERVICE_RUNNING;
  }
  ::SetServiceStatus(servicestatushandle, &servicestatus);
  return;
}

void Start()
{
  LogEvent("Service Starting…");
}

void LogEvent(LPCTSTR pFormat, …)
{
  TCHAR chMsg[256];
  HANDLE hEventSource;
  LPTSTR lpszStrings[1];
  va_list pArg;

  va_start(pArg, pFormat);
  _vstprintf(chMsg, pFormat, pArg);
  va_end(pArg);

  lpszStrings[0] = chMsg;

  if (1)
  {
    // Get a handle to use with ReportEvent().
    hEventSource = RegisterEventSource(NULL, "KService");
    if (hEventSource != NULL)
    {
    // Write to event log.
      ReportEvent(hEventSource, EVENTLOG_INFORMATION_TYPE, 0, 0, NULL, 1, 0, (LPCTSTR*) &lpszStrings[0], NULL);
      DeregisterEventSource(hEventSource);
    }
  }
  else
  {
    // As we are not running as a service, just write the error to the console.
    _putts(chMsg);
  }
}

void Stop()
{
  LogEvent("Service Stoped.");
}

有那么一类应用程序,是能够为各种用户(包括本地用户和远程用户)所用的,拥有用户授权级进行管理的能力,并且不论用户是否物理的与正在运行该应用程序的计算机相连都能正常执行,这就是所谓的服务了。

(一)服务的基础知识

Question 1. 什么是服务?它的特征是什么?

  在NT/2000中,服务是一类受到操作系统优待的程序。一个服务首先是一个Win32可执行程序,如果要写一个功能完备且强大的服务,需要熟悉动态连接库(Dlls)、结构异常处理、内存映射文件、虚拟内存、设备I/O、线程及其同步、Unicode以及其他的由WinAPI函数提供的应用接口。当然本文讨论的只是建立一个可以安装、运行、启动、停止的没有任何其他功能的服务,所以无需上述知识仍可以继续看下去,我会在过程中将理解本文所需要的知识逐一讲解。

  第二要知道的是一个服务决不需要用户界面。大多数的服务将运行在那些被锁在某些黑暗的,冬暖夏凉的小屋子里的强大的服务器上面,即使有用户界面一般也没有人可以看到。如果服务提供任何用户界面如消息框,那么用户错过这些消息的可能性就极高了,所以服务程序通常以控制台程序的形式被编写,进入点函数是main()而不是WinMain()。

  也许有人有疑问:没有用户界面的话,要怎样设置、管理一个服务?怎样开始、停止它?服务如何发出警告或错误信息、如何报告关于它的执行情况的统计数据?这些问题的答案就是服务能够被远程管理,Windows NT/2000提供了大量的管理工具,这些工具允许通过网络上的其它计算机对某台机器上面的服务进行管理。比如Windows 2000里面的“控制台”程序(mmc.exe),用它添加“管理单元”就可以管理本机或其他机器上的服务。

Question 2. 服务的安全性…

  想要写一个服务,就必须熟悉Win NT/2000的安全机制,在上述操作系统之中,所有安全都是基于用户的。换句话说——进程、线程、文件、注册表键、信号、事件等等等等都属于一个用户。当一个进程被产生的时候,它都是执行在一个用户的上下文(context),这个用户帐号可能在本机,也可能在网络中的其他机器上,或者是在一个特殊的账号:System Account——即系统帐号的上下文

  如果一个进程正在一个用户帐号下执行,那么这个进程就同时拥有这个用户所能拥有的一切访问权限,不论是在本机还是网络。系统帐号则是一个特殊的账号,它用来标识系统本身,而且运行在这个帐号下的任何进程都拥有系统上的所有访问权限,但是系统帐号不能在域上使用,无法访问网络资源…

  服务也是Win32可执行程序,它也需要执行在一个context,通常服务都是在系统账号下运行,但是也可以根据情况选择让它运行在一个用户账号下,也就会因此获得相应的访问资源的权限。

Question 3. 服务的三个组成部分

  一个服务由三部分组成,第一部分是Service Control Manager(SCM)。每个Windows NT/2000系统都有一个SCM,SCM存在于Service.exe中,在Windows启动的时候会自动运行,伴随着操作系统的启动和关闭而产生和终止。这个进程以系统特权运行,并且提供一个统一的、安全的手段去控制服务。它其实是一个RPC Server,因此我们可以远程安装和管理服务,不过这不在本文讨论的范围之内。SCM包含一个储存着已安装的服务和驱动程序的信息的数据库,通过SCM可以统一的、安全的管理这些信息,因此一个服务程序的安装过程就是将自身的信息写入这个数据库。

  第二部分就是服务本身。一个服务拥有能从SCM收到信号和命令所必需的的特殊代码,并且能够在处理后将它的状态回传给SCM。

  第三部分也就是最后一部分,是一个Service Control Dispatcher(SCP)。它是一个拥有用户界面,允许用户开始、停止、暂停、继续,并且控制一个或多个安装在计算机上服务的Win32应用程序。SCP的作用是与SCM通讯,Windows 2000管理工具中的“服务”就是一个典型的SCP。

  在这三个组成部分中,用户最可能去写服务本身,同时也可能不得不写一个与其伴随的客户端程序作为一个SCP去和SCM通讯,本文只讨论去设计和实现一个服务,关于如何去实现一个SCP则在以后的其它文章中介绍。

Question 4. 怎样开始设计服务

  还记得前面我提到服务程序的入口点函数一般都是main()吗?一个服务拥有很重要的三个函数,第一个就是入口点函数,其实用WinMain()作为入口点函数也不是不可以,虽然说服务不应该有用户界面,但是其实存在很少的几个例外,这就是下面图中的选项存在的原因。

  由于要和用户桌面进行信息交互,服务程序有时会以WinMain()作为入口点函数。

  入口函数负责初始化整个进程,由这个进程中的主线程来执行。这意味着它应用于这个可执行文件中的所有服务。要知道,一个可执行文件中能够包含多个服务以使得执行更加有效。主进程通知SCM在可执行文件中含有几个服务,并且给出每一个服务的ServiceMain回调(Call Back)函数的地址。一旦在可执行文件内的所有服务都已经停止运行,主线程就在进程终止前对整个进程进行清除。

  第二个很重要的函数就是ServiceMain,我看过一些例子程序里面对自己的服务的进入点函数都固定命名为ServiceMain,其实并没有规定过一定要那样命名,任何的函数只要符合下列的形式都可以作为服务的进入点函数。

VOID WINAPI ServiceMain(
  DWORD dwArgc, // 参数个数
  LPTSTR *lpszArgv // 参数串
);

  这个函数由操作系统调用,并执行能完成服务的代码。一个专用的线程执行每一个服务的ServiceMain函数,注意是服务而不是服务程序,这是因为每个服务也都拥有与自己唯一对应的ServiceMain函数,关于这一点可以用“管理工具”里的“服务”去察看Win2000里面自带的服务,就会发现其实很多服务都是由service.exe单独提供的。当主线程调用Win32函数StartServiceCtrlDispatcher的时候,SCM为这个进程中的每一个服务产生一个线程。这些线程中的每一个都和它的相应的服务的ServiceMain函数一起执行,这就是服务总是多线程的原因——一个仅有一个服务的可执行文件将有一个主线程,其它的线程执行服务本身。

  第三个也就是最后的一个重要函数是CtrlHandler,它必须拥有下面的原型:

VOID WINAPI CtrlHandler(
DWORD fdwControl //控制命令
)

  像ServiceMain一样,CtrlHandler也是一个回调函数,用户必须为它的服务程序中每一个服务写一个单独的CtrlHandler函数,因此如果有一个程序含有两个服务,那么它至少要拥有5个不同的函数:作为入口点的main()或WinMain(),用于第一个服务的ServiceMain函数和CtrlHandler函数,以及用于第二个服务的ServiceMain函数和CtrlHandler函数。

  SCM调用一个服务的CtrlHandler函数去改变这个服务的状态。例如,当某个管理员用管理工具里的“服务”尝试停止你的服务的时候,你的服务的CtrlHandler函数将收到一个SERVICE_CONTROL_STOP通知。CtrlHandler函数负责执行停止服务所需的一切代码。由于是进程的主线程执行所有的CtrlHandler函数,因而必须尽量优化你的CtrlHandler函数的代码,使它运行起来足够快,以便相同进程中的其它服务的CtrlHandler函数能在适当的时间内收到属于它们的通知。而且基于上述原因,你的CtrlHandler函数必须要能够将想要传达的状态送到服务线程,这个传递过程没有固定的方法,完全取决于你的服务的用途。

(二)对服务的深入讨论之上

  上一章其实只是概括性的介绍,下面开始才是真正的细节所在。在进入点函数里面要完成ServiceMain的初始化,准确点说是初始化一个SERVICE_TABLE_ENTRY结构数组,这个结构记录了这个服务程序里面所包含的所有服务的名称和服务的进入点函数,下面是一个SERVICE_TABLE_ENTRY的例子:

SERVICE_TABLE_ENTRY service_table_entry[] =
{
  { "MyFTPd" , FtpdMain },
  { "MyHttpd", Httpserv},
  { NULL, NULL },
};

  第一个成员代表服务的名字,第二个成员是ServiceMain回调函数的地址,上面的服务程序因为拥有两个服务,所以有三个SERVICE_TABLE_ENTRY元素,前两个用于服务,最后的NULL指明数组的结束。

  接下来这个数组的地址被传递到StartServiceCtrlDispatcher函数:

BOOL StartServiceCtrlDispatcher(
LPSERVICE_TABLE_ENTRY lpServiceStartTable
)

  这个Win32函数表明可执行文件的进程怎样通知SCM包含在这个进程中的服务。就像上一章中讲的那样,StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程,每一个进程开始执行由数组元素中的lpServiceStartTable指明的ServiceMain函数。

  SCM启动一个服务程序之后,它会等待该程序的主线程去调StartServiceCtrlDispatcher。如果那个函数在两分钟内没有被调用,SCM将会认为这个服务有问题,并调用TerminateProcess去杀死这个进程。这就要求你的主线程要尽可能快的调用StartServiceCtrlDispatcher。

  StartServiceCtrlDispatcher函数则并不立即返回,相反它会驻留在一个循环内。当在该循环内时,StartServiceCtrlDispatcher悬挂起自己,等待下面两个事件中的一个发生。第一,如果SCM要去送一个控制通知给运行在这个进程内一个服务的时候,这个线程就会激活。当控制通知到达后,线程激活并调用相应服务的CtrlHandler函数。CtrlHandler函数处理这个服务控制通知,并返回到StartServiceCtrlDispatcher。StartServiceCtrlDispatcher循环回去后再一次悬挂自己。

  第二,如果服务线程中的一个服务中止,这个线程也将激活。在这种情况下,该进程将运行在它里面的服务数减一。如果服务数为零,StartServiceCtrlDispatcher就会返回到入口点函数,以便能够执行任何与进程有关的清除工作并结束进程。如果还有服务在运行,哪怕只是一个服务,StartServiceCtrlDispatcher也会继续循环下去,继续等待其它的控制通知或者剩下的服务线程中止。

  上面的内容是关于入口点函数的,下面的内容则是关于ServiceMain函数的。还记得以前讲过的ServiceMain函数的的原型吗?但实际上一个ServiceMain函数通常忽略传递给它的两个参数,因为服务一般不怎么传递参数。设置一个服务最好的方法就是设置注册表,一般服务在
HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServiceServiceNameParameters
子键下存放自己的设置,这里的ServiceName是服务的名字。事实上,可能要写一个客户应用程序去进行服务的背景设置,这个客户应用程序将这些信息存在注册表中,以便服务读取。当一个外部应用程序已经改变了某个正在运行中的服务的设置数据的时候,这个服务能够用RegNotifyChangeKeyValue函数去接受一个通知,这样就允许服务快速的重新设置自己。

  前面讲到StartServiceCtrlDispatcher为每一个传递到它的数组中的非空元素产生一个新的线程。接下来,一个ServiceMain要做些什么呢?MSDN里面的原文是这样说的:The ServiceMain function should immediately call the RegisterServiceCtrlHandler function to specify a Handler function to handle control requests. Next, it should call the SetServiceStatus function to send status information to the service control manager. 为什么呢?因为发出启动服务请求之后,如果在一定时间之内无法完成服务的初始化,SCM会认为服务的启动已经失败了,这个时间的长度在Win NT 4.0中是80秒,Win2000中不详…

  基于上面的理由,ServiceMain要迅速完成自身工作,首先是必不可少的两项工作,第一项是调用RegisterServiceCtrlHandler函数去通知SCM它的CtrlHandler回调函数的地址:

SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler(
LPCTSTR lpServiceName, //服务的名字
LPHANDLER_FUNCTION lpHandlerProc //CtrlHandler函数地址
)

  第一个参数指明你正在建立的CtrlHandler是为哪一个服务所用,第二个参数是CtrlHandler函数的地址。lpServiceName必须和在SERVICE_TABLE_ENTRY里面被初始化的服务的名字相匹配。RegisterServiceCtrlHandler返回一个SERVICE_STATUS_HANDLE,这是一个32位的句柄。SCM用它来唯一确定这个服务。当这个服务需要把它当时的状态报告给SCM的时候,就必须把这个句柄传给需要它的Win32函数。注意:这个句柄和其他大多数的句柄不同,你无需关闭它。

  SCM要求ServiceMain函数的线程在一秒钟内调用RegisterServiceCtrlHandler函数,否则SCM会认为服务已经失败。但在这种情况下,SCM不会终止服务,不过在NT 4中将无法启动这个服务,同时会返回一个不正确的错误信息,这一点在Windows 2000中得到了修正。

  在RegisterServiceCtrlHandler函数返回后,ServiceMain线程要立即告诉SCM服务正在继续初始化。具体的方法是通过调用SetServiceStatus函数传递SERVICE_STATUS数据结构。

BOOL SetServiceStatus(
SERVICE_STATUS_HANDLE hService, //服务的句柄
SERVICE_STATUS lpServiceStatus //SERVICE_STATUS结构的地址
)

  这个函数要求传递给它指明服务的句柄(刚刚通过调用RegisterServiceCtrlHandler得到),和一个初始化的SERVICE_STATUS结构的地址:

typedef struct _SERVICE_STATUS
{
DWORD dwServiceType;
DWORD dwCurrentState;
DWORD dwControlsAccepted;
DWORD dwWin32ExitCode;
DWORD dwServiceSpecificExitCode;
DWORD dwCheckPoint;
DWORD dwWaitHint;
} SERVICE_STATUS, *LPSERVICE_STATUS;

  SERVICE_STATUS结构含有七个成员,它们反映服务的现行状态。所有这些成员必须在这个结构被传递到SetServiceStatus之前正确的设置。

  成员dwServiceType指明服务可执行文件的类型。如果你的可执行文件中只有一个单独的服务,就把这个成员设置成SERVICE_WIN32_OWN_PROCESS;如果拥有多个服务的话,就设置成SERVICE_WIN32_SHARE_PROCESS。除了这两个标志之外,如果你的服务需要和桌面发生交互(当然不推荐这样做),就要用“OR”运算符附加上SERVICE_INTERACTIVE_PROCESS。这个成员的值在你的服务的生存期内绝对不应该改变。

  成员dwCurrentState是这个结构中最重要的成员,它将告诉SCM你的服务的现行状态。为了报告服务仍在初始化,应该把这个成员设置成SERVICE_START_PENDING。在以后具体讲述CtrlHandler函数的时候具体解释其它可能的值。

  成员dwControlsAccepted指明服务愿意接受什么样的控制通知。如果你允许一个SCP去暂停/继续服务,就把它设成SERVICE_ACCEPT_PAUSE_CONTINUE。很多服务不支持暂停或继续,就必须自己决定在服务中它是否可用。如果你允许一个SCP去停止服务,就要设置它为SERVICE_ACCEPT_STOP。如果服务要在操作系统关闭的时候得到通知,设置它为SERVICE_ACCEPT_SHUTDOWN可以收到预期的结果。这些标志可以用“OR”运算符组合。

  成员dwWin32ExitCode和dwServiceSpecificExitCode是允许服务报告错误的关键,如果希望服务去报告一个Win32错误代码(预定义在WinError.h中),它就设置dwWin32ExitCode为需要的代码。一个服务也可以报告它本身特有的、没有映射到一个预定义的Win32错误代码中的错误。为了这一点,要把dwWin32ExitCode设置为ERROR_SERVICE_SPECIFIC_ERROR,然后还要设置成员dwServiceSpecificExitCode为服务特有的错误代码。当服务运行正常,没有错误可以报告的时候,就设置成员dwWin32ExitCode为NO_ERROR。

  最后的两个成员dwCheckPoint和dwWaitHint是一个服务用来报告它当前的事件进展情况的。当成员dwCurrentState被设置成SERVICE_START_PENDING的时候,应该把dwCheckPoint设成0,dwWaitHint设成一个经过多次尝试后确定比较合适的数,这样服务才能高效运行。一旦服务被完全初始化,就应该重新初始化SERVICE_STATUS结构的成员,更改dwCurrentState为SERVICE_RUNNING,然后把dwCheckPoint和dwWaitHint都改为0 dwCheckPoint成员的存在对用户是有益的,它允许一个服务报告它处于进程的哪一步。每一次调用SetServiceStatus时,可以增加它到一个能指明服务已经执行到哪一步的数字,它可以帮助用户决定多长时间报告一次服务的进展情况。如果决定要报告服务的初始化进程的每一步,就应该设置dwWaitHint为你认为到达下一步所需的毫秒数,而不是服务完成它的进程所需的毫秒数。

  在服务的所有初始化都完成之后,服务调用SetServiceStatus指明SERVICE_RUNNING,在那一刻服务已经开始运行。通常一个服务是把自己放在一个循环之中来运行的。在循环的内部这个服务进程悬挂自己,等待指明它下一步是应该暂停、继续或停止之类的网络请求或通知。当一个请求到达的时候,服务线程激活并处理这个请求,然后再循环回去等待下一个请求/通知。

  如果一个服务由于一个通知而激活,它会先处理这个通知,除非这个服务得到的是停止或关闭的通知。如果真的是停止或关闭的通知,服务线程将退出循环,执行必要的清除操作,然后从这个线程返回。当ServiceMain线程返回并中止时,引起在StartServiceCtrlDispatcher内睡眠的线程激活,并像在前面解释过的那样,减少它运行的服务的计数。

在Windows中建立一个共享目录,通常是使用NetShareAdd函数.但是在Win2k和Win9x下,这个函数分别是包含在Netapi32.dll和Svrapi.dll中.
那么如果直接在程序中使用Netapi32.lib或Svrapi.lib,在不同的操作系统下运行时会发生找不到xxx函数的输出错误. 解决的办法就是通过GetProcAddress动态获取NetShareAdd.正好ccrun在csdn回答一位网友的问题,顺便写了些代码,在这里贴出来.

注:
1. 以下代码已在Win2k ADV Server和Win9x SE中通过测试.编译时不需要包含lm.h或Svrapi.h,如果包含的话,会引起数据类型重名的错误.
2. 请使用Borland C++ Builder编译器.如果您用VC,个别参数需要改动

代码如下:

typedef DWORD (WINAPI *NETSHAREADD2K)(LPWSTR, DWORD, LPBYTE, LPDWORD);
typedef DWORD (WINAPI *NETSHAREADD9X)(const char FAR*, shortconst char FAR*, unsigned short);
NETSHAREADD2K NetShareAdd2k;
NETSHAREADD9X NetShareAdd9x;

bool bWinNT; // 操作系统标志
HINSTANCE hDll; // DLL文件句柄

#define NNLEN           80      // 共享名的长度
#define LM20_NNLEN      12      // LM 2.0 共享名的长度
#define SHPWLEN         8       // 共享密码的长度
#define STYPE_DISKTREE  0       // 磁盘共享
#define SHI50F_FULL     0×0002
#define NERR_Success    0       // 操作成功

#define NERR_BASE                2100
#define NERR_DuplicateShare     (NERR_BASE+18)  // 共享名称已经存在
#define NERR_RedirectedPath     (NERR_BASE+17)  // 重定向资源的操作非法
#define NERR_UnknownDevDir      (NERR_BASE+16)  // 设备或目录不存在

typedef struct _share_info_50 // 用于Win9x
{
    char            shi50_netname[LM20_NNLEN+1];  // 共享名
    unsigned char   shi50_type;                   // 
    unsigned short  shi50_flags;                  // 
    char FAR *      shi50_remark;                 // ANSI 备注
    char FAR *      shi50_path;                   // 共享的资源,也就是共享的目录
    char            shi50_rw_password[SHPWLEN+1]; // 可读写的密码
    char            shi50_ro_password[SHPWLEN+1]; // 只读密码
}share_info_50;

typedef struct _SHARE_INFO_2 // 用于Win2k
{
    LPWSTR  shi2_netname;
    DWORD   shi2_type;
    LPWSTR  shi2_remark;
    DWORD   shi2_permissions;
    DWORD   shi2_max_uses;
    DWORD   shi2_current_uses;
    LPWSTR  shi2_path;
    LPWSTR  shi2_passwd;
}SHARE_INFO_2, *PSHARE_INFO_2, *LPSHARE_INFO_2;

//—————————————————————————
bool __fastcall InitApi() // 根据操作系统的不同,从Dll中获取API
{
    // 判断系统平台
    OSVERSIONINFO info;
    info.dwOSVersionInfoSize = sizeof(info);
    GetVersionEx(&info);
    bWinNT = (info.dwPlatformId == VER_PLATFORM_WIN32_NT)? truefalse;

    if(bWinNT)
    {
        hDll = LoadLibrary("Netapi32.dll"); // win2k
        if(hDll)
        {
            NetShareAdd2k = (NETSHAREADD2K)GetProcAddress(hDll, "NetShareAdd");
            return (NetShareAdd2k != NULL);
        }
        else
            return false;
    }
    else
    {
        hDll = LoadLibrary("Svrapi.dll"); // win9x
        if(hDll)
        {
            NetShareAdd9x = (NETSHAREADD9X)GetProcAddress(hDll, "NetShareAdd");
            return NetShareAdd9x != NULL;
        }
        else
            return false;
    }
}
//—————————————————————————
String __fastcall MyPathToNetShare(
    String strPath,        // 共享目录
    String strNetName,     // 共享名
    String strPassword,    // 密码
    String strRemark)      // 共享目录的注释
{
    if(!InitApi())
        return "函数获取错误!";

    // 文档标题:如何在Win9x/2k下建立共享目录
    // 作者:ccrun(老妖) 信箱:info@ccrun.com
    // 为防止不负责任的转载者遗漏作者信息,故在此插入此信息,请见谅.
    // 欢迎访问C++Builder 研究 http://www.ccrun.com
    
    if(!bWinNT)  // 操作系统是win9x
    {
        char FAR *pszServerName = NULL;
        short nLevel = 50;
        struct share_info_50* pBuf = NULL;
        unsigned short cbBuffer;
        DWORD nStatus;
        cbBuffer = sizeof(struct share_info_50);
        pBuf = (share_info_50 *)malloc(cbBuffer);
        if(pBuf == NULL)
            return "内存分配失败!";

        // Assign values to the share_info_50 structure.
        strcpy(pBuf->shi50_netname, strNetName.c_str());
        pBuf->shi50_type = STYPE_DISKTREE;
        pBuf->shi50_flags = SHI50F_FULL;
        pBuf->shi50_remark = strRemark.c_str();
        pBuf->shi50_path = strPath.c_str();
        strcpy(pBuf->shi50_rw_password, strPassword.c_str()); // No password
        strcpy(pBuf->shi50_ro_password, strPassword.c_str()); // No password
        
        // Call the NetShareAdd function specifying information level 50.
        nStatus = NetShareAdd9x(pszServerName,
                             nLevel,
                             (char FAR *)pBuf,
                             cbBuffer);
                             
        // 释放分配的内存
        if(pBuf != NULL)
            free(pBuf);
        //
        FreeLibrary(hDll);
                
        if(nStatus == NERR_Success)
            return("添加成功!");
        else
            return String("有错误发生,返回码: " + String(nStatus));
    }
    else   // 操作系统是Win2k
    {
        HANDLE hToken;
        TOKEN_PRIVILEGES tk;
        
        // 首先提升本进程的调试级别
        OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken);
        LookupPrivilegeValue(NULL, SE_SHUTDOWN_NAME, &tk.Privileges[0].Luid);
        tk.PrivilegeCount = 1;
        tk.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
        AdjustTokenPrivileges(hToken, FALSE, &tk, 0, (PTOKEN_PRIVILEGES)NULL, 0);
        
        DWORD    nas;
        SHARE_INFO_2      p;
        DWORD             dwParamErr = NULL;
        p.shi2_path           = StringToOleStr(strPath);
        p.shi2_remark         = StringToOleStr(strRemark);
        p.shi2_netname        = StringToOleStr(strNetName);
        p.shi2_passwd         = StringToOleStr(strPassword);
        p.shi2_type           = STYPE_DISKTREE; // disk drive
        p.shi2_permissions    = 0;
        p.shi2_max_uses       = -1;
        p.shi2_current_uses   = 0;
        nas = NetShareAdd2k(NULL, 2, (LPBYTE)&p, &dwParamErr);
        
        FreeLibrary(hDll);
        
        switch(nas)
        {
            case NERR_Success:              return "设置成功!";
            case ERROR_ACCESS_DENIED:       return "拒绝访问!";
            case ERROR_INVALID_LEVEL:       return "非法的级别!";
            case ERROR_INVALID_NAME:        return "非法的名称!";
            case ERROR_INVALID_PARAMETER:   return "参数非法!";
            case NERR_DuplicateShare:       return "共享重名!";
            case NERR_RedirectedPath:       return "重定向路径!";
            case NERR_UnknownDevDir:        return "未知的驱动路径!";
            default:                        return "未知错误!";
        }
    }
}
// 调用举例
//—————————————————————————
void __fastcall TForm1::Button1Click(TObject *Sender)
{
    ShowMessage(MyPathToNetShare("C:\\ccrun\\123""MyShare""""共享目录的注释"));
}