Sunday, August 08, 2004

 

Limiting an application to a single Instance in C#

首先,我们回顾一下MFC中我们是怎么干的。

1. 首先,Joseph M. Newcomer的经典大作Avoiding Multiple Instances of an Application
http://www.codeproject.com/cpp/avoidmultinstance.asp
是一定要看的。只是有点繁琐。文后的讨论很精彩。

不过,简单里说,我还喜欢采用下面的两种方法

2. Armen Hakobyan的
http://www.codeproject.com/threads/singleinstancemfc.asp
较完善,有效
其中,下面的代码非常关键,能在一定程度上保证正确性

[code]
//...
BOOL CWinAppEx::EnableTokenPrivilege( LPCTSTR lpszSystemName,
BOOL bEnable /*TRUE*/ )
{
ASSERT( lpszSystemName != NULL );
BOOL bRetVal = FALSE;

if( ::GetVersion() < 0x80000000 ) // NT40/2K/XP // afxData ???
{
HANDLE hToken = NULL;
if( ::OpenProcessToken( ::GetCurrentProcess(),
TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
{
TOKEN_PRIVILEGES tp = { 0 };
if( ::LookupPrivilegeValue( NULL, lpszSystemName, &tp.Privileges[0].Luid ) )
{
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = ( bEnable ? SE_PRIVILEGE_ENABLED : 0 );

// To determine whether the function adjusted all of the
// specified privileges, call GetLastError:

if( ::AdjustTokenPrivileges( hToken, FALSE, &tp,
sizeof( TOKEN_PRIVILEGES ), (PTOKEN_PRIVILEGES)NULL, NULL ) )
{
bRetVal = ( ::GetLastError() == ERROR_SUCCESS );
}
}
::CloseHandle( hToken );
}
}

return bRetVal;
}
//...
[/code]

参考文献
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/WinUI/WindowsUserInterface/Windowing/MessagesandMessageQueues/
MessagesandMessageQueuesReference/MessagesandMessageQueuesFunctions/
BroadcastSystemMessage.asp

3.
http://www.losoft.de/code/SingleInstance.zip
这个代码也不错,核心还是“enrich the mutex name with additional information, to ensure the right definition of "unique instance" is used. If you want to ensure that there is only one instance per visible desktop, include the desktop name in the name of the mutex etc.”

SingleInstance.h
[code]
#ifndef __SINGLEINSTANCE_H__
#define __SINGLEINSTANCE_H__

const SI_SESSION_UNIQUE = 0x0001; // Allow only one instance per login session
const SI_DESKTOP_UNIQUE = 0x0002; // Allow only one instance on current desktop
const SI_TRUSTEE_UNIQUE = 0x0004; // Allow only one instance for current user
const SI_SYSTEM_UNIQUE = 0x0000; // Allow only one instance at all (on the whole system)

// Note: SI_SESSION_UNIQE and SI_TRUSTEE_UNIQUE can
// be combined with SI_DESKTOP_UNIQUE

LPTSTR CreateUniqueName( LPCTSTR pszGUID, LPTSTR pszBuffer, int nMode = SI_DESKTOP_UNIQUE );
BOOL IsInstancePresent( LPCTSTR pszGUID, int nMode = SI_DESKTOP_UNIQUE );

#endif
[/code]

SingleInstance.cpp
[code]
#include "stdafx.h"
#include "SingleInstance.h"
#include // We use the _alloca() function

////////////////////////////////////////////////////////////////////////////////
// LPTSTR CreateUniqueName( pszGUID, pszBuffer, nMode )
//
// Creates a "unique" name, where the meaning of "unique" depends on the nMode
// flag values. Returns pszBuffer
//
// pszGUID: Copied to the beginning of pszBuffer, should be an GUID
// pszBuffer: Buffer for unique name. Length (in chars) must be >= MAX_PATH
// nMode: Information, that should be used to create the unique name.
// Can be one of the following values:
//
//
// SI_SESSION_UNIQUE - Allow one instance per login session
// SI_DESKTOP_UNIQUE - Allow one instance per desktop
// SI_TRUSTEE_UNIQUE - Allow one instance per user account
// SI_SESSION_UNIQUE | SI_DESKTOP_UNIQUE - Allow one instance per login session,
// instances in different login sessions
// must also reside on a different desktop
// SI_TRUSTEE_UNIQUE | SI_DESKTOP_UNIQUE - Allow one instance per user account,
// instances in login sessions running a
// different user account must also reside
// on different desktops.
// SI_SYSTEM_UNIQUE - Allow only one instance on the whole system
//
LPTSTR CreateUniqueName(
LPCTSTR pszGUID,
LPTSTR pszBuffer,
int nMode // = SI_DESKTOP_UNIQUE
)
{
if( pszBuffer == NULL ) {
SetLastError( ERROR_INVALID_PARAMETER );
return NULL;
}

// First copy GUID to destination buffer
if( pszGUID )
_tcscpy( pszBuffer, pszGUID );
else
*pszBuffer = 0;

if( nMode & SI_DESKTOP_UNIQUE ) {
// Name should be desktop unique, so add current desktop name

_tcscat( pszBuffer, _T("-" ) );
HDESK hDesk = GetThreadDesktop( GetCurrentThreadId() );
ULONG cchDesk = MAX_PATH - _tcslen( pszBuffer ) - 1;

if( !GetUserObjectInformation( hDesk, UOI_NAME, pszBuffer + _tcslen( pszBuffer ), cchDesk, &cchDesk ) )
// Call will fail on Win9x
_tcsncat( pszBuffer, _T("Win9x"), cchDesk );
}
if( nMode & SI_SESSION_UNIQUE ) {
// Name should be session unique, so add current session id

HANDLE hToken = NULL;
// Try to open the token (fails on Win9x) and check necessary buffer size
if( OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY, &hToken )
&& ( MAX_PATH - _tcslen( pszBuffer ) > 9 ) )
{
DWORD cbBytes = 0;

if( !GetTokenInformation( hToken, TokenStatistics, NULL, cbBytes, &cbBytes )
&& GetLastError() == ERROR_INSUFFICIENT_BUFFER )
{
PTOKEN_STATISTICS pTS = (PTOKEN_STATISTICS) _alloca( cbBytes );
if( GetTokenInformation( hToken, TokenStatistics, (LPVOID) pTS, cbBytes, &cbBytes ) ) {
wsprintf(
pszBuffer + _tcslen( pszBuffer ),
_T("-%08x%08x"),
pTS->AuthenticationId.HighPart,
pTS->AuthenticationId.LowPart
);
}
}
}
}
if( nMode & SI_TRUSTEE_UNIQUE ) {
// Name should be unique to the current user

TCHAR szUser[ 64 ] = {0};
TCHAR szDomain[ 64 ] = {0};
DWORD cchUser = 64;
DWORD cchDomain = 64;

if( GetUserName( szUser, &cchUser ) ) {
// Since NetApi() calls are quite time consuming
// we retrieve the domain name from an environment variable
cchDomain = GetEnvironmentVariable( _T("USERDOMAIN"), szDomain, cchDomain );

UINT cchUsed = _tcslen( pszBuffer );
if( MAX_PATH - cchUsed > cchUser + cchDomain + 3 ) {
wsprintf(
pszBuffer + cchUsed,
_T("-%s-%s"),
szDomain,
szUser
);
}
}
}

return pszBuffer;
}

////////////////////////////////////////////////////////////////////////////////
// BOOL IsInstancePresent( pszGUID, nMode )
//
// Returns TRUE, if there exists, according to the meaning of "unique" passed
// in nMode, another instance of this process.
//

BOOL IsInstancePresent(
LPCTSTR pszGUID,
int nMode // = SI_DESKTOP_UNIQUE
)
{
static HANDLE hMutex = NULL;

if( hMutex == NULL ) {
TCHAR szName[ MAX_PATH ];
hMutex = CreateMutex( NULL, FALSE, CreateUniqueName( pszGUID, szName, nMode ) );
return ( GetLastError() == ERROR_ALREADY_EXISTS || GetLastError() == ERROR_ACCESS_DENIED );
}
return FALSE;
}
[/code]

4. 而再弱一点
Niek Albers的
http://www.codeproject.com/threads/singleinstance.asp
可以说是方便快捷,效果尚可,不过“does not behave gracefully in a multiuser environment”

参考文献
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/messagesandmessagequeues/
messagesandmessagequeuesreference/messagesandmessagequeuesfunctions/
sendmessagetimeout.asp

还有这个就不要看了
http://support.microsoft.com/default.aspx?scid=kb;EN-US;Q243953

再来看.Net中方法

5. 最简单的
[code]
public static void Main( String[] args ) {
Int32 RunningProcesses = Process.GetProcessesByName( Process.GetCurrentProcess().ProcessName ).Length;
if ( RunningProcesses <= 1 ) {
Application.Run( new Foo() );
} else {
MessageBox.Show( "already running the program" );
}
}
}
[/code]

不过要当心该进程的名字被改写等不少情况下将出错。

6. 使用Net下面的Mutex,翻译上述算法
参见
http://www.windevnet.com/documents/s=7535/win1061231571777/0815dotnet.html

下面是翻译
...
m_mutex=new Mutex(false,mutexname); // 使用GUID等生成独一无二的mutexname
...
但是使用.Net下面的Mutex的多用户环境没有测试过,希望牛人出来说说是否可行。

一个新的问题是
class App : Form

{

Mutex mutex;

App()

{

Text = "Single Instance!";

mutex = new Mutex(false, "SINGLE_INSTANCE_MUTEX");

if (!mutex.WaitOne(0, false))

{

mutex.Close();

mutex = null;

}

}

protected override void Dispose(bool disposing)

{

if (disposing)

mutex.ReleaseMutex();

base.Dispose(disposing);

}

static void Main()

{

App app = new App();

if (app.mutex != null) Application.Run(app);

else MessageBox.Show("Instance already running");

}

}

在以上代码中,方法Dispose()并不严格需要,这是因为当程序结束时,垃圾回收器将会dispose并且release mutex对象。但是我仍然添加了它,这是因为Form需要一段很长的时间来释放(dispose),并且另一个窗体(Form)的实例将会启动。

这种方法只允许第一个进程实例运行。但如果你只想运行最新的那个实例时怎么办呢?就是说如果我启动了一个新的进程实例,而且之前已经有一个实例正在运行中,那么我们需要停止之前的那个线程。例如屏保的显示属性对话框。这个对话框显示一个小的预览窗口,当用用户点击预览按钮时,另一个屏保程序实例(instance)就会启动并全屏显示。当全屏实例停止时(例如你移动了鼠标),另一个屏保实例就会开始在小的预览窗口中开始运行。很明显,当全屏实例开始,这个预览的屏保窗口进程就要结束。

一种实现的方法是为每一个实例去存取一个命名的事件核心对象(named event kernel object),如果事件对象没有被触发(nonsignaled),那么此实例就继续运行;如果事件被触发则程序实例会结束。应用程序可以周期性地测试事件去看是否它已经被触发。当一个新的实例启动,它就会设置事件(去关闭其它任何实例)然后重设事件以使它可以继续运行。这种方案除了一个小问题外运行良好:.NET Framework不会允许你命名一个核心事件(kernel event)。以下是一个实现的类

public class NamedEventHelper

{

[DllImport("kernel32")]

static extern uint CreateEvent(

uint sec, bool manualReset, bool initialState, string name);

static IntPtr CreateEvent(bool manualReset, bool initialState, string name)

{

return new IntPtr(CreateEvent(0, manualReset, initialState, name));

}

[DllImport("kernel32")]

static extern bool CloseHandle(IntPtr handle);

public static ManualResetEvent CreateNamedEvent(

bool initialState, string name)

{

ManualResetEvent mre = new ManualResetEvent(false);

CloseHandle(mre.Handle);

mre.Handle = CreateEvent(true, initialState, name);

return mre;

}

}



以上的代码中,静态(static)方法CreateNamedEvent()创建了一个ManualResetEvent对象,并释放了当前的(underlying)Win32句柄(handle)。然后再创建一个命名事件(named event)并且使用此新事件的句柄初始化了ManualResetEvent对象。此事件对象就能被用于两个应用程序间的通信。

这种方案的弱点是一个应用程序一定要看它是否已经结束。一种解决方法是运行一个后台线程去管理事件

ManualResetEvent mre;

mre = NamedEventHelper.CreateNamedEvent(false, "LAST_INSTANCE_ONLY");

// Stop the other instances

mre.Set();

// Reset the event so that we can run

mre.Reset();

// Create a monitor thread

Thread t = new Thread(new ThreadStart(Monitor));

// Make sure that this thread cannot keep the app alive

t.IsBackground = true;

t.Start();



//The Monitor() method looks like this:



void Monitor()

{

mre.WaitOne();

Application.Exit();

}

如果程序有一个窗体,那么另一种方法就是告诉其它窗口去关闭这个新的程序实例。如果这是一个Win32应用程序,那么此程序可以简单地通过调用FindWindowsEx(),并传入一个特定的窗口类参数,然后发送WM_CLOSE消息。但是你不能对Windows Form这样做 ,因为大多数窗体(Forms)都有相同的类名,正如我在上一封newsletter是所说的。

7.
又比如

发信人: swimdaily (每日一游), 信区: DotNET
标 题: 只运行一个实例,并把已运行实例激活
发信站: BBS 水木清华站 (Mon Sep 27 01:26:54 2004), 站内

网上找了找,这个还不错。
有没有办法不导入USER32库激活某个进程的方法?

using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Diagnostics;
using System.Reflection;

public class OneInstnace
{
[STAThread]
public static void Main()
{
//Get the running instance.
Process instance = RunningInstance();
if (instance == null)
{
//There isn't another instance, show our form.
Application.Run (new Form());
}
else
{
//There is another instance of this process.
HandleRunningInstance(instance);
}
}
public static Process RunningInstance()
{
Process current = Process.GetCurrentProcess();
Process[] processes = Process.GetProcessesByName (current.ProcessName);

//Loop through the running processes in with the same name
foreach (Process process in processes)
{
//Ignore the current process
if (process.Id != current.Id)
{
//Make sure that the process is running from the exe file.
if (Assembly.GetExecutingAssembly().Location.Replace("/", "\\") ==
current.MainModule.FileName)
{
//Return the other process instance.
return process;
}
}
}

//No other instance was found, return null.
return null;
}


public static void HandleRunningInstance(Process instance)
{
//Make sure the window is not minimized or maximized
ShowWindowAsync (instance.MainWindowHandle , WS_SHOWNORMAL);

//Set the real intance to foreground window
SetForegroundWindow (instance.MainWindowHandle);
}

[DllImport("User32.dll")]

private static extern bool ShowWindowAsync(
IntPtr hWnd, int cmdShow);
[DllImport("User32.dll")] private static extern bool
SetForegroundWindow(IntPtr hWnd);
private const int WS_SHOWNORMAL = 1;
}


--

※ 来源:·BBS 水木清华站 smth.org·[FROM: 166.111.145.*]

8. 翻来翻去,我认为还是用DllImport将已经写好的MFC模块引进来最方便,最有把握正确。
一句老话,MFC,Windows消息系统还将继续活下去。



<< Home

This page is powered by Blogger. Isn't yours?