.Net Programming

Big Problem : Access Denied when Using Qwinsta.exe or WTSOpenServer API

This article describes the solution for the “access denied” error occurs when using qwinsta.exe/rwinsta.exe or WTSOpenServer Windows API function to access to a Windows XP SP2 PC.

The same thing happens when you are trying to connect to a Windows XP SP2 using Terminal Services Manager from a Windows 2003 Server. 

- Because essentially qwinsta.exe command calls WTS functions (WTSOpenServer, WTSEnumerateSessions, …), you will encounter the same error when using either the command or the API.

As a quick answer, check the following points.
1. Ensure you have the correct credential
Although you do not need to provide a username and a password when executing qwinsta.exe or WTSOpenServer, Windows use its stored credentials automatically, just like accessing a shared folder.

The easiest way to confirm the credential is to use Explorer to access remote server’s C$ share. Also, you can use “net use” command.

For those who want to connect to a remote server programmatically using WTSOpenServer API, WNetAddConnection2 function should be enough to make sure you got a piece of credential before calling WTSOpenServer function.

Of course the user in the credential should be a use on the remote server who has enough privilege to carry out the operation.

2. Open RPC ports on firewall
An API call involves connecting to a remote machine use RPC. In most cases the RPC service is running and you can confirm that from the service management interface. To open RPC ports, simply enable “File and Printer Sharing” in the Windows Firewall setting.

(Generally, if the RPC is blocked by the firewall on remote machine, the error should be "1722 RPC server is unavailable" rather then "5 Access denied")

3. Disable "Force Guest" log on

You can change this option from either local security policy or registery.

Local security policy : run secpol.msc > Secuirty Settings > Local Policies > Security Options > Network access : Sharing and security model for local accounts, and set to "Classic".

Registry : find "forceguest" item in HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa, and set it to 0.

The two ways are equivalent. If this option is set to 1, remote logged in user will be impersonated as Guest account (and if Guest account is disabled, login fails). Set it to 0 enables user logging in as themselves.

4. If you still receive “access denied” and it only happens when the remote OS is Windows XP SP2, it is very probably the following case.

To confirm, you need a packet sniffer such as Wireshark (a great freeware).
When you use a sniffer to capture the SMB packets, you can see the authentication is OK (NTLM if workstation, Kerberos if Windows 2003 domain), but the RPC get nca_s_fault_access_denied (0×00000005) as error code.

This means the remote RPC component failed to execute the requested operation. It is because in Windows XP SP2, it is not allowed to operate on terminal service (Remote Desktop) through RPC by default. To modify this setting to enable Remote Desktop API through RPC, you need to find the following registry key:

HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server

Then add a DWORD value named “AllowRemoteRPC” and change its value to 1.

I googled it and it seems there is no official document on it. (I don’t know why)

Access denied error often misleads us into thinking it must be because of the insufficient privilege, or because that the qwinsta.exe command / WTSOpenServer failed to utilized the credential. It wasted me about 5 hours in researching how to attach a credential to a RPC call, and I even got caught deeply into the complicated SSPI/LSA authentication architecture.

XBAP and Silverlight : Product Lining in .NET 3.0/WPF

今天在@IT网站看到一篇关于Expression Blend的文章,这篇文章开头部分对于.NET 3.0/WPF时代微软产品结构、地位作了简单介绍,觉得概括的很精妙,这里就加以借题发挥,做一个WPF入门。

什么是WPF?

要是到微软的网站查找答案,肯定能得到一大堆华丽却让人糊涂的辞藻。WPF (Windows Presentation Foundation)是.NET 3.0的一部分,是一种新的用户界面技术,用术语说是一种新的“图形子系统 (Graphic Subsystem)”。换句话说,在.NET 2.0当中我们可以开发两种应用程序:

  • Windows Forms Application
  • ASP.NET Web Application

那么在.NET 3.0当中,可以开发的应用程序又多了一种:

  • WPF Application

而WPF Application则又可以继续分成下面两种:

  • Windows下的应用程序:何传统的桌面应用程序、Windows Forms应用程序一样,只不过用户界面部分采用了WPF技术 (XAML等)编写的应用程序。这种程序由于可以使用WPF的全部功能,因此可以将WPF丰富绚丽的用户界面表现能力发挥到极致。
  • XAML Browser Application (XBAP),其特点是:
    • 在Internet Explorer中运行
    • 无需安装
    • 像ClickOnce一样,在部分信赖的域中执行

那么最近沸沸扬扬的Silverlight又是什么呢?Silverlight的前身是很早以前就万人期待的WPF/E,其中E的意义是Everywhere。众所周知,要想使用WPF,就必须安装.NET 3.0;而.NET 3.0虽然宣称跨平台,但目前还仅限于Windows XP以上版本。因此,WPF/E最初的目标,就是把WPF/XAML的丰富的用户界面表现能力带到各个平台。

带着这个目标,WPF/E经历了数十个月的秘密开发之后,作为Silverlight诞生了——其诞生之日起,就被人们命名为Flash杀手——因为它和Adobe的Flash太相似了。很多.NET程序员都乐了——以后可以用C#开发Flash了!从这一点,想必Silverlight的特点已经很清晰了:客户端是浏览器,在下载一个插件之后即可享受Silverlight带来的激动人心的用户体验了。

最后,用一张图表来总结各种产品的关系、地位:

.NET 3.0 Products/Technologies

Write Outlook add-ins with C#

针对Microsoft Office系列的编程一直是比较麻烦的领域;这主要是因为Office里面大量的COM对象构成的庞大的体系结构让人望而生畏。

编写Outlook插件也是如此。虽然.Net的到来使得深入系统底层的RAD开发成为了可能,但是由于Outlook仍然是COM接口,要写Outlook插件,从根本上来说仍旧是编写Outlook COM插件的过程。凭借.Net和COM的互操作能力,才使得C#/VB.Net等语言编写的插件能够被Outlook等Office程序调用。

首先,无论是Outlook也好,Word也好,所有的Office应用程序都使用了共同的COM接口_IDTExtensibility2来与插件进行通信。因此,要开发一个Outlook插件(当然其他Office插件也一样),必须实现这个接口。这个接口定义在AddInDesigner Object Library当中,位于<drive>\Program Files\Common Files\DESIGNER\MSADDNDR.DLL文件中。在Visual Studio.Net当中,对这个COM接口进行了包装,包装后的Assembly名为Extensibility,该接口名为IDTExtensibility2。

下面就通过一个演示项目来说明一下。

在Outlook当中有一个BUG/设计缺陷,就是当回复、转发一封邮件的时候,这封回复/转发邮件的文字编码会自动变得和收到的邮件相同。例如,我经常从美国收到编码为US-ASCII的邮件,当我用中文添加一些评注,转发给中国人的朋友时,如果我忘记了手动把编码改成UTF-8或GB2312,邮件内容就会变成乱码。即便我在“新邮件选项”里面把新邮件的默认编码设置为UTF-8也没有用。

这种时候,如果有一个Outlook插件能够自动将所发送邮件的编码改成UTF-8有多好啊。于是,就有了下面的制作过程。

Step 1 创建Outlook插件项目

首先,在Visual Studio 2005(其他版本也可。这里以2005为例说明)中,新建一个Project,类型为Other Project Types > Extensibility > Shared Add-in,并在Application Host画面选择要使用这个Add-in的Office程序。这样一个Add-in Project就生成了。在这个Project当中,我们可以看到如下两个平时不常见的Reference被添加到了项目当中:

  • Extensibility:这就是所有Office插件都要实现的接口所在的Assembly
  • Microsoft.Office.Core:Office插件的共通组件库

Step 2 实现IDTExtensibility2接口

打开Connect.cs文件,我们就可以看到VS已经帮我们创建好了一个叫做Connect的类,来实现IDTExtensibility2接口。这个接口和一般的Host/Add-in体系结构类似,内容如下:

  • void OnConnection(object Application, ext_ConnectMode ConnectMode, object AddInInst, ref Array custom);
    当COM add-in被连接到host应用程序时OnConnection事件将被触发。

    • Application - 指向加载这个add-in的host应用程序,如Outlook。
    • ConnectMode - 指定了add-in的加载模式。
    • AddInInst - 指向一个代表当前add-in的COMAddIn对象(定义在Microsoft.Office.Core当中)。
    • Custom - 用户自定义参数。
  • void OnDisconnection(ext_DisconnectMode RemoveMode, ref Array custom);
    与上一个相反,这个是把add-in从host程序中卸载时引发的事件。RemoveMode指定了卸载模式。
  • void OnAddInsUpdate(ref Array custom);
    当COM add-in被安装或移除的时候引发该事件。
  • void OnStartupComplete(ref Array custom);
  • void OnBeginShutdown(ref Array custom);
    上面两个事件在host程序完全结束加载、以及即将开始关闭的时候引发。

简单实现上面几个接口的例子可以参考微软知识库的302901号文章

Step 3 截获ItemSend事件 更改MailItem属性

实现了上面的接口,我们就要在其中添加代码了。由于我们的目的是在发送邮件时强制改变邮件的编码(encoding),因此只需要在OnConnection当中添加截获发送邮件的事件即可:

public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode, object addInInst, ref System.Array custom)
{
    outlookApplication = application as Outlook.Application;
    addInInstance = addInInst as COMAddIn;
    if (outlookApplication == null || addInInstance == null)
    { return; }
    outlookApplication.ItemSend += new Outlook.ApplicationEvents_11_ItemSendEventHandler(outlookApplication_ItemSend);
    if (connectMode != Extensibility.ext_ConnectMode.ext_cm_Startup)
    {
        OnStartupComplete(ref custom);
    }
}

之后,我们就可以在事件函数中控制邮件的编码了:

void outlookApplication_ItemSend(object Item, ref bool Cancel)
{
    Outlook.MailItem mitem = (Outlook.MailItem)Item;
    mitem.InternetCodepage = 65001; //Code page for UTF-8
}

在完成上面的代码之后,我们只需要打开Outlook,就可以测试这个插件的效果了。要确认该插件是否安装,可以在Outlook的菜单上选择Tools > Options > Other,然后按Advances Options按钮,COM Add-ins即可看到插件的列表,以及启用和禁用插件。

How to get the path of temporary folder in .Net

有时候在编程当中我们需要获取临时目录的路径。临时目录在DOS/Win9x时代一直是C:\Temp之类形式,以环境变量来指定;到了多用户的WinXP,虽然默认路径变成了C:\Documents and Settings\Username\Local Settings\Temp,但是仍然可以通过TMP或者TEMP环境变量来指定。因此我们首先可以通过直接读取环境变量的方式找到临时目录:

Environment.GetEnvironmentVariable("TMP");

或者,

Environment.GetEnvironmentVariable("TEMP");

这时就有一个问题随之而来,万一TMP和TEMP两个环境变量的值不同时,应该以哪个为准呢?此外,万一两个变量都不存在时,应该以哪个目录为临时目录呢? 看来,这个方法不够“标准”。

实际上,Win32中关于文件系统的API里,已经为我们提供了这样一个API:

DWORD GetTempPath(
  DWORD nBufferLength,
  LPTSTR lpBuffer
);

GetTempPath函数会按照如下的顺序确定临时目录:

1. The path specified by the TMP environment variable.
2. The path specified by the TEMP environment variable.
3. The path specified by the USERPROFILE environment variable.
4. The Windows directory.

而在.Net里面,对于这个API已经有了封装,这就是Path.GetTempPath()方法。因此在.Net当中获取临时目录时,只需用如下语句:

string tempFolder = Path.GetTempPath();

 

给.Net程序员的PInvoke Tips [2]: Are Strings Immutable?

早在Java到来之际,程序员们都已逐渐接受并乐于接受String的这一特性:immutable。从C/C++转到.Net/C#的程序员们,在最初可能非常不适应把char[]和string分开对待,但是一旦习惯了就会觉得非常方便,尤其是直接以+进行连接,以及支持switch…case等。

这个时候问题来了,string真的是immutable的吗?

cbrumme的blog上给出了一个例子:

using System;
using System.Runtime.InteropServices;
 
public class Class1
{
    static void Main(string[] args)
    {
        String computerName = "strings are always immutable";
        String otherString = "strings are always immutable";
 
        int len = computerName.Length;
        GetComputerName(computerName, ref len);
 
        Console.WriteLine(otherString);
    }
 
    [DllImport("kernel32", CharSet=CharSet.Unicode)]
    static extern bool GetComputerName(
        [MarshalAs (UnmanagedType.LPWStr)] string name,
        ref int len);
}

该程序的执行结果也许正在你的预料之中,输出的是类似MYCOMPUTERNAMElways immutable之类的字符串,也就是说原字符串的前面一部分被计算机名覆盖掉了。

对上面的程序,我们可以做出如下分析:
1,computerName和otherString 的文本相同,因此由于编译器的Interning的结果,二者其实指向同一个字符串,用Object.ReferenceEquals()可以验证其相等。
2,红色部分标出的Marshal指令,使得该string被marshal为一个unmanaged pointer(LPWSTR)传递给了GetComputerName函数;
3,GetComputerName函数直接改写了computerName指向的缓冲区,string的immutable特性即被破坏。

由此我们可以看到,在与Unamanaged代码进行交互操作时必须额外小心,因为从某种意义上来说Unmanaged代码权限更大,破坏力也就更大,也就更容易引起意想不到的问题。

那么,上面那段使用GetComputerName的代码中,对该函数的包装要如何改进呢?

首先,在使用一个API之前应该注意其各个参数的in, out性质,例如关于GetComputerName,MSDN上有如下一段:

BOOL GetComputerName( LPTSTR lpBuffer, LPDWORD lpnSize);
lpBuffer : [out] Pointer to a buffer that receives a null-terminated string containing the computer name or the cluster virtual server name. The buffer size should be large enough to contain MAX_COMPUTERNAME_LENGTH + 1 characters.

很显然,lpBuffer应该是用来输出的缓冲区,因此不应该用string,而是用byte[],StringBuilder之类的类型与之对应;即便一定要用String,也绝对不能Marshal为LPWSTR/LPTSTR,而是Marshal为VBByRefStr,以确保Managed代码侧string的immutable性质。

此外,使用.Net中unsafe代码也可以打破String的immutable,这和使用Unmanaged代码的本质是相同的。

Throw an Exception Which is NOT an Exception

在C#中,用什么语句可以捕获所有的异常?——对于这个问题,很多人可能很习惯地用写出下面的程序:

try {
    //Some code
}
catch (System.Exception ex) {
    System.Console.Write("Error!");
}

这条语句的捕获对象是“System.Exception”。由于.Net中所有的异常必须直接或间接继承自这个类,因此理论上讲似乎是的确可以捕获所有异常的。但是仔细想想,真的没有办法抛出一个不是继承自System.Exception的对象吗?如果答案是可以的,那么显然这种异常就无法由上面的语句捕获。

抛出不是异常的异常……这种想起来都觉得不可能的事情,可能吗?事实上,关于这个问题.Net 1.x 和 2.0有一些细微差别,下面分开考虑。

■在.Net 1.x当中,抛出不继承自System.Exception的对象是可能的

例如,在C++当中,我们可以用throw "Error!"这样的语句,将一个字符串作为“异常”抛出;在IL当中,我们甚至可以用下面的方式,将任何对象作为异常抛出:

.assembly ThrowerLib { } .class public Thrower {
    .method static public void Start( ) {
        ldstr "Oops"
        throw
        ret
    }
}

因此,在.Net 1.x当中,经常使用下面这种最保险的方式:

try
{
    //Some code
}
catch(System.Exception ex)
{
    System.Console.WriteLine("System.Exception error: " + ex.Message);
}
catch
{
    System.Console.WriteLine("Non System.Exception based error.);
}

其中第二个catch的效果就是,拦截“不是Exception及其子类的异常”。

■在.Net 2.0中,默认所有异常都必须继承自System.Exception;亦可和1.x相同方式运作

在.Net2.0当中,为了确保跨语言的兼容性(在C#下无法throw一个字符串,在C++下则可以),CLR会自动将不是继承自System.Exception的异常包裹在RuntimeWrappedException对象中——例如,在C++中throw一个字符串,该字符串将被包裹起来,实际上抛出的则是一个RuntimeWrappedException。这样的结果就是,所有语言抛出的异常将都是System.Exception的子类了。

但是同时,为了保证和1.x版本的兼容性,.Net 2.0提供了RuntimeCompatibilityAttribute类,指定CLR不要对异常进行包装:

[assembly:System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows = false)]

 

 附:测试用程序

  1. [assembly: System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows = false)]
  2.  
  3. namespace ThrowerExample
  4. {
  5.     class ThrowerHarness
  6.     {
  7.         static void Main(string[] args)
  8.         {
  9.             try
  10.             {
  11.                 Thrower.ThrowException();
  12.             }
  13.             catch (System.Exception ex)
  14.             {
  15.                 System.Console.WriteLine("System.Exception error: " + ex.Message);
  16.             }
  17.             catch
  18.             {
  19.                 System.Console.WriteLine("Non System.Exception based error.");
  20.             }
  21.  
  22.             try
  23.             {
  24.                 Thrower.ThrowString();
  25.             }
  26.             catch (System.Exception ex)
  27.             {
  28.                 System.Console.WriteLine("System.Exception error: " + ex.Message);
  29.             }
  30.             catch
  31.             {
  32.                 System.Console.WriteLine("Non System.Exception based error.");
  33.             }
  34.         }
  35.     }
  36. }

执行结果是,第一个异常将被catch(System.Exception ex){}捕获;第二个异常由于不是System.Exception的子类,将落到catch{}中。

但是如果把第一行的属性去掉,编译时将出现下面的警告:

warning CS1058: A previous catch clause already catches all exceptions. All non-exceptions thrown will be wrapped in a System.Runtime.CompilerServices.RuntimeWrappedException

执行的话,两个异常都将被catch(System.Exception ex){}捕获,看消息可知第二个异常的类型为RuntimeWrappedException。

给.Net程序员的PInvoke Tips [1]: String is Sometimes an Integer

习惯了.Net编程,尤其是以前几乎没有用过Basic,Pascal,C/C++等“较古老”的语言的程序员,遇到PInvoke,尤其是COM interoperation的时候,往往是一头雾水不得要领。相信在在将来,一方面是从C#,.Net以及Java开始学习编程的人会越来越多,一方面整个Windows也逐渐往Managed平台迁移,懂得如何直接同Win32API打交道的程序员就会越来越少(当然绝对数量肯定还是很多的,至少比用DDK写驱动程序的多吧……)

但是,对于.Net程序员来说,虽然不用直接使用C++调用API,但是PInvoke的知识还是必不可少的,对于Handle,Unmanaged Thread,COM等等最基本的东西仍然是必修课。

前一段写程序时遇到这么一个PInvoke的实际例子:

某个C#写的WindowsForm中,要对资源DLL进行读取(Resource DLL,Win32格式的只含有资源的DLL)。其中要用到这么一个API函数:

BOOL EnumResourceTypes(
    HMODULE hModule,
    ENUMRESTYPEPROC lpEnumFunc,
    LONG_PTR lParam
);

其中,ENUMRESTYPEPROC,其定义为:

BOOL CALLBACK EnumResTypeProc(
    HMODULE hModule,
    LPTSTR lpszType,
    LONG_PTR lParam
);

其中的LPTSTR,在C#里应该怎么写呢?我当时想也没想,既然是LPTSTR (Long Pointer to String),那肯定就是字符串了。于是就有了:

public delegate bool EnumResTypeProc(IntPtr hModule, string strType, IntPtr param);

结果,这段代码在执行的时候出错了——用Marshal.GetLastWin32Error()查看,发现是ERROR_NOACCESS(998)。说明,程序访问了禁止访问的地址空间。经测试,其他几个参数:hModule是正确无误的DLL库的Handle,最后的参数也被设置为NULL,那么原因只能是这个strType了。

再回过头来仔细想想……该参数的类型是LPTSTR,在Unicode环境下也即LPWSTR,也即WCHAR * ——没错啊,不就是字符串么?

但是我们仔细看一下MSDN的说明,就会发现,Windows提供了一些标准的资源类型;例如RT_ICON,RT_STRING,RT_VERSION,……仔细看这些类型的定义就会发现它们都是数字。

查阅了相关资料才明白,lpszType这个参数,在值小于某个数值(例如256)的情况下应该被当作数字来处理;而在大于这个数值时,则作为指针,其值是内存中的地址值,也即应该将其当作指针处理。这种情况下,显然就不能用.Net的String类型来一概而论。正确的做法是,使用IntPtr,如果判断其值不是在256以下,再将其Marshal为指向字符串的指针,获取字符串。

结论:在.Net中,数据类型是“强类型”的,不同类型之间无法被强制转换,因此保证了类型安全;但是在C/C++等语言为基础的Win32中,类型却是可以任意变换的,各种Object“一切皆指针”,你可以把一个int* 当作char*,可以把HGDI当作HMODULE……没有人来保证这种变换的安全性,但是也正因此带来了灵活性。Windows的各种数据类型当中,很多类型都是名称不同,但是实际上的数据结构是相同的(例如关于Handle,就有HWND,HMENU,HINSTANCE,HHOOK等等许多种)。相反,同一个数据类型,时而代表数值,时而代表指针的例子也不少见。因此在处理Win32API的时候,必须特别注意这些特点。

Simulate Mouse Movement and Click Programmatically

有时,我们需在我们的程序中模拟鼠标的移动、点击等动作。——比如,一个再现用户操作的宏,或者一个演示操作方法的Demo程序。那么,我们在.Net中如何实现呢?

.Net并没有提供改变鼠标指针位置、模拟点击操作的函数;但是Windows API提供了。其中一个是SetCursorPos,在.Net里调用形式为:

[DllImport("user32.dll")]
static extern bool SetCursorPos(int X, int Y);

该函数可以改变鼠标指针的位置。其中X,Y是相对于屏幕左上角的绝对位置。另一个函数是mouse_event:

[DllImport("user32.dll")]
static extern void mouse_event(MouseEventFlag flags, int dx, int dy, uint data, UIntPtr extraInfo);

这个函数更强一些,不仅可以设置鼠标指针绝对的位置,而且可以以相对坐标来设置。另外,该函数还可以模拟鼠标左右键点击、鼠标滚轮操作等。其中的MouseEventFlag是一个基于uint类型的枚举,定义如下:

[Flags]
enum MouseEventFlag : uint
{
        Move        = 0×0001,
        LeftDown    = 0×0002,
        LeftUp      = 0×0004,
        RightDown   = 0×0008,
        RightUp     = 0×0010,
        MiddleDown  = 0×0020,
        MiddleUp    = 0×0040,
        XDown       = 0×0080,
        XUp         = 0×0100,
        Wheel       = 0×0800,
        VirtualDesk = 0×4000,
        Absolute    = 0×8000
}

利用这些这些函数,再配合一些其他的API函数,就可以自动寻找窗口、控件,用程序对其进行鼠标点击、键盘输入等等了。