避免应用程序的多个实例






4.92/5 (74投票s)
2000年5月17日

541208
学习限制应用程序只运行一个实例的正确方法。
摘要
出于多种原因,通常希望将特定计算机上运行的程序实例限制为一个。关于如何做到这一点,有很多民间传说,其中很多都是错误的。有一些好的方法,但也有很多有缺陷的方法。
我知道。我使用了一种有缺陷的方法多年。我甚至将该技术写进了我们的书《Win32 Programming》中。我对此感到非常尴尬。
本文探讨了一些解决方案,并指出了失败的解决方案存在的问题以及它们失败的条件。
真正危险的是,微软不仅没有记录正确的方法,还提供了两个存在致命缺陷的解决方案的代码示例,我将在后面详细讨论。
什么是“多个实例”?
一位读者 Daniel Lohmann(mailto:(daniel@uni-koblenz.de) 提出的一点很有趣,那就是“多个实例”这个术语在本文早期版本中定义得不够清晰。他指出,“多个实例”在 NT 的多用户环境中可能有不同的含义。他指出,可以使用 API 调用,如 LogonUser
、CreateProcessAsUser
、CreateDesktop
和 SwitchDesktop
,因此可以拥有多个用户和多个桌面会话在不同帐户下运行。
因此,有几个关于为什么人们不希望应用程序有多个实例的问题。他将它们归类为:
- 避免在同一用户会话中启动多个实例,无论该用户会话有多少桌面,但允许为不同的用户会话并发运行多个实例。
- 避免在同一桌面中启动多个实例,但允许只要每个实例都在单独的桌面上运行。
- 避免为同一个用户帐户启动多个实例,无论该帐户下有多少桌面或会话在运行,但允许为不同用户帐户下的会话并发运行多个实例。
- 避免在同一台计算机上启动多个实例。这意味着,无论任意数量的用户使用多少桌面,最多只能运行一个程序的实例。
他观察到,许多程序员实际上指的是 (a),有时指的是 (b) 或 (c)。本文早期版本中概述的技术始终将问题解释为 (d)。请注意,在 Win9x/Millenium 的情况下,只有 (d) 存在,因为每台机器只有一个用户会话和一个桌面。
因此,在阅读下面的文章时,请注意,我们使用此技术仅回答 (d)。之后,我将介绍他提出的解决方案,以便能够将此技术扩展到包括 (a)-(c)。他观察到,某些属于 (d) 类别的“面向服务的”应用程序通常最好作为系统服务来完成。该服务可以按需启动或在系统启动时启动,但所有用户和所有桌面共享同一个通用服务。
正确解决方案:使用内核对象
下面的代码改编自 David Lowndes 在 microsoft.public.mfc
新闻组中发布的一个示例。他的示例有一个使用 FindWindow
的选项,出于我将在下面解释的原因,该选项无法可靠工作。相反,我的版本使用了一个我认为更可靠的技术来定位其他实例的窗口。他的代码还使用了一个共享的(进程间)变量,这引入了另一个我将在后面讨论的问题。但是,如果您认为问题不重要,您可以选择使用更简单的版本。此解决方案是多桌面和多用户解决方案的基础通用解决方案;关键区别在于唯一 ID 的形成方式,这是一个将在稍后讨论的主题。
在下面的示例中,您必须将函数 CMyApp::searcher
声明为 static
类成员,例如:
static BOOL CALLBACK searcher(HWND hWnd, LPARAM lParam);
您必须声明一个已注册的窗口消息,例如 UWM_ARE_YOU_ME
。有关详细信息,请参阅我关于消息管理的文章。在 CMainFrame
类定义中,添加一个处理该消息的程序:
afx_msg LRESULT OnAreYouMe(WPARAM, LPARAM);
并添加一个 MESSAGE_MAP
条目:
ON_REGISTERED_MESSAGE(UWM_ARE_YOU_ME, OnAreYouMe)
实现如下:
LRESULT CMainFrame::OnAreYouMe(WPARAM, LPARAM) { return UWM_ARE_YOU_ME; } // CMainFrame::OnAreYouMe
现在您可以实现查找目标窗口的 searcher
方法了。
BOOL CALLBACK CMyApp::searcher(HWND hWnd, LPARAM lParam) { DWORD result; LRESULT ok = ::SendMessageTimeout(hWnd, UWM_ARE_YOU_ME, 0, 0, SMTO_BLOCK | SMTO_ABORT_IF_HUNG, 200, &result); if(ok == 0) return TRUE; // ignore this and continue if(result == UWM_ARE_YOU_ME) { /* found it */ HWND * target = (HWND *)lParam; *target = hWnd; return FALSE; // stop search } /* found it */ return TRUE; // continue search } // CMyApp::searcher //-------------------------------------------------------- BOOL CMyApp::InitInstance() { bool AlreadyRunning; HANDLE hMutexOneInstance = ::CreateMutex( NULL, FALSE, _T("MYAPPNAME-088FA840-B10D-11D3-BC36-006067709674")); // what changes for the alternative solutions // is the UID in the above call // which will be replaced by a call on // createExclusionName AlreadyRunning = ( ::GetLastError() == ERROR_ALREADY_EXISTS || ::GetLastError() == ERROR_ACCESS_DENIED); // The call fails with ERROR_ACCESS_DENIED if the Mutex was // created in a different users session because of passing // NULL for the SECURITY_ATTRIBUTES on Mutex creation); if ( AlreadyRunning ) { /* kill this */ HWND hOther = NULL; EnumWindows(searcher, (LPARAM)&hOther); if ( hOther != NULL ) { /* pop up */ ::SetForegroundWindow( hOther ); if ( IsIconic( hOther ) ) { /* restore */ ::ShowWindow( hOther, SW_RESTORE ); } /* restore */ } /* pop up */ return FALSE; // terminates the creation } /* kill this */ // ... continue with InitInstance return TRUE; } // CMyApp::InitInstance
在阅读了“竞态条件”一节之后,您可能会发现这段代码存在竞态条件。如果 Mutex 是由实例 1 创建的,而实例 1 仍在启动中(它还没有创建主窗口),并且实例 2 检测到 Mutex 已存在,它会尝试查找实例 1 的窗口。但由于实例 1 尚不存在,这段代码不会弹出实例 1 的窗口。但这没关系。因为实例 1 还没有创建它的窗口,它将继续成功创建它的窗口并弹出,正如您所期望的那样。
基本问题:竞态条件
在任何具有潜在并行执行线程或伪并行执行线程的系统中,都存在一种称为“竞态条件”的现象。这是一种情况,在这种情况下,两个(或更一般地,多个)进程之间协作的正确性取决于程序中某些路径执行的时序关系。如果程序 A 在程序 B 尚未正确设置事物之前到达某个执行点,程序 A 可能会失败。
许多“查找另一个正在运行的程序实例”的解决方案都因竞态条件而失败。在查看一种似乎确实有效的方法之后,我们将对其进行检查。
为什么 FindWindow 无效
一种流行的民间说法是可以使用 FindWindow
API 调用来避免多个实例。这个 API 使用起来非常危险。您的应用程序有很大的可能性在此调用中永久挂起。无论如何,即使不会导致挂起的条件不存在,围绕其使用的技术也非常有缺陷,以至于这种技术根本不可能奏效。所以它有两种故障模式:不出现实例,或出现多个实例。这两种情况都不能令人满意。
不幸的是,这种民间说法非常流行,以至于连微软也相信它。在他们的 KB 文章 Q109175 中,他们建议下载一个名为 onetime.exe
的程序,这是一个自解压的 zip 文件,其中包含一个执行此操作的示例。KB Q141752 及其附带的 onet32.exe
传播了这种迷思。相信我。我经历过。这不起作用。不要使用这种技术。
为什么它不起作用?嗯,如果一切完美,它就会起作用。但当出现一个微小、轻微、小小的错误时,它就会灾难性地失败,至少对最终用户而言是这样。此外,“错误”的事情并不意味着存在故障。一个完全正确运行的程序也可能导致失败。
FindWindow
调用是:
HWND CWnd::FindWindow(LPCTSTR classname, LPCTSTR caption)
classname 参数可以是 NULL
,表示匹配所有窗口类,或者是一个窗口类名。(底层 API 调用还可以接受一个转换为 LPCTSTR
的 HATOM
,但在 MFC 中很少使用 ::RegisterWindowClass
调用生成的 HATOM
)。caption 参数是一个字符串,与枚举的每个窗口的标题进行比较。该函数返回第一个类名与 classname 匹配且标题与 caption 匹配的窗口的 HWND
。
为什么这不起作用?
嗯,除非您使用 AfxRegisterClass
注册了应用程序的窗口类,否则您不知道类名。所以,您会说,“没关系,我只用 NULL
,但我知道标题名称”。
您不知道。
好吧,也许您知道。这周。在这个国家。对于这个版本。可能吧。只要它不是 MDI 应用程序。
首先,您不想硬编码一个常量值作为 caption 值。这是一个极其糟糕的主意。因为如果您对应用程序进行国际化,您将更改标题名称。所以您就失败了。
啊哈!您会将标题名称放在资源文件中。然后您将使用 LoadString
来获取它。好吧,这没问题,但然后您必须确保使用相同的 LoadString
来设置标题。好的,也许这可行。对于对话框应用程序。但对于其他应用程序,输入文件的文件名将被合并到标题中,您无法预测是什么,所以 FindWindow
已经无用了。
因此,如果您正在寻找标题,就不能使用 FindWindow
。
但情况变得更糟。FindWindow
实际上执行了一个 EnumWindows
调用,对于找到的每个顶级窗口句柄,它都会执行一个 GetWindowText
操作。这是通过向窗口句柄发送一个 WM_GETTEXT
消息来完成的。但如果句柄所属的线程被阻塞,例如在信号量、互斥体、事件、I/O 操作或其他方式上,SendMessage
将阻塞,直到该线程释放并运行。但这可能永远不会发生。所以 FindWindow
将永远阻塞,您的应用程序将永远不会启动。
当您不得不长途跋涉,因为您最好的客户在他的机器上无法启动应用程序时,您要确保 (a) 他再也不会遇到这种情况,(b) 他的客户绝对不会遇到这种情况!花了很长时间才找到这一点。
所以,您说,这显然是个愚蠢的主意。我将使用类名。毕竟,微软在他们的示例中就是这样做的,他们必须知道他们在做什么。
微软提供的示例代码是有缺陷的。但表面上的缺陷掩盖了深层次的缺陷。例如,类名被给定为一个字符串名称。但并不显而易见的是,您真的、真的必须修改这个名称,使其具有唯一性。全局唯一。也就是说,宇宙中任何其他实现者都绝不会使用相同的名称。
现在讲一个小故事,你们都应该牢记:很久以前,我写了一个 Win16 应用程序。它注册了一个窗口类“generic”,因为我从微软的通用示例中克隆了它。投诉是“您的程序失败了”。猜猜怎么着?它尝试注册窗口类“generic”,而另一个从通用示例克隆了其应用程序的人也使用了相同的名称。在那个时代,窗口类名是系统级的全局名称,所以它未能注册该类。
今天,您会说,这些名称不是全局唯一的;一个程序 A 注册一个类“MainWindow”,另一个程序 B 也注册一个类“MainWindow”是完全有效的。这是真的。但前提是它们中的任何一个都不关心另一个。如果程序 B 开始询问窗口的类名是什么,它就无法分辨名称“MainWindow”是它自己的一个实例注册的,还是由某个从未听说过它的其他程序注册的。
所以要做的第一件事是确保您的类名是唯一的,因为即使名称不是全局的,您也将要搜索该类名,而您无法分辨您正在与众多同名已注册类实例中的哪一个对话。
好的,您已经阅读了我其他的文章,知道如何通过使用GUIDGEN 来做到这一点。它创建一个 128 位数字,表示为一个十六进制字符字符串,已知它是全局唯一的。所以您只需将其附加到人类可读的类名后面。很酷。这样,您就知道名称是唯一的,并且您将使用该类名进行搜索。由于您不会陷入 WM_GETTEXT
的陷阱,因此其他任务是否挂起将无关紧要,因为毕竟,您不必让它运行就能获取有关窗口类的类信息。
猜猜怎么着。您刚刚掉入了竞态条件的陷阱。
下面是微软示例代码的一个骨架。我只包括了“好部分”。
LPCTSTR lpszUniqueClass = _T("MyNewClass"); //--------------------------------------------------- BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) { // Use the specific class name we established earlier cs.lpszClass = lpszUniqueClass; // [A] return CMDIFrameWnd::PreCreateWindow(cs); } //---------------------------------------------------------------- BOOL COneT32App::InitInstance() { // If a previous instance of the application is already running, // then activate it and return FALSE from InitInstance to end the // execution of this instance. if(!FirstInstance())// [B] return FALSE; // Register our unique class name that we wish to use WNDCLASS wndcls; memset(&wndcls, 0, sizeof(WNDCLASS)); // start with NULL defaults wndcls.style = ...; ... other wndcls variable inits here, irrelevant for ... this discussion // Specify our own class name for using FindWindow later wndcls.lpszClassName = lpszUniqueClass; // Register new class and exit if it fails if(!AfxRegisterClass(&wndcls)) // [C] { return FALSE; } ... rest of InitInstance here... CMainFrame* pMainFrame = new CMainFrame; // [D] ... more here return TRUE; } //---------------------------------------------------------------- BOOL COneT32App::FirstInstance() { CWnd *pWndPrev, *pWndChild; // Determine if another window with our class name exists... if (pWndPrev = CWnd::FindWindow(lpszUniqueClass,NULL)) // [E] //----------------------------------------------------------------
应用程序通常会通过执行代码来初始化,以便按照 // [x]
注释(按字母顺序从上到下,尽管代码的执行顺序并非如此)所示的顺序进行:
[B]-[E]-[C]-[D]-[A]
考虑下面显示的两个应用程序初始化序列。
时间 | 应用程序实例 1 | 应用程序实例 2 |
1 | B FirstInstance() | |
2 | E FindWindow() => FALSE | |
3 | B FirstInstance() | |
4 | E FindWindow() => FALSE | |
5 | C AfxRegisterClass() | |
6 | C AfxRegisterClass() | |
7 | D new CMainFrame | |
8 | D new CMainFrame | |
9 | A (in PreCreateWindow) | |
10 | A (in PreCreateWindow) |
请注意,这成功通过了测试!在时间 2,当应用程序实例 1 调用 FindWindow
时,没有其他窗口实例。所以 FindWindow 返回 FALSE
,表示没有第二个实例。因此,初始化序列继续进行。但与此同时,应用程序实例 2 正在初始化。在时间 4,当它执行 FindWindow
时,没有所需的类型的其他窗口实例。所以 FindWindow
返回 FALSE
,应用程序实例 2 知道它是唯一的实例。因此,它继续初始化。所以,当应用程序实例 1 创建 FindWindow
会找到的窗口时,测试早就过去了,应用程序实例 2 本来可以找到它。结果就是我们得到了两个正在运行的实例!
如果您认为这不可能发生,那么您就生活在一个与现实世界不同的世界里。我亲眼见过。反复如此。大约每三次就发生一次。
我发现这一点是因为一个客户有一台非常快的 Pentium 并且将 Win98 桌面配置为单击启动。多年的习惯让用户习惯了双击图标,所以每当他双击图标时,他就会启动两个应用程序副本。而且,发生这种情况的频率非常高,两个副本都启动了!
那么 CreateMutex
方法为什么有效呢?它不也存在同样的问题吗?不,因为 Mutex 的创建是内核中的一个*原子*操作。Mutex 的创建保证在任何其他线程能够成功创建 Mutex 之前完成。因此,我们绝对保证对象创建及其存在性测试是单一操作,而不是像 FindWindow
或(我在下一节中讨论的)SendMessage
方法那样被成千上万个可抢占的指令分割。
Daniel Lohmann,他对本文做出了重大贡献,他还指出,在“唯一性”方面,FindWindow
有一个问题,因为它只枚举与调用线程在同一桌面上的窗口。因此,如果另一个实例在另一个桌面上运行,您将找不到它来弹出它!
SendMessage:竞态条件
最常见的民间方法之一(也是我多年来使用的,唉)是使用 EnumWindows
,对每个窗口执行 SendMessage
,并查看 SendMessage
的返回值。您发送的是一个已注册的窗口消息(请参阅我关于消息管理的文章),如果您收到此消息,则返回 TRUE
。所有其他窗口都不会理解这一点并返回 FALSE
。事实证明,这在很多方面都存在严重缺陷。
请注意,此方法在 Win16 中*总是*有效的,因为它使用了协作式多任务处理。正是 Win32 的抢占式多任务处理使得此方法失败。它确实失败了。
SendMessage
可能会无限期挂起。但如果窗口句柄所属的线程被阻塞,例如在信号量、互斥体、事件、I/O 操作或其他方式上,SendMessage
将阻塞,直到该线程释放并运行。但这可能永远不会发生。所以您没有获得任何可靠性。
您可以使用 SendMessageTimeout
来解决这个问题。这或多或少可行。您通常会选择一个较短的超时时间,例如大约 200 毫秒。
但情况变得更糟。
微软,违反了所有已知的 Windows 规范,创建了一个应用程序,它*不会*将它不理解的消息传递给 DefWindowProc
,后者将为任何它不理解的消息返回 0。相反,他们有一个组件,似乎与个人 Web 服务器相关联,它具有一个真正反社会的特性,即返回 1 作为发送到其顶级窗口的每个消息的值,无论它是否理解。所以您不能依赖 0 来表示它不是您的应用程序。
好吧,这可以解决。当我因为其他原因需要这样做时,我最终不得不返回已注册的窗口消息值,这避免了微软的失误。
所以我们解决了超时和无效消息的问题。我们知道不要依赖标题内容,并且可能希望避免担心窗口类名。那么为什么这不起作用呢?
因为有一个更根本的问题:竞态条件。就像上一节中描述的那样。
代码执行了一个 EnumWindows
循环,并对每个 HWND
执行了 SendMessage
。所以发生的情况是,应用程序实例 1 搜索另一个它自己的实例,没有找到,然后继续启动。与此同时,应用程序实例 2 搜索它自己的一个实例,但由于实例 1 还没有启动并创建它自己的主窗口,实例 2 没有找到冲突的实例,然后它继续启动。使用一个类似于我用来演示基本 FindWindow
方法为何无效的表格的方案,您可以证明该方法会因为同样的原因而失败。
这是该机制最根本的失败之处,也是它无法使用的原因。
共享变量:一个不同的问题
另一个被提出的方法是使用应用程序所有实例之间的共享变量。这可以通过创建共享数据段来实现。该技术的形式为:
#pragma comment(linker, "/SECTION:.shr,RWS") #pragma data_seg(".shr") HWND hGlobal = NULL; #pragma data_seg() // in the startup code: // g_hWnd is set when the main window is created. BOOL CMyApp::InitInstance() { bool AlreadyRunning; HANDLE hMutexOneInstance = ::CreateMutex( NULL, TRUE, _T("MYAPPNAME-088FA840-B10D-11D3-BC36-006067709674")); AlreadyRunning = (GetLastError() == ERROR_ALREADY_EXISTS); if (hMutexOneInstance != NULL) { ::ReleaseMutex(hMutexOneInstance); } if ( AlreadyRunning ) { /* kill this */ HWND hOther = g_hWnd; if (hOther != NULL) { /* pop up */ ::SetForegroundWindow(hOther); if (IsIconic(hOther)) { /* restore */ ::ShowWindow(hOther, SW_RESTORE); } /* restore */ } /* pop up */ return FALSE; // terminates the creation } /* kill this */ // ... continue with InitInstance return TRUE; } // CMyApp::InitInstance
这几乎可行。它避免了根本的竞态条件,因为 CreateMutex
调用是一个原子操作。无论两个进程的相对时序如何,总会有一个进程首先创建 Mutex,而另一个进程会收到 ERROR_ALREADY_EXISTS
。请注意,我使用了 GUIDGEN
来获得一个保证唯一的 ID。
共享变量的使用带来了一个问题。这个共享变量只与其他来自同一可执行文件的实例共享。这意味着,如果您运行调试可执行文件的一个版本和发布可执行文件的一个版本,它们就无法找到对方的窗口来弹出它。因此,当一个实例发现自己是重复的时(它们仍然共享相同的 Mutex 名称),它就无法找到它的另一个实例来弹出它。这会让你感到困惑。
然而,代码比我的代码更简单;它不需要 EnumWindows
处理程序,也不需要其中的代码,或者用户定义的已注册窗口消息,或者 CMainFrame
中的处理程序。
我不明白为什么 Mutex 以拥有模式创建(第二个参数为 TRUE
)。微软的文档甚至说,当从不同的线程执行 CreateMutex
时,这个参数必须始终为 FALSE
,因为否则无法确定哪个线程真正拥有 Mutex。由于这个 Mutex 在代码中没有任何使用,因此使用 TRUE
参数似乎没有任何价值。
Daniel Lohmann 观察到,即使进程在不同用户帐户下运行,只要实例在同一台计算机上,共享内存也是共享的。当然,如果实例位于不同的桌面上,它们也是共享的。因此,使用共享变量在您将他下面将在下一节的建议中推广的概念时,其价值微乎其微,甚至可能是有害的。
泛化 NT 的解决方案
Daniel Lohmann 指出了上述机制的基本缺陷。虽然它*可靠*,但它并不*完整*,因为它只解决了“唯一实例”的三个可能含义之一。
- 避免在同一用户会话中启动多个实例。
- 避免在同一桌面中启动多个实例。
- 避免在同一用户帐户的任何会话中启动多个实例。
- 避免在同一台计算机上启动多个实例。
特别是,他指出我创建 Mutex 名称的方式使用了系统全局名称,保证对应用程序是唯一的,但对所有用户、会话和桌面都已知。请注意,这引发了关于任何使用全局名称用于 Mutex、信号量、事件甚至共享内存映射文件的问题:单个名称有效的假设取决于您对上述三个点的解释。因此,如果您正在构建一个使用同步原语且需要除 (d) 之外的解决方案的系统,您将不得不将以下技术应用于同步原语命名。
他指出,在 NT 的终端服务器版本(内置于 Windows 2000 中)中,内核不再有一个单一的“全局”命名空间,实际上每个终端服务器会话都有一个私有命名空间。系统服务共享一个通用命名空间,用于所谓的“控制台会话”。他指出,“这一切的结果是消耗更多内存,并使一些编程任务非常棘手,但结果是每个登录到终端服务器的用户都能启动其电子邮件客户端”。
他还对我进行了另一个小修改:
如果 Mutex 是在另一个用户的会话中创建的,
CreateMutex
() 调用将以ERROR_ACCESS_DENIED
失败。这是因为传递NULL
作为SECURITY_ATTRIBUTES
导致了默认的安全设置。典型的默认 DACL 只允许 CREATOR/OWNER 和 SYSTEM 访问对象。
他提出的解决方案是,将 Mutex 的名称扩展到我使用的 GUID 技术之外,以解决 (a)-(c) 的解决方案。他写道:
“我从 (b) 开始,因为它更简单。使用
GetThreadDesktop()
,您可以获取您线程运行所在桌面的句柄。将其传递给GetUserObjectInformation()
,您将获得桌面的名称,该名称是唯一的”。“即使是 (c) 也相当容易。解决方案是添加当前用户的帐户名。使用
GetUserName()
,您可以获取当前用户的帐户名。您应该用当前用户的域来限定它,该域可以通过使用GetEnvironmentVariable()
并将 USERDOMAIN 作为变量名来确定。”“对于 (a),这稍微复杂一些。您必须使用
OpenProcessToken()
打开进程令牌。将该令牌传递给GetTokenInformation()
以检索TOKEN_STATISTICS
结构。该结构中的AuthenticationId
成员是一个 64 位数字(编码为LUID
),它包含登录会话的唯一 ID。将其转换为字符串”。
根据他的描述,我创建了以下子程序和头文件。请注意,对于任何给定的应用程序,您必须在编译时决定您想要的排除选项;例如,如果您希望应用程序对桌面是唯一的,请选择 UNIQUE_TO_DESKTOP
选项来生成密钥。如果您有一个动态选择此项的应用程序,您可以让一个运行在系统中,认为它是唯一的,另一个运行在桌面上,认为它是唯一的。我构建了一个小项目来测试这段代码,您可以在本文顶部下载。
exclusion.h
#define UNIQUE_TO_SYSTEM 0 #define UNIQUE_TO_DESKTOP 1 #define UNIQUE_TO_SESSION 2 #define UNIQUE_TO_TRUSTEE 3 CString createExclusionName(LPCTSTR GUID, UINT kind = UNIQUE_TO_SYSTEM);
exclusion.cpp
#include "stdafx.h" #include "exclusion.h" /**************************************************************************** * createExclusionName * Inputs: * LPCTSTR GUID: The GUID for the exclusion * UINT kind: Kind of exclusion * UNIQUE_TO_SYSTEM * UNIQUE_TO_DESKTOP * UNIQUE_TO_SESSION * UNIQUE_TO_TRUSTEE * Result: CString * A name to use for the exclusion mutex * Effect: * Creates the exclusion mutex name * Notes: * The GUID is created by a declaration such as * #define UNIQUE _T("MyAppName-{44E678F7-DA79-11d3-9FE9-006067718D04}") ****************************************************************************/ CString createExclusionName(LPCTSTR GUID, UINT kind) { switch(kind) { /* kind */ case UNIQUE_TO_SYSTEM: return CString(GUID); case UNIQUE_TO_DESKTOP: { /* desktop */ CString s = GUID; DWORD len; HDESK desktop = GetThreadDesktop(GetCurrentThreadId()); BOOL result = GetUserObjectInformation(desktop, UOI_NAME, NULL, 0, &len); DWORD err = ::GetLastError(); if(!result && err == ERROR_INSUFFICIENT_BUFFER) { /* NT/2000 */ LPBYTE data = new BYTE[len]; result = GetUserObjectInformation(desktop, UOI_NAME, data, len, &len); s += _T("-"); s += (LPCTSTR)data; delete [ ] data; } /* NT/2000 */ else { /* Win9x */ s += _T("-Win9x"); } /* Win9x */ return s; } /* desktop */ case UNIQUE_TO_SESSION: { /* session */ CString s = GUID; HANDLE token; DWORD len; BOOL result = OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token); if(result) { /* NT */ GetTokenInformation(token, TokenStatistics, NULL, 0, &len); LPBYTE data = new BYTE[len]; GetTokenInformation(token, TokenStatistics, data, len, &len); LUID uid = ((PTOKEN_STATISTICS)data)->AuthenticationId; delete [ ] data; CString t; t.Format(_T("-%08x%08x"), uid.HighPart, uid.LowPart); return s + t; } /* NT */ else { /* 16-bit OS */ return s; } /* 16-bit OS */ } /* session */ case UNIQUE_TO_TRUSTEE: { /* trustee */ CString s = GUID; #define NAMELENGTH 64 TCHAR userName[NAMELENGTH]; DWORD userNameLength = NAMELENGTH; TCHAR domainName[NAMELENGTH]; DWORD domainNameLength = NAMELENGTH; if(GetUserName(userName, &userNameLength)) { /* get network name */ // The NetApi calls are very time consuming // This technique gets the domain name via an // environment variable domainNameLength = ExpandEnvironmentStrings(_T("%USERDOMAIN%"), domainName, NAMELENGTH); CString t; t.Format(_T("-%s-%s"), domainName, userName); s += t; } /* get network name */ return s; } /* trustee */ default: ASSERT(FALSE); break; } /* kind */ return CString(GUID); } // createExclusionName
在原始示例中,将我硬编码到 ::CreateMutex
调用中的字符串替换为调用 createExclusionName
并指定所需的规范,以获得格式正确的唯一名称用于 Mutex。
摘要
传统的多个实例检测方法,包括微软文档化的方法,都存在严重缺陷。只有一种方法被证实能正确工作,那就是创建内核对象,本文档以该技术可能使用的多种形式中的两种进行了介绍。
“唯一”的概念应该被清晰地定义;在大多数情况下,它意味着“对会话唯一”并且也许“对桌面唯一”,而简单的做法实际上可能会阻止独立的 NT 系统用户并发运行。
致谢
特别感谢 Daniel Lohmann 对本文的精彩建议!他的代码的一个纯 C 版本(与我实现的代码略有不同)可以在此处找到。如果您对多桌面和终端服务器感兴趣,他的网站值得一看。
这些文章中表达的观点是作者的观点,不代表,也不被微软认可。