Win 7的CBS_Persist_X.log占据大量硬盘空间 (76GB in my case)

公司电脑Win7的系统,硬盘空间时不时报警,一直没怎么管,最近实在看不下去了,弄了一下。

  1. 首先,需要找到什么文件占据大量的磁盘空间,推荐一个工具 — WinDirStat
    https://windirstat.net/
  2. 发现C:\Windows\Logs\CBS\目录下有大量的cbs_persist_xxx.log
  3. 原因是Win7一个Bug,微软一直知晓,却一直也没动静:
    http://www.infoworld.com/article/3112358/microsoft-windows/windows-7-log-file-compression-bug-can-fill-up-your-hard-drive.html
  4. 解决方法如下:
    1. 运行->services.msc
    2. 停掉Windows Modules Installer服务
    3. 删除C:\Windows\Logs\CBS\下面的全部内容
    4. 【可选】删除全部C:\Windows\Temp\cab*
    5. 重启电脑

嗯…

手动解密微软Agile Encryption的ECMA-376文档

先给一些参考资料,如果有漏掉的部分,可以参考这里:

  1. [MS-OFFCRYPTO]: Office Document Cryptography Structure
  2. [MS-CFB]: Compound File Binary File Format
  3. Standard ECMA-376 Office Open XML File Formats
  4. Apache POI Encryption Support

一 解析CFB文件结构

先介绍一下CFB文件结构,以从中取出解密需要的信息:

  1. CFB文件被切分为等长的Sector,然后用如下方式组织起来:
    Header Sector + Sector #0 + Sector #1 + …
    由于有Header的存在,算Sector偏移的时候,Sector Number需要加一。
  2. Sector之间由FAT表关联,形成多条Sector链
    FAT[0] – Sector #0 的下一个Sector Number (4 bytes)
    FAT[1] – Sector #1 的下一个Sector Number

    还有一些特殊的内容,比如:
    FAT Sector (0xFFFFFFFD)
    链结尾 (0xFFFFFFFE)
    空的占位符 (0xFFFFFFFF)

    例子:
    FAT[0]: fd ff ff ff
    FAT[1]: 04 00 00 00

  3. FAT表也存储在Sector里,FAT表由DIFAT表关联,关联方式和FAT一致,唯一的区别是前109个DIFAT项,被直接存储在Header Sector里。

    例子:
    DIFAT[0]: 00 00 00 00
    DIFAT[1]: 07 00 00 00

  4. Sector内存储了各种类型的数据,包括一个简单的类似文件系统的树状结构。

    其中,Storage类似于文件夹,Stream类似于文件。
    Root Storage是唯一的根目录,下面挂其他的Storage或者Stream。

    由于Stream大小比较大,还提供了Mini Stream,用于存储比较小的数据。

  5. Header的结构参考[MS-CFB] 2.2 Compound File Header,一些关键信息如下:
    1. Byte Order – 确定字节序 (0xFFFE)
    2. Sector Shift – 单个Sector的大小 0x9 (512B) 或者 0xc (4096B)
    3. Mini Sector Shift – 单个Mini Sector的大小 0x6 (64B)
    4. Mini Stream Cutoff Size – 小于这个大小的数据,被放在Mini Stream里 (4096B)
    5. First Directory Sector Location – Directory Stream的起始Sector Number
    6. First Mini FAT Sector Location – Mini FAT表的起始Sector Number
    7. First DIFAT Sector – DIFAT表的起始Sector Number (如果Header里的109项已经够用了,则为链结尾 – 0xFFFFFFFE)
  6. 接下来解析Directory Stream所在的起始Sector
    1. 偏移:[(Sector Number + 1) * Sector Shift]
    2. 每个Directory Entry的大小是128B,如果Sector大小为512B,则每个Sector可以放四个Directory Entry
    3. Directory Entry的结构参考[MS-CFB] 2.6.1 Compound File Directory Entry,一些关键信息解释如下:
      1. Directory Entry Name – 项名称 一般第一个为Root Entry [UTF-16]
      2. Object Type – 0x0 未分配 0x1 Storage 0x2 Stream 0x5 Root Storage
      3. Child ID:子项Directory Entry的ID (如果没有子项,则为0xFFFFFFFF)
      4. Left Sibling ID: 左兄弟项Directory Entry的ID (如果没有左兄弟项,则为0xFFFFFFFF)
      5. Right Sibling ID: 右兄弟项Directory Entry的ID (如果没有右兄弟项,则为0xFFFFFFFF)
      6. Starting Sector Location – 对Stream而言,表示起始Sector Number;对Root Storage而言,则指示了Mini Stream的起始Sector Number
      7. Stream Size: 对Stream而言,表示数据大小;对Root Storage而言,则表示Mini Stream的大小
    4. 例子
      Directory Entry [0]:
      Directory Entry Name: 52 00 6f 00 6f 00 74 00 20 00 45 00 6e 00 74 00 72 00 79 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [UTF-16]
      Directory Entry Name: Root Entry [UTF-16]
      Directory Entry Name Length: 16 00
      Object Type: 05
      Color Flag: 00 [0x00 Red 0x01 Black]
      Left Sibling ID: ff ff ff ff
      Right Sibling ID: ff ff ff ff
      Child ID: 0a 00 00 00
      CLSID: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
      State Bits: 00 00 00 00
      Creation Time: 00 00 00 00 00 00 00 00
      Modified Time: 00 3d 2a 3d 8e 12 d2 01
      Starting Sector Location: 03 00 00 00
      Stream Size: 00 08 00 00 00 00 00 00
  7. 根据以上的内容,我们可以得到两个关键的Stream
    1. EncryptionInfo Stream,这个Stream包含一个明文的XML字符串,包含我们需要的解密相关信息
      1. 从提供的密码 和 EncryptedKeyValue里解密中间密钥 encryption/keyEncryptors/keyEncryptor/encryptedKey
        1. spinCount – 加密多少轮,例如10000
        2. saltSize
        3. blockSize
        4. keyBits – 决定AES128 或者 AES256
        5. hashSize
        6. cipherAlgorithm – 例如AES
        7. cipherChaining – 例如CBC
        8. hashAlgorithm – 例如SHA512
        9. saltValue – 用于加密中间密钥的盐值 (base64 编码)
        10. encryptedKeyValue – 加密后的中间密钥 (base64 编码)
      2. 用上面获得的中间密钥解密实际数据 encryption/keyData
        1. saltSize
        2. blockSize
        3. keyBits
        4. hashSize
        5. cipherAlgorithm
        6. cipherChaining
        7. hashAlgorithm
        8. saltValue
    2. EncryptedPackage Stream,这个Stream包含我们待解密的数据
      1. Stream Size : 8 bytes (无符号整数)
      2. Encrypted Data: variable size

二 解密中间密钥

我们用AES256 + SHA512的组合举例, AES256需要Key和初始向量IV

  1. 预先处理
    1. Salt Value 和 Encrypted Key Value 在XML里是base64编码的,我们需要先解码为二进制 base64.b64decode()
    2. 用户输入的密码也需要用utf-16小尾编码 “password”.encode(“utf-16le”)
  2. 获得Key
    1. SHA512编码 (encryptedKey/saltValue + 用户输入的密码)
      pwHash = hashlib.sha512()
      pwHash.update(saltValue)
      pwHash.update(password)
      key = pwHash.digest()
    2. SHA512编码spinCount轮 Hn = H(count + Hn-1)
      其中count为从0开始的无符号32位数
      for i in xrange(spinCount):
      pwHash = hashlib.sha512()
      pwHash.update(struct.pack(“<I”, i))
      pwHash.update(key)
      key = pwHash.digest()
    3. SHA512编码 Hfinal = H(Hn + BlockKey)
      如果Hfinal大于keyBits,则需要按keyBits截断;
      反之,如果小于,则需要用0x36来append补足
      pwHash = hashlib.sha512()
      pwHash.update(key)
      pwHash.update(struct.pack(‘<BBBBBBBB’, 0x14, 0x6e, 0x0b, 0xe7, 0xab, 0xac, 0xd0, 0xd6))
      key = pwHash.digest()[:32]
  3. 获得IV
    1. 就是encryptedKey/saltValue
    2. 如果大于blockSize,则按blockSize截断
    3. 反之,如果小于,则需要用0x36来append补足
  4. 解密encryptedKey/encryptedKeyValue得到中间密钥
    aes = AES.new(key, AES.MODE_CBC, iv)
    secretKey = aes.decrypt(encryptedKeyValue)

三 解密EncryptedPackage Stream

解密的时候,每4096B为一个解密单元,count从0开始的32位无符号整数。

  1. 获得IV H = H(keyData/saltValue + count)
    contentHash = hashlib.sha512()
    contentHash.update(saltValue)
    contentHash.update(struct.pack(“<I”, count))
    iv = contentHash.digest()
  2. 解密当前单元
    aes = AES.new(secretKey, AES.MODE_CBC, iv)
    content = aes.decrypt(encryptedContent)

 

What The C*nt…

其他方面不知道,但UI让我只晃了一眼,就求饶不想再看另一眼的,恐怕也非Windows 2k8 Server Core Edition莫属了。

乍看之下,像是中毒之后,explorer被cmd换掉的产物。

唉,工作了就是惨。见过烂的,没见过这么烂的…

System.Transactions:实现你自己的Resource Manager

System.Transactions:实现你自己的Resource Manager

By Sahil Malik

.net 2.0所带来最大的变化之一也许就是System.Transactions命名空间的引入。在我以前关于SQLCLR的文章中,也简略的提到过这个命名空间。但是谁说事务的概念只能局限于数据库?难道我们在数据库以外的领域就不需要可靠的代码了么?答案是,我们当然需要! 这正是你不能忽视System.Transactions命名空间的原因,也是她将在下一代微软平台引起深刻变革的原因。让我们首先来看看 System.Transactions是如何工作的。

System.Transactions如何工作?

System.Transactions为您提供最简洁明了的方式来实现事务方式的各种操作。你当然可以用各种各样的方式使用她,但也许最典型的一个实现就是如下,将事务操作包裹在TransactionScope(用来构建代码段事务的一个实例变量)里面:

using (TransactionScope ts = new TransactionScope())
{
// 事务代码放这里
ts.Complete();
}

在上面注释的部分里面,你可以使用持有或实现了resource manager(RM)的类。

事务工作于各种被RM管理着的资源上。RM同其他实体(典型的包括:其他进程或者被称为transaction managers(TMs)的服务)协同工作。

RM与TM在事务中协同工作的方式常被称为”两段提交过程”。

下面是典型的“两段提交过程”工作流:

1.RM加入事务;
2.RM做完准备工作后,向TM发送第一阶段完成信号。这个阶段也被称为”准备阶段”;
3.TM在所有RMs成功执行完准备工作后,向所有RMs发送绿灯信号;
4.RMs获得绿灯信号后实际开始提交工作(如果收到红灯信号,则回滚他们自己的工作)。这是第二个阶段–“提交阶段”;
5.否则,事务协调器同所有RMs进行协调以保证他们要么成功,要么一起回滚;

上面提到的整个过程都由System.Transactions框架提供,例如,使用MSDTC(Microsoft分布式事务协调器)作为TMs。为了使用System.Transactions来管理参与事务操作的资源,你必须要么使用现有的RM或者实现你自己的RM。

就目前而论, System.Transactions已可以使用,但实际能应用的RMs却很少,所以很多程序员可能会发现实现自己的RM非常有用。在这之后,可以将这些RMs同未来.net框架中或第三方提供的TM协同工作。另外,同.net 2.0一同发布的一个很常用的RM就是SqlConnection类。本文中会以此类和一个自定义的RM为例,描述它们究竟如何同TM一起工作。但是,如果你确实不得不实现自己的RM,最重要的问题恐怕就是–“RM究竟要做些什么?”

RM究竟要做些什么?

影响实现你自己的RM的一个关键问题就是:这个RM管理的资源本身的特性是持久的还是易变的?

在典型的”两段提交过程”中,持久的资源需要”失败恢复”。一个很好的例子就是事务文件拷贝。如果你不得不实现一个RM来封装拷贝文件的操作到一个事务中,文件可以在第一个阶段被实际拷贝。如果RM失败,恢复策略将保证在回滚过程中,TM有足够的信息来恢复原来的状态。具体来说,可能就是指删除文件或者用以前存在的文件来代替。这种资源的收集操作,使用System.Transactions.Transaction.EnlistDurable方法。

反之,易变资源是不需要”失败恢复”的。同样一个很好的例子,就是持有在内存中的数据。封装这类操作到事务中的RM使用System.Transactions.Transaction.EnlistVolatile方法。

另外一种收集资源的方式是PSPE(可提升的单段式收集)。这种方式由持久的RM拥有事务,当条件改变时,可以逐步升级为由TM管理。因为持久的资源收集比易变的资源收集代价更大,PSPE提供了一种很有吸引力的持久资源收集的实现方式,但是这种方式在许多地方是以牺牲性能为代价的。

在选好资源收集的方式后,接下来你需要实现IEnlistmentNotification接口,以使你从TM获得必要的回调来进行恰当的提交或回滚。 Windows平台的TMs的例子是适合易变资源收集的LTM(轻量级的事务管理器)以及适合分布式或者持久资源收集的MSDTC。

话不多说,下面的例子将向你示范如何实现这些概念。

使用易变资源收集方式实现RM

下面的例子示范了在事务中写成员变量的RM:

public class VolatileRM : IEnlistmentNotification
{
private int memberValue = 0;
private int oldMemberValue = 0;
public int MemberValue
{
get { return memberValue; }
}
public void SetMemberValue(int newMemberValue)
{
Transaction currentTx = Transaction.Current;
if (currentTx != null)
{
Console.WriteLine("VolatileRM: SetMemberValue - EnlistVolatile");
currentTx.EnlistVolatile(this, EnlistmentOptions.None);
}
oldMemberValue = memberValue;
memberValue = newMemberValue;
}
IEnlistmentNotification Members#region IEnlistmentNotification Members
public void Commit(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: Commit");
// Clear out oldMemberValue
oldMemberValue = 0;
}
public void InDoubt(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: InDoubt");
}
public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine("VolatileRM: Prepare");
preparingEnlistment.Prepared();
}
public void Rollback(Enlistment enlistment)
{
Console.WriteLine("VolatileRM: Rollback");
// Restore previous state
memberValue = oldMemberValue;
oldMemberValue = 0;
}
}

正如你所看到的那样,我们在SetMemberValue中首先检查了现在是否在事务中。如果是的话,它如下所示,将自己收集进易变事务中,并且为可能的回滚做一些必要的操作:

public void SetMemberValue(int newMemberValue)
{
Transaction currentTx = Transaction.Current;
if (currentTx != null)
{
Console.WriteLine("VolatileRM: SetMemberValue - EnlistVolatile");
currentTx.EnlistVolatile(this, EnlistmentOptions.None);
}
oldMemberValue = memberValue;
memberValue = newMemberValue;
}

示例中也实现了一系列其他的方法。提交(Commit)、回滚(RollBack),顾名思义,要么提交工作,要么回滚工作。TM调用Prepare方法来通知RM它正请求第一阶段的事务提交工作,并且测试你的RM是否为工作做好了准备。这里,你有下面三种选择:

1.调用preparingEnlistment.Prepare()方法。

这通知TM你已做好准备,并且正等待下一步提交的通知。

2.调用preparingEnlistment.ForceRollBack()方法或者指定preparingEnlistment.RecoveryInformation。

在重新收集资源的时候,这将提供新的RM实例和完成恢复工作所需要的充足的信息。但是现在你还不必为此担心。

3.调用preparingEnlistment.Done()方法。

这将RM作为旁观者。只观察事务,但并不参与其中。通过调用Done方法,TM不会给你第二阶段的通知。

既然你已经建好了RM,你现在能够依赖框架来编写简单可靠的代码。使用上面的RM是相当轻松惬意的一件事情。例如,键入下面的代码:

VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);

上面代码产生下面的输出:

VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3

如你所见,成员值被更新了,并且RM在准备和提交阶段得到了相应的通知。

如果你想通过RM实现强制回滚,你可以如下简单的修改RM的Prepare方法:

public void Prepare(PreparingEnlistment preparingEnlistment)
{
Console.WriteLine("VolatileRM: Prepare");
preparingEnlistment.ForceRollback();
}

这将引起如下异常:

The Transaction Has Aborted.

事实上,你可以通过适当的preparingEnlistment.RollBack重载方法引起自定义的异常。

另外一种引起回滚的方法是简单的删掉TransactionScope.Complete语句或者在同一个事务中收集其他引起回滚的RM。

VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
// txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);

当你运行这段代码的时候,将产生如下输出:

VolatileRM: SetMemberValue - EnlistVolatile
VolatileRM: Rollback
Member Value: 0

你可能觉得这个范例有点不知所云。其实,你通过这个范例可以看到,你能够如此有效的创建与其他RM协同工作的事务代码。例如,你能够将这个非数据库操作的修改成员变量的操作同数据库的事务操作放在同一个事务中。你能够使用如下的T-SQL代码轻松建立数据库:

Create Database Test
Go
Create Table Demo
(
DemoValue varchar(5)
)

然后通过下面的代码将VolatileRM和两个SqlConnection实例收入同一个事务中:

private static string connStr = "Data Source=(local);Initial Catalog=Test;Integrated Security=True";
static void Main(string[] args)
{
VolatileRM vrm = null ;
using (TransactionScope txSc = new TransactionScope())
{
vrm = new VolatileRM();
vrm.SetMemberValue(3);
using (SqlConnection cn = new SqlConnection(connStr))
{
SqlCommand cmd = cn.CreateCommand();
cmd.CommandText = "Insert into Demo(DemoValue) Values ('XXX')";
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
using (SqlConnection cn = new SqlConnection(connStr))
{
SqlCommand cmd = cn.CreateCommand();
cmd.CommandText = "Insert into Demo(DemoValue) Values ('YYY')";
cn.Open();
cmd.ExecuteNonQuery();
cn.Close();
}
Console.WriteLine( "Transaction identifier:" + Transaction.Current.TransactionInformation.DistributedIdentifier);
txSc.Complete();
}
Console.WriteLine("Member Value:" + vrm.MemberValue);
}

框架能够通过PSPE自动识别出你的SqlConnection实例已经参与到事务中。当第二个SqlConnection实例出现时,它将在第二个 cn.Open方法被调用时自动将事务提升至MSDTC。你能够通过”控制面板->管理工具->组件服务”中观察这些操作:

提示: 连接至SQL Server 2005的SqlConnection采用PSPE方式,而连接至SQL Server 2000或更低版本的SqlConnection将采用持久方式,即使在当前事务中仅有一个RM。

当你运行程序时,产生如下输出:

VolatileRM: SetMemberValue - EnlistVolatile
Transaction identifier:c40015f6-5086-4688-b565-c65db1cbc8e7
VolatileRM: Prepare
VolatileRM: Commit
Member Value:3

如你所见,你依然在收集易变资源。但是,如果需要,你的事务将自动提升至MSDTC,并且获得分布式ID。如果你是架构师,这将给予你两个最好的世界:事务集成和可能最好的性能。你获得这些仅仅需要写如下的简单的代码:

Transaction
{
Operation A ;
Operation B ;
....
....
Commit();
}

实现基于System.Transactions的事务

现在你已经具备了基于System.Transactions实现事务处理的基础知识。你学到了各种各样的将资源收集到当前事务中的方法,并且看到了自定义RM收集易变资源是多么的简单。

下载代码

下载相关代码,点击这里

关于作者

Sahil Malik熟悉从Dos至.Net等许多尖端的微软科技。他是Pro ADO.NET 2.0的作者以及Pro ADO.NET with VB.NET 1.1的合著者。Sahil目前也在基于ADO.NET 2.0的多媒体领域有所建树。他曾被授予微软MVP称号。

梦寐以求,太强了,自己看,不爽砍我

1:拼写检查
Spell Check

2:智能探测Filter
Filter

3:色彩
Color

4:无前缀、非前缀智能补写+“结构体”自动补全
Acronym

还不满足,还有功能不爽,看看附件里面的图,再对照自己的比较一下,差距啊:)

厚道的放出链接:

http://www.wholetomato.com/index.html

破解都找到了,我太好了T T

Crack for VA_X_Setup1438.exe

Browser Helper Object

那天有个信息安全实验室的博士生叫我们做IE插件,主要用于拦截IE浏览器的各项消息,从而做出我们自己的处理。

实现方法,当然千奇百怪,全局钩子函数,进程注入,消息拦截等等都可以,不过我最终选择了全局钩子函数,感觉应该好做一点。

由于最近比较多接触C#,第一反映当然还是.net framework。
MSDN->Filter:c#->“全局钩子”一找,郁闷死人:

在 .NET 框架中不支持全局挂钩

您无法在 Microsoft .NET 框架中实现全局挂钩。若要安装全局挂钩,挂钩必须有一个本机动态链接库 (DLL) 导出以便将其本身插入到另一个需要调入一个有效而且一致的函数的进程中。这需要一个 DLL 导出,而 .NET 框架不支持这一点。托管代码没有让函数指针具有统一的值这一概念,因为这些函数是动态构建的代理。
[http://support.microsoft.com/kb/318804]

当然,其实这段文字本身有点问题,先不说网上有人说好像并不是所有的全局挂钩都不能加载,但就.net支持对非受控代码的访问这点,就可以通过在中间增加一层Managed Code->UnManaged Code->Managed Code实现调用。

但是这样的实现难度陡增,很是麻烦。不过在搜索结果中看到一个比较乖的咚咚:Browser Helper Object,看起来感觉比较象。

记得以前有篇写Office Doc Object架构的文章很有名(好像小排发过吧),不过记忆中,当时看了一点就放弃了,感觉挺麻烦的。特别是针对COM组件的调用部分,看着就头大。

话说回来,调用COM组件在.net环境下,还是C++来得方便快捷,毕竟都是非受控的代码,不用处理太多受控、非受控对象之间的交互问题。不过C++.net依然还是VC++6.0的模样,看着都恶心,于是乎放弃:P

粗粗看了看Browser Helper Object(BHO)的机理,这个咚咚主要用于下面的场景:

想实现一个浏览器,提供一些自己的功能,但是如果用IE Control来写的话,还要自己写很多很多很多诸如:前进、后退、历史记录等等IE已经提供的功能,所以更简便的方法是给扩展、修改IE的功能,直接使用IE来实现。这方面,当然不能不提很多人深恶痛绝的3721,他提供了一个个性化的浏览器,却只写了很少的代码,其余的都是IE自带的内容。

打开一个IE,打开Spy++,再结合Internet Explorer 4.0 Architecture的图看看

Internet Explorer 4.0 Architecture

举个例子,如果想要获取IE地址栏的内容:
IEExplorer->IEFrame->WorkerW->ReBarWindow32->ComboBoxEx32->ComboBox->Edit
这就是他的层次结构

IE和Explorer在启动新线程的时候(Explorer要看看选项里面是否选择了在新的线程中打开窗口),会检查注册表项:

[HKLMSOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects]

他下面有很多子项,每一个子项就是一个GUID
例如:
{06849E9F-C8D7-4D59-B87D-784B7D6BE0B3}

这个GUID对应一个BHO组件(其实就是一个COM组件)
这个组件在[HKLMSOFTWAREClassesCLSID]下面进行描述,包括dll路径,签名等等信息

IE或Explorer在每个线程里会依次实例化每个在上述路径注册的BHO对象,从而BHO对象实际上运行在IE或Explorer的地址空间内,并且能够访问IE的几乎所有资源,拦截几乎所有事件,挂载自己的事件处理函数,而不需要跨进程调用和传消息。

看来完全符合最初的应用要求并且是最简单的方法了。

接下来的问题,就只剩下COM组件的使用了。.NET对COM组件的调用提供了很方便的100% pure 受控的访问方式 – Interop, Marshal。查错过程比较艰辛,因为每次重新生成都要重启Explorer.exe T T并且调试的时候,要先开IE,再下断点,再将VS .NET附加到IE进程,再点新建IE窗口…

COM组件的使用:
首先,当然是用Create GUID工具生成我们自己BHO组件的GUID({B29E305D-BC4D-4a80-B522-B0ABC9EBDFFC}),然后添加对 Microsoft Internet Controls COM组件(%systemroot%ShDocVw.dll)的引用。VS.NET会自动Wrapper之为 Interop.SHDocVw.dll,值得注意的是,这里由于最后要使用BHO注册,所以要求这个COM组件必须用私钥签名,从而使用强名机制。具体步骤如下:
1、sn -k BHO.key生成Bho.key文件,也就是我们的私钥。
2、设置项目属性,ActiveX/COM对象的包装程序集->包装程序集密钥文件,指向Bho.key
3、添加COM引用(此时查看相关信息就可以看到强名选项已设为true)
4、设置AssemblyInfo.cs填写key字段,指向Bho.key

然后,就可以声明我们要继承的各项接口:

using SHDocVw;
#region 引入IObjectWithSite接口
[ComImport(), Guid("fc4801a3-2ba9-11cf-a229-00aa003d7352")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IObjectWithSite
{
void SetSite([In ,MarshalAs(UnmanagedType.IUnknown)] object site);
void GetSite(ref Guid guid, [MarshalAs(UnmanagedType.IUnknown)] out object site);
}
#endregion
#region 定义默认BHO COM接口
[GuidAttribute("181C179B-7CC9-4457-8C1D-4B45E7C8589D")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsDual)]
public interface IObserver
{
}
#endregion

然后定义我们自己的BHO类:

/// <summary>
///定义BHO类,此类由浏览器实例化
/// </summary>
[ClassInterfaceAttribute(ClassInterfaceType.None)]
[GuidAttribute("B29E305D-BC4D-4a80-B522-B0ABC9EBDFFC")]//由CreateGUID程序生成
[ProgIdAttribute("Observer.BrowserMonitor")]
public class BrowserMonitor : IObserver, IObjectWithSite
{}

这个类里面如下引入定义:

protected IWebBrowser2 browser; //浏览器对象
protected DWebBrowserEvents2_Event browserEvents;//浏览器事件

并且实现IObjectWithSite接口:

#region IObjectWithSite 成员
/// <summary>
/// IE调用此方法,并传递指向容器Site的IUnknown指针,由此我们可以获得IWebBrowser2接口
/// 并挂载DWebBrowserEvents2事件
/// </summary>
/// <param name="site">容器Site的IUnknown指针</param>
public void SetSite(object site)
{
// TODO: 添加 BrowserMonitor.SetSite 实现
#region 取得 IWebBrowser2 引用
if (browser != null)
Release();
if (site == null)
return;
browser = site as IWebBrowser2;
#endregion
#region 检查名称,当且仅当为IEXPLORE.EXE时加载
string hostName = browser.FullName;
if (!(hostName.ToUpper().EndsWith("IEXPLORE.EXE")))
{
Release();
return;
}
#endregion
#region 挂载浏览器事件
browserEvents = browser as DWebBrowserEvents2_Event ;
if (browserEvents != null)
{
browserEvents.DocumentComplete += new DWebBrowserEvents2_DocumentCompleteEventHandler(this.OnDocumentComplete);
}
else
{
Release();
return;
}
#endregion
}
/// <summary>
/// 调用者调用此方法以获得前面浏览器发送给SetSite()方法的浏览器对象
/// </summary>
/// <param name="guid">请求Site接口对象的GUID</param>
/// <param name="site">返回的Site接口对象</param>
public void GetSite(ref System.Guid guid, out object site)
{
// TODO: 添加 BrowserMonitor.GetSite 实现
site = null;
if (browser != null)
{
IntPtr pSite = IntPtr.Zero;
IntPtr pUnk = Marshal.GetIUnknownForObject(browser); //引用计数增加
Marshal.QueryInterface(pUnk, ref guid, out pSite); //引用计数增加
Marshal.Release(pUnk); //引用计数减少
Marshal.Release(pUnk); //引用计数减少
if (!pSite.Equals(IntPtr.Zero))
{
site = pSite;
}
else
{
// 若找不到请求的接口,将返回E_NOINTERFACE
Release();
Marshal.ThrowExceptionForHR(E_NOINTERFACE);
}
}
else
{
// 若找不到请求的接口对象,将返回E_FAIL
Release();
Marshal.ThrowExceptionForHR(E_FAIL);
}
}
#endregion

这里挂载了我们自己的事件处理函数,处理DocumentComplete事件,此外还有很多事件可以使用,例如:

DownloadBegin
DownloadComplete
BeforeNavigate2
CommandStateChange
FileDownload
NavigateComplete
NewWindow
FullScreen
MenuBar
Quit
FrameBeforeNavigate
FrameNavigateComplete
FrameNewWindow
ProgressChange
PropertyChange
StatusTextChange
TitleChange
WindowActivate
WindowMove
WindowResize

using System.IO;
#region 用于挂载的自定义函数
protected void OnDocumentComplete(object display, ref object url)
{
try
{
if (Marshal.Equals(browser, display))
{
StreamWriter sw = new StreamWriter(@"C:Test.txt", true);
sw.WriteLine(url.ToString());
sw.Close();
}
}
catch
{
Release();
Marshal.ThrowExceptionForHR(E_FAIL);
}
}
#endregion

此外,关于错误处理,由于涉及包裹为受控组件的非受控组件,所以需要使用以下方式抛出异常:

#region 定义HRESULT值 : 预定义的COMException值
//在未检查的上下文中,如果表达式产生目标类型范围之外的值,则结果被截断
const int E_FAIL = unchecked((int)0x80004005);//失败
const int E_NOINTERFACE = unchecked((int)0x80004002);//QueryInterface时,接口不存在
#endregion
Marshal.ThrowExceptionForHR(E_FAIL);
Marshal.ThrowExceptionForHR(E_NOINTERFACE);

使用一下方式,释放对象:

#region Release操作
protected void Release()
{
if (browserEvents != null)
{
Marshal.ReleaseComObject(browserEvents);
browserEvents = null;
}
if (browser != null)
{
Marshal.ReleaseComObject(browser);
browser = null;
}
}
#endregion

至此已基本完成,但是要想自动注册BHO组件,还需要做几件事情:

#region 挂载注册/注销操作
/// <summary>
/// 在注册COM组件时,由运行时调用
/// </summary>
[ComRegisterFunctionAttribute]
public static void Register(Type type)
{
// 注册BHO组件
string guid = type.GUID.ToString("B");
RegistryKey rkey =
Registry.LocalMachine.CreateSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects");
RegistryKey rkeyBHO = rkey.CreateSubKey(guid);
}
/// <summary>
/// 在注销COM组件时,由运行时调用
/// </summary>
[ComUnregisterFunctionAttribute]
public static void Unregister(Type type)
{
// 注销BHO组件
string guid = type.GUID.ToString("B");
RegistryKey rkey =
Registry.LocalMachine.CreateSubKey(@"SOFTWAREMicrosoftWindowsCurrentVersionExplorerBrowser Helper Objects");
rkey.DeleteSubKey(guid,false);
}
#endregion

由这些可见.NET平台对于COM组件的调用支持还是相当强大的,可以在受控环境下实现100% pure的COM访问能力。

PS:
结尾怎么写成这样了,写一写的就有点偏题了- –

BHO Project

Cached FlyWeight

其实因为C#语言自身的一些特点,和FlyWeight的实现方式不太一样- –

初学,不足之处见谅:)

详细叙述见附件

======2005-12-13======

作了一些修改,使用方式不变,不过加入了对任何单参数构造函数的支持。

有人问,为什么不支持任何多参数构造函数, 因为实际上参数就是缓存时用于查找的Key,多参数生成Key的时候较麻烦而已:)

最后,说明一点,本FlyWeight在Scavenging的时候,会根据使用的情况,删除最不常使用的Key

======2006-1-19=======

VS2005发布后,更新版本

Download Fly Weight

C#中调用Windows API的要点

在.Net Framework SDK文档中,关于调用Windows API的指示比较零散,并且其中稍全面一点的是针对Visual Basic .net讲述的。本文将C#中调用API的要点汇集如下,希望给未在C#中使用过API的朋友一点帮助。另外如果安装了Visual Studio .net的话,在C:Program FilesMicrosoft Visual Studio .NETFrameworkSDKSamplesTechnologiesInteropPlatformInvokeWinAPIsCS 目录下有大量的调用API的例子。

一、调用格式
using System.Runtime.InteropServices; //引用此名称空间,简化后面的代码
...
//使用DllImportAttribute特性来引入api函数,注意声明的是空方法,即方法体为空。
[DllImport("user32.dll")]
public static extern ReturnType FunctionName(type arg1,type arg2,...);
//调用时与调用其他方法并无区别,可以使用字段进一步说明特性,用逗号隔开,如:
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )]

DllImportAttribute特性的公共字段如下:

1、CallingConvention 指示向非托管实现传递方法参数时所用的CallingConvention 值。
CallingConvention.Cdecl : 调用方清理堆栈。它使您能够调用具有 varargs 的函数。
CallingConvention.StdCall : 被调用方清理堆栈。它是从托管代码调用非托管函数的默认约定。

2、CharSet 控制调用函数的名称版本及指示如何向方法封送 String 参数。
此字段被设置为 CharSet 值之一。如果 CharSet 字段设置为 Unicode,则所有字符串参数在传递到非托管实现之前都转换成 Unicode 字符。这还导致向 DLL EntryPoint 的名称中追加字母“W”。如果此字段设置为 Ansi,则字符串将转换成 ANSI 字符串,同时向 DLL EntryPoint 的名称中追加字母“A”。大多数 Win32 API 使用这种追加“W”或“A”的约定。如果 CharSet 设置为 Auto,则这种转换就是与平台有关的(在 Windows NT 上为 Unicode,在 Windows 98 上为 Ansi)。CharSet 的默认值为 Ansi。CharSet 字段也用于确定将从指定的 DLL 导入哪个版本的函数。CharSet.Ansi 和 CharSet.Unicode 的名称匹配规则大不相同。
对于 Ansi 来说,如果将 EntryPoint 设置为“MyMethod”且它存在的话,则返回“MyMethod”。如果 DLL 中没有“MyMethod”,但存在“MyMethodA”,则返回“MyMethodA”。对于 Unicode 来说则正好相反。如果将 EntryPoint 设置为“MyMethod”且它存在的话,则返回“MyMethodW”。如果 DLL 中不存在“MyMethodW”,但存在“MyMethod”,则返回“MyMethod”。如果使用的是 Auto,则匹配规则与平台有关(在 Windows NT 上为 Unicode,在 Windows 98 上为 Ansi)。如果 ExactSpelling 设置为 true,则只有当 DLL 中存在“MyMethod”时才返回“MyMethod”。

3、EntryPoint 指示要调用的 DLL 入口点的名称或序号。
如果你的方法名不想与api函数同名的话,一定要指定此参数,例如:
[DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")]
public static extern int MsgBox(IntPtr hWnd,string txt,string caption, int type);

4、ExactSpelling 指示是否应修改非托管 DLL 中的入口点的名称,以与 CharSet 字段中指定的 CharSet 值相对应。
如果为 true,则当 DllImportAttribute.CharSet 字段设置为 CharSet 的 Ansi 值时,向方法名称中追加字母 A,当 DllImportAttribute.CharSet 字段设置为 CharSet 的 Unicode 值时,向方法的名称中追加字母 W。此字段的默认值是 false。

5、PreserveSig 指示托管方法签名不应转换成返回 HRESULT、并且可能有一个对应于返回值的附加 [out, retval] 参数的非托管签名。

6、SetLastError 指示被调用方在从属性化方法返回之前将调用 Win32 API SetLastError。
true 指示调用方将调用 SetLastError,默认为 false。运行时封送拆收器将调用 GetLastError 并缓存返回的值,以防其被其他 API 调用重写。用户可通过调用 GetLastWin32Error 来检索错误代码。

二、参数类型:
1、数值型直接用对应的就可。(DWORD -> int , WORD -> Int16)

2、API中字符串指针类型 -> .net中string

3、API中句柄 (dWord) -> .net中IntPtr

4、API中结构 -> .net中结构或者类。
注意这种情况下,要先用StructLayout特性限定声明结构或类
公共语言运行库利用StructLayoutAttribute控制类或结构的数据字段在托管内存中的物理布局,即类或结构需要按某种方式排列。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。它的构造函数中用LayoutKind值初始化 StructLayoutAttribute 类的新实例。 LayoutKind.Sequential 用于强制将成员按其出现的顺序进行顺序布局。
LayoutKind.Explicit 用于控制每个数据成员的精确位置。利用 Explicit,每个成员必须使用 FieldOffsetAttribute 指示此字段在类型中的位置。如:
[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)]
public class MySystemTime
{
[FieldOffset(0)]public ushort wYear;
[FieldOffset(2)]public ushort wMonth;
[FieldOffset(4)]public ushort wDayOfWeek;
[FieldOffset(6)]public ushort wDay;
[FieldOffset(8)]public ushort wHour;
[FieldOffset(10)]public ushort wMinute;
[FieldOffset(12)]public ushort wSecond;
[FieldOffset(14)]public ushort wMilliseconds;
}

下面是针对API中OSVERSIONINFO结构,在.net中定义对应类或结构的例子:
/**********************************************
* API中定义原结构声明
* OSVERSIONINFOA STRUCT
* dwOSVersionInfoSize DWORD ?
* dwMajorVersion DWORD ?
* dwMinorVersion DWORD ?
* dwBuildNumber DWORD ?
* dwPlatformId DWORD ?
* szCSDVersion BYTE 128 dup (?)
* OSVERSIONINFOA ENDS
*
* OSVERSIONINFO equ
*********************************************/
//.net中声明为类
[ StructLayout( LayoutKind.Sequential )]
public class OSVersionInfo
{
public int OSVersionInfoSize;
public int majorVersion;
public int minorVersion;
public int buildNumber;
public int platformId;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )]
public String versionString;
}
//或者
//.net中声明为结构
[ StructLayout( LayoutKind.Sequential )]
public struct OSVersionInfo2
{
public int OSVersionInfoSize;
public int majorVersion;
public int minorVersion;
public int buildNumber;
public int platformId;
[MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )]
public String versionString;
}

此例中用到MashalAs特性,它用于描述字段、方法或参数的封送处理格式。用它作为参数前缀并指定目标需要的数据类型。例如,以下代码将两个参数作为数据类型长指针封送给 Windows API 函数的字符串 (LPStr):
[MarshalAs(UnmanagedType.LPStr)]
String existingfile;
[MarshalAs(UnmanagedType.LPStr)]
String newfile;

注意结构作为参数时候,一般前面要加上ref修饰符,否则会出现错误:对象的引用没有指定对象的实例。
[DllImport("kernel32", EntryPoint="GetVersionEx")]
public static extern bool GetVersionEx2(ref OSVersionInfo2 osvi);

三、如何保证使用托管对象的平台调用成功?
如果在调用平台 invoke 后的任何位置都未引用托管对象,则垃圾回收器可能将完成该托管对象。这将释放资源并使句柄无效,从而导致平台invoke 调用失败。用 HandleRef 包装句柄可保证在平台 invoke 调用完成前,不对托管对象进行垃圾回收。
例如下面:
FileStream fs = new FileStream( "a.txt", FileMode.Open );
StringBuilder buffer = new StringBuilder( 5 );
int read = 0;
ReadFile(fs.Handle, buffer, 5, out read, 0 ); //调用Win API中的ReadFile函数

由于fs是托管对象,所以有可能在平台调用还未完成时候被垃圾回收站回收。将文件流的句柄用HandleRef包装后,就能避免被垃圾站回收:
[DllImport( "Kernel32.dll" )]
public static extern bool ReadFile(
HandleRef hndRef,
StringBuilder buffer,
int numberOfBytesToRead,
out int numberOfBytesRead,
ref Overlapped flag );
......
......
FileStream fs = new FileStream( "HandleRef.txt", FileMode.Open );
HandleRef hr = new HandleRef( fs, fs.Handle );
StringBuilder buffer = new StringBuilder( 5 );
int read = 0;
// platform invoke will hold reference to HandleRef until call ends
ReadFile( hr, buffer, 5, out read, 0 );