调试COM+应用的安全性
这是一个比较简单但可以被用来测试远程访问COM+数据库的应用,在服务端只使用了Delphi
中TClientDataset的直接数据访问功能,理论上不需要任何后端数据库驱动程序支持,
这样也就减少了程序在调试过程中的不可控因素,可以让我们将关注点放在核心的客户远程
访问问题上。
先简单介绍下服务器端的代码:
unit uDemoRmtDB;
中TClientDataset的直接数据访问功能,理论上不需要任何后端数据库驱动程序支持,
这样也就减少了程序在调试过程中的不可控因素,可以让我们将关注点放在核心的客户远程
访问问题上。
先简单介绍下服务器端的代码:
unit uDemoRmtDB;
{$WARN SYMBOL_PLATFORM OFF}
interface
uses
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComServ, ComObj, VCLCom, StdVcl, bdemts, DataBkr, DBClient,
MtsRdm, Mtx, MTSDemoSvr_TLB, DB, Provider;
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
ComServ, ComObj, VCLCom, StdVcl, bdemts, DataBkr, DBClient,
MtsRdm, Mtx, MTSDemoSvr_TLB, DB, Provider;
type
TMTSDemo = class(TMtsDataModule, IMTSDemo)
ClientDataSet1: TClientDataSet;
ClientDataSet1Name: TStringField;
ClientDataSet1Address: TStringField;
ClientDataSet1PhoneNum: TIntegerField;
ClientDataSet1HandPhone1: TIntegerField;
ClientDataSet1HandPhone2: TIntegerField;
private
{ Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;
procedure GetDemoData(out vData: OleVariant); safecall;
procedure SaveDemoData(vData: OleVariant); safecall;
public
{ Public declarations }
end;
TMTSDemo = class(TMtsDataModule, IMTSDemo)
ClientDataSet1: TClientDataSet;
ClientDataSet1Name: TStringField;
ClientDataSet1Address: TStringField;
ClientDataSet1PhoneNum: TIntegerField;
ClientDataSet1HandPhone1: TIntegerField;
ClientDataSet1HandPhone2: TIntegerField;
private
{ Private declarations }
protected
class procedure UpdateRegistry(Register: Boolean; const ClassID, ProgID: string); override;
procedure GetDemoData(out vData: OleVariant); safecall;
procedure SaveDemoData(vData: OleVariant); safecall;
public
{ Public declarations }
end;
var
MTSDemo: TMTSDemo;
MTSDemo: TMTSDemo;
implementation
{$R *.DFM}
class procedure TMTSDemo.UpdateRegistry(Register: Boolean; const ClassID, ProgID: string);
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
begin
if Register then
begin
inherited UpdateRegistry(Register, ClassID, ProgID);
EnableSocketTransport(ClassID);
EnableWebTransport(ClassID);
end else
begin
DisableSocketTransport(ClassID);
DisableWebTransport(ClassID);
inherited UpdateRegistry(Register, ClassID, ProgID);
end;
end;
procedure TMTSDemo.GetDemoData(out vData: OleVariant);
begin
with ClientDataSet1 do begin
LoadFromFile('MyHandPhone.xml');
vData:=data;
end;
end;
begin
with ClientDataSet1 do begin
LoadFromFile('MyHandPhone.xml');
vData:=data;
end;
end;
procedure TMTSDemo.SaveDemoData(vData: OleVariant);
begin
with ClientDataSet1 do begin
Data:=vData;
SaveToFile('MyHandPhone.xml');
end;
end;
begin
with ClientDataSet1 do begin
Data:=vData;
SaveToFile('MyHandPhone.xml');
end;
end;
initialization
TComponentFactory.Create(ComServer, TMTSDemo,
Class_MTSDemo, ciMultiInstance, tmApartment);
end.
这段代码中只是简单在服务器端实现了GetDemoData和SaveDemoData两个接口方法,
分别用于获取服务器端的数据和将数据保存至服务器。数据文件是MyHandPhone.xml,
从文件名上大家也能看出这是一个不能再简单的电话簿。这个文件可以直接由TClientDataset
的属性编辑器生成,生成过程就不详细介绍了,大家可以在MTS数据模块中的TClientDataset
控件上右键点击观查其功能菜单项。
服务器代码非常简单相信方家不值一晒。
============================
下面是客户端的代码,客户端的GUI上只是一个DBGird控件和几个TButton,用于浏览和
调用远程服务器上的方法。比较特殊的是,为了观察接口的引用计数,我在GUI上放了个TTimer
控件,用于监视接口的引用计数。
GUI主窗体的代码如下:
TComponentFactory.Create(ComServer, TMTSDemo,
Class_MTSDemo, ciMultiInstance, tmApartment);
end.
这段代码中只是简单在服务器端实现了GetDemoData和SaveDemoData两个接口方法,
分别用于获取服务器端的数据和将数据保存至服务器。数据文件是MyHandPhone.xml,
从文件名上大家也能看出这是一个不能再简单的电话簿。这个文件可以直接由TClientDataset
的属性编辑器生成,生成过程就不详细介绍了,大家可以在MTS数据模块中的TClientDataset
控件上右键点击观查其功能菜单项。
服务器代码非常简单相信方家不值一晒。
============================
下面是客户端的代码,客户端的GUI上只是一个DBGird控件和几个TButton,用于浏览和
调用远程服务器上的方法。比较特殊的是,为了观察接口的引用计数,我在GUI上放了个TTimer
控件,用于监视接口的引用计数。
GUI主窗体的代码如下:
unit MTSDemoClnt platform;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, DBGrids, DB, DBClient, MConnect, StdCtrls, ExtCtrls;
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, Grids, DBGrids, DB, DBClient, MConnect, StdCtrls, ExtCtrls;
type
TForm1 = class(TForm)
DataSource1: TDataSource;
ClientDataSet1: TClientDataSet;
DBGrid1: TDBGrid;
btGetData: TButton;
btSaveData: TButton;
btExit: TButton;
edRef: TEdit;
Timer1: TTimer;
procedure btGetDataClick(Sender: TObject);
procedure btSaveDataClick(Sender: TObject);
procedure btExitClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
TForm1 = class(TForm)
DataSource1: TDataSource;
ClientDataSet1: TClientDataSet;
DBGrid1: TDBGrid;
btGetData: TButton;
btSaveData: TButton;
btExit: TButton;
edRef: TEdit;
Timer1: TTimer;
procedure btGetDataClick(Sender: TObject);
procedure btSaveDataClick(Sender: TObject);
procedure btExitClick(Sender: TObject);
procedure Timer1Timer(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
Form1: TForm1;
implementation
uses comobj,MTSDemoSvr_TLB, uRmtObj;
{$R *.dfm}
var
it{,it2,it3,it4}:IMTSDemoDisp;
it{,it2,it3,it4}:IMTSDemoDisp;
procedure TForm1.btGetDataClick(Sender: TObject);
var
tmp:OleVariant;
//IID_IUnknown:TGUID;
begin
//IID_IUnknown:=IUnknown;
it:=IMTSDemoDisp(DoConnect(
@CLASS_MTSDemo,@IID_IMTSDemo,'omiga','ibrow','ibrow'));
it.GetDemoData(tmp);
ClientDataSet1.Data:=tmp;
// it2:=it;
// it3:=it;
// it4:=it;
end;
var
tmp:OleVariant;
//IID_IUnknown:TGUID;
begin
//IID_IUnknown:=IUnknown;
it:=IMTSDemoDisp(DoConnect(
@CLASS_MTSDemo,@IID_IMTSDemo,'omiga','ibrow','ibrow'));
it.GetDemoData(tmp);
ClientDataSet1.Data:=tmp;
// it2:=it;
// it3:=it;
// it4:=it;
end;
procedure TForm1.btSaveDataClick(Sender: TObject);
begin
if Assigned(it) then begin
it.SaveDemoData(ClientDataset1.data);
end;
end;
begin
if Assigned(it) then begin
it.SaveDemoData(ClientDataset1.data);
end;
end;
procedure TForm1.btExitClick(Sender: TObject);
begin
it:=nil;//主动清除接口引用,用于测试关闲接口时,远程服务器上的认证反应
close;
end;
begin
it:=nil;//主动清除接口引用,用于测试关闲接口时,远程服务器上的认证反应
close;
end;
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:integer;
begin
if Assigned(it) then begin
i:=IUnknown(it)._AddRef;
i:=IUnknown(it)._Release;
edRef.Text:=inttostr(i);
end;
end;
var
i:integer;
begin
if Assigned(it) then begin
i:=IUnknown(it)._AddRef;
i:=IUnknown(it)._Release;
edRef.Text:=inttostr(i);
end;
end;
end.
主窗体代码中需要说明的的代码是 btGetDataClick(Sender: TObject),其中我用
自编的过程DoConnect()来获取远程计算机上的dispinterface接口,这里的关键点就在
DoConnect()上,该过程在uRmtObj单位无中实现,其代码如下:
unit uRmtObj platform;
自编的过程DoConnect()来获取远程计算机上的dispinterface接口,这里的关键点就在
DoConnect()上,该过程在uRmtObj单位无中实现,其代码如下:
unit uRmtObj platform;
interface
uses windows,comobj,activex;
type
pUnShort=^Word;
pCoAuthIdentity=^_CoAuthIdentity;
_CoAuthIdentity=record
user:PWideChar;
UserLength:ULONG;
Domain:PWideChar;
DomainLength:Ulong;
password:PWideChar;
PasswordLength:ulong;
Flags:ulong;
end;
_CoAuthIdentity=record
user:PWideChar;
UserLength:ULONG;
Domain:PWideChar;
DomainLength:Ulong;
password:PWideChar;
PasswordLength:ulong;
Flags:ulong;
end;
_CoAuthInfo=record
dwAuthnSvc:DWORD;
dwAuthzSvc:DWORD;
pwszServerPrincName:PWideChar;
dwAuthnLevel:DWORD;
dwImpersonationLevel:DWORD;
pAuthIdentityData:pCoAuthIdentity;
dwCapabilities:DWORD;
end;
dwAuthnSvc:DWORD;
dwAuthzSvc:DWORD;
pwszServerPrincName:PWideChar;
dwAuthnLevel:DWORD;
dwImpersonationLevel:DWORD;
pAuthIdentityData:pCoAuthIdentity;
dwCapabilities:DWORD;
end;
Function MySetBlanket(var itf: IUnknown; const vCai: _CoAuthInfo):HRESULT;
function DoConnect(const Class_IID,itf_iid:PIID; computer,username,psw:WideString):IUnknown;
implementation
Function MySetBlanket(var itf: IUnknown; const vCai: _CoAuthInfo):HRESULT;
begin
with vCai do begin
result:=CoSetProxyBlanket(Itf,dwAuthnSvc,dwAuthzSvc,pwidechar(pAuthIdentityData^.Domain),
dwAuthnLevel,dwImpersonationLevel,pAuthIdentityData,dwCapabilities);
end;
end;
begin
with vCai do begin
result:=CoSetProxyBlanket(Itf,dwAuthnSvc,dwAuthzSvc,pwidechar(pAuthIdentityData^.Domain),
dwAuthnLevel,dwImpersonationLevel,pAuthIdentityData,dwCapabilities);
end;
end;
function DoConnect(const Class_IID,itf_iid:PIID; computer,username,psw:WideString):IUnknown;
var
FCai:_CoAuthInfo;
FCid:_CoAuthIdentity;
FSvInfo:COSERVERINFO;
//tmpCmpName:widestring;
//IID_IUnknown:TGUID;
//iiu:IDispatch;
Mqi:MULTI_QI;
Size: DWORD;
LocalMachine: array [0..MAX_COMPUTERNAME_LENGTH] of char;
//qr:HRESULT;
begin
Result:=nil;
if Length(computer)>0 then begin
size:=sizeof(LocalMachine);
if GetComputerName(LocalMachine,size) and (computer <> LocalMachine) then
begin
FillMemory(@Fcai,sizeof(Fcai),0);
FillMemory(@FCid,sizeof(FCid),0);
FillMemory(@FSvInfo,sizeof(FSvInfo),0);
with fcid do begin
user:=pwideChar(userName);//pUnshort(@userName[1]);
UserLength:=length(username);
Domain:=pWideChar(Computer);//pUnshort(@computer[1]);
DomainLength:=length(computer);
password:=pWideChar(psw);//pUnShort(@psw[1]);
PasswordLength:=length(psw);
Flags:=2;//Unicode 字符串
end;
with FCai do begin
dwAuthnSvc:=10;//RPC_C_AUTHN_WINNT NTML认证服务
dwAuthzSvc:=0;// RPC_C_AUTHZ_NONE
dwAuthnLevel:=0;//RPC_C_AUTHN_LEVEL_DEFAULT 默认级别
dwImpersonationLevel:=3;//身份模拟
pAuthIdentityData:=@fcid;
dwCapabilities:=$0800;//静态跟踪
end;
FSvInfo.pwszName:=PWideChar(computer);
FSvinfo.pAuthInfo:=@Fcai;
var
FCai:_CoAuthInfo;
FCid:_CoAuthIdentity;
FSvInfo:COSERVERINFO;
//tmpCmpName:widestring;
//IID_IUnknown:TGUID;
//iiu:IDispatch;
Mqi:MULTI_QI;
Size: DWORD;
LocalMachine: array [0..MAX_COMPUTERNAME_LENGTH] of char;
//qr:HRESULT;
begin
Result:=nil;
if Length(computer)>0 then begin
size:=sizeof(LocalMachine);
if GetComputerName(LocalMachine,size) and (computer <> LocalMachine) then
begin
FillMemory(@Fcai,sizeof(Fcai),0);
FillMemory(@FCid,sizeof(FCid),0);
FillMemory(@FSvInfo,sizeof(FSvInfo),0);
with fcid do begin
user:=pwideChar(userName);//pUnshort(@userName[1]);
UserLength:=length(username);
Domain:=pWideChar(Computer);//pUnshort(@computer[1]);
DomainLength:=length(computer);
password:=pWideChar(psw);//pUnShort(@psw[1]);
PasswordLength:=length(psw);
Flags:=2;//Unicode 字符串
end;
with FCai do begin
dwAuthnSvc:=10;//RPC_C_AUTHN_WINNT NTML认证服务
dwAuthzSvc:=0;// RPC_C_AUTHZ_NONE
dwAuthnLevel:=0;//RPC_C_AUTHN_LEVEL_DEFAULT 默认级别
dwImpersonationLevel:=3;//身份模拟
pAuthIdentityData:=@fcid;
dwCapabilities:=$0800;//静态跟踪
end;
FSvInfo.pwszName:=PWideChar(computer);
FSvinfo.pAuthInfo:=@Fcai;
//IID_IUnknown:=IUnknown;
//mqi.IID:=@IID_IUnknown;mqi.Itf:=nil;mqi.hr:=0;
//mqi.IID:=@IID_IUnknown;mqi.Itf:=nil;mqi.hr:=0;
with mqi do begin
iid:=itf_iid;
itf:=nil;
hr:=0;
end;
//以远程用户身份激活并取得接口引用
olecheck(CoCreateInstanceEx(class_iid^,nil,CLSCTX_REMOTE_SERVER,@FSvinfo,1,@mqi));
olecheck(mqi.hr);
//对取得的接口引用,要再次设置其安全属性为远程用户,否则返回的指针将仍然
//使用本地用户进程的安全属性向远程发起调用,此时的结果就是"拒绝访问"
olecheck(MySetBlanket(mqi.Itf,Fcai));
//qr:=mqi.Itf.QueryInterface(IID_IMySendKey,result);
//olecheck(qr);
//MySetBlanket(mqi.itf,FCai);
result:=mqi.itf;
end;
end
else
OleCheck(CoCreateInstance(class_iid^, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, itf_iid^, Result));
end;
iid:=itf_iid;
itf:=nil;
hr:=0;
end;
//以远程用户身份激活并取得接口引用
olecheck(CoCreateInstanceEx(class_iid^,nil,CLSCTX_REMOTE_SERVER,@FSvinfo,1,@mqi));
olecheck(mqi.hr);
//对取得的接口引用,要再次设置其安全属性为远程用户,否则返回的指针将仍然
//使用本地用户进程的安全属性向远程发起调用,此时的结果就是"拒绝访问"
olecheck(MySetBlanket(mqi.Itf,Fcai));
//qr:=mqi.Itf.QueryInterface(IID_IMySendKey,result);
//olecheck(qr);
//MySetBlanket(mqi.itf,FCai);
result:=mqi.itf;
end;
end
else
OleCheck(CoCreateInstance(class_iid^, nil, CLSCTX_INPROC_SERVER or
CLSCTX_LOCAL_SERVER, itf_iid^, Result));
end;
end.
过程DoConnect()用指定的远程计算机上的用户名/密码来激活远程服务器并获得指定的接口
引用,定义如下
function DoConnect(const Class_IID,
itf_iid:PIID;
computer,
username,
psw:WideString):IUnknown;
Class_IID是远程MTS对象的类ID,itf_iid是要取得的接口指针ID(实际是指向一个TGUID
结构的指针)。computer是远程计算机的名称,username是远程计算机上的用户名,psw是该用
户的密码。DoConnect返回一个指定接口类型的引用。由于返回接口类型为IUnknown,所以在具
体使用时要做强制类型转换。
示例:
it:=IMTSDemoDisp(DoConnect( @CLASS_MTSDemo,
@IID_IMTSDemo,
'omiga','ibrow','ibrow'));
引用,定义如下
function DoConnect(const Class_IID,
itf_iid:PIID;
computer,
username,
psw:WideString):IUnknown;
Class_IID是远程MTS对象的类ID,itf_iid是要取得的接口指针ID(实际是指向一个TGUID
结构的指针)。computer是远程计算机的名称,username是远程计算机上的用户名,psw是该用
户的密码。DoConnect返回一个指定接口类型的引用。由于返回接口类型为IUnknown,所以在具
体使用时要做强制类型转换。
示例:
it:=IMTSDemoDisp(DoConnect( @CLASS_MTSDemo,
@IID_IMTSDemo,
'omiga','ibrow','ibrow'));
将服务器端编译并安装到远程计算机的COM+应用中,然后运行客户端,然后可以进入
调试了。
调试的步骤:
1、打开远程计算机上的"事件查看器",我的建议是运行dcomcnfg.exe,使用其中的事件
查看器,这样你在高度过程中可以用dcomcnfg来随时调整远程计算机上的安全设置。
2、支行本地计算机上的dcomcnfg.exe,切换到事件查看器。
3、将远程计算机和本地计算上"应用程序","安全性","系统"三个日志全部清空
4、 在urmtObj单元doconnect()过程中设断点,建议最少在这两行上高断点:
olecheck(CoCreateInstanceEx(class_iid^,nil,CLSCTX_REMOTE_SERVER,@FSvinfo,1,@mqi));
olecheck(MySetBlanket(mqi.Itf,Fcai));
在主窗体btExitClick()中下述代码上设断点
it:=nil;
5、每个断点采用调试模式的CPU窗口模式逐步跟进,并注意密切观查远程计算机和本地
计算机上的事件日志。
6、将远程计算机上com安全认证由最严格向最松散调整,将在每一个安全级别上跟踪代码
运行情况。
好了,经过上述的试验后,你应该最少有以下几点认识:
1、远程计算机上要产生多次的用户登录认证
2、“拒绝访问”提示在本地和远程计算机上都有提示,但内容的详细程序有别。要解决
"拒绝访问"问题,要同时参考两台计算机上的日志。建议你在远程计算机上用组策略编辑
器打开帐户的登录、注销、提权审核,这样你的日志会更详细,有助于你解决问题
3、远程计算机上的com默认模拟级别可能要设置成"模拟"。
4、远程计算机上,你应该将用于远程访问COM+接口的用户加入到 "访问权限"及
"启动和激活权限"中去,并且你可以控制启动、激活、访问三个阶段的安全性。
5、如果你的远程系统是windows server 2003,别忘记将用来访问的用户加入到com+应用
的CreatorOwner角色里,如何加?自己找......呵呵。
6、源代码在我的资源中有打包下栽。
7、调试完了,告诉我你的心得,并且帮我解决个问题:
为什么接口注销后,远程系统安全日志中会出现两个用客户计算机用户名进行的失败登录?
8、如果怎么整都无法完成远程访问,别骂我,我的代码没问题,你的客户和远程计算机安全设置
有问题。建议看看我的其他文章(不许拍黑砖!!!!)
9、请将你的数据库文件myhandphone.xml主在windows系统的搜索目录里,刚开始最后放在windows/system32目录里,否则你会看到令你头晕的、无法解释的拒绝访问提示。,为什么?
应为com+应用是由dllhost.exe这个系统程序以服务账户创建的,它不会在你的dll的当前目录
中寻找其他文件。同时,请注意正确设置数据文件的访问权限,因为如果你用非管理员账号
远程访问它,很可能会因为只有读而没有写的权利,那就又会出问题。
调试了。
调试的步骤:
1、打开远程计算机上的"事件查看器",我的建议是运行dcomcnfg.exe,使用其中的事件
查看器,这样你在高度过程中可以用dcomcnfg来随时调整远程计算机上的安全设置。
2、支行本地计算机上的dcomcnfg.exe,切换到事件查看器。
3、将远程计算机和本地计算上"应用程序","安全性","系统"三个日志全部清空
4、 在urmtObj单元doconnect()过程中设断点,建议最少在这两行上高断点:
olecheck(CoCreateInstanceEx(class_iid^,nil,CLSCTX_REMOTE_SERVER,@FSvinfo,1,@mqi));
olecheck(MySetBlanket(mqi.Itf,Fcai));
在主窗体btExitClick()中下述代码上设断点
it:=nil;
5、每个断点采用调试模式的CPU窗口模式逐步跟进,并注意密切观查远程计算机和本地
计算机上的事件日志。
6、将远程计算机上com安全认证由最严格向最松散调整,将在每一个安全级别上跟踪代码
运行情况。
好了,经过上述的试验后,你应该最少有以下几点认识:
1、远程计算机上要产生多次的用户登录认证
2、“拒绝访问”提示在本地和远程计算机上都有提示,但内容的详细程序有别。要解决
"拒绝访问"问题,要同时参考两台计算机上的日志。建议你在远程计算机上用组策略编辑
器打开帐户的登录、注销、提权审核,这样你的日志会更详细,有助于你解决问题
3、远程计算机上的com默认模拟级别可能要设置成"模拟"。
4、远程计算机上,你应该将用于远程访问COM+接口的用户加入到 "访问权限"及
"启动和激活权限"中去,并且你可以控制启动、激活、访问三个阶段的安全性。
5、如果你的远程系统是windows server 2003,别忘记将用来访问的用户加入到com+应用
的CreatorOwner角色里,如何加?自己找......呵呵。
6、源代码在我的资源中有打包下栽。
7、调试完了,告诉我你的心得,并且帮我解决个问题:
为什么接口注销后,远程系统安全日志中会出现两个用客户计算机用户名进行的失败登录?
8、如果怎么整都无法完成远程访问,别骂我,我的代码没问题,你的客户和远程计算机安全设置
有问题。建议看看我的其他文章(不许拍黑砖!!!!)
9、请将你的数据库文件myhandphone.xml主在windows系统的搜索目录里,刚开始最后放在windows/system32目录里,否则你会看到令你头晕的、无法解释的拒绝访问提示。,为什么?
应为com+应用是由dllhost.exe这个系统程序以服务账户创建的,它不会在你的dll的当前目录
中寻找其他文件。同时,请注意正确设置数据文件的访问权限,因为如果你用非管理员账号
远程访问它,很可能会因为只有读而没有写的权利,那就又会出问题。
10、有许多代码被注释掉了,你可以试着去掉注释,再看看运行中的反应。注意是事件中的反应。
祝DELPHI同志们好运。
祝DELPHI同志们好运。