摘要:
本文闡述了在基於.NET平臺的應用程序開發中如何實現唯一應用程序運行實例,對幾種實現方式進行分析測試比較,從而尋找一種合適的處理方式。單擊此處才查看本文的示例代碼。
概述
在開發一些應用系統的時候,由於程序內在的一些特徵,系統的某些組成子程序只允許運行一個應用程序實例,以保證業務和數據處理安全。本文將從實際應用角度來分析其實現原理,對三種實現方式進行測試比較,從而確定一種合適的實現方法。文章的例子使用C#語言進行描述。
進程匹配
對於每一個應用程序運行實例都會包含該實例的一個或多個進程,而且在程序運行過程中可能會動態的創建或銷燬進程,或者訪問其他現有進程進行通信。不難發現,在程序最先初始化的那一刻只有一個進程運行,而且應用程序進程生命週期最大進程名稱集合是不變的。因此,在應用程序初始化的時候,可以根據進程關鍵信息檢查系統進程列表是否存在同當前初始化進程匹配的進程來確定是否已經運行進程實例。
邏輯處理步驟如下,
1.初始化應用程序,啓動程序初始化進程;
2.訪問系統進程列表,根據初始化進程關鍵信息進行匹配查找;
3.沒有找到匹配進程(這一步是不會發生的,因爲當前初始化進程也在列表中,不過還要看獲取進程列表的實現代碼怎麼寫),繼續初始化進程,程序初始化完成運行。
4.找到第一個匹配進程,判斷找到的進程ID是否同初始化進程ID相同;
5.如果第一個匹配進程ID同初始化進程ID相同,則爲當前初始化進程,繼續查找;
6.沒有找到第二個匹配進程,表明當前運行的是首個實例,繼續初始化進程,程序初始化完成運行。
7.找到第二個,表明已有一個實例在運行,停止當前程序初始化,提示已有應用程序運行。
8.如果找到第一個匹配進程ID不同,表明已有一個實例在運行,停止當前程序初始化,提示已有應用程序運行。
可見上面的邏輯實現中用於進程匹配的信息是關鍵,選擇不當功能就無法實現。在這個實例中筆者使用了應用程序完全文件名稱作爲關鍵信息。
在代碼中首先需要引用下面命名空間,以調用WinAPI函數。
using System.Runtime.InteropServices;
把實現唯一運行實例功能的類名取爲SingleInstance,在類前面加static關鍵字爲C# 2.0新增的語言特徵。
public static class SingleInstance {}
使用GetRunningInstance靜態方法獲取應用程序進程實例,如果沒有匹配進程,返回Null值,
public static Process GetRunningInstance()
{
Process currentProcess = Process.GetCurrentProcess(); //獲取當前進程
//獲取當前運行程序完全限定名
string currentFileName = currentProcess.MainModule.FileName;
//獲取進程名爲ProcessName的Process數組。
Process[] processes = Process.GetProcessesByName(currentProcess.ProcessName);
//遍歷有相同進程名稱正在運行的進程
foreach (Process process in processes)
{
if (process.MainModule.FileName == currentFileName)
{
if (process.Id != currentProcess.Id) //根據進程ID排除當前進程
return process;//返回已運行的進程實例
}
}
return null;
}
接下來調用兩個WinAPI,其功能將在包裝方法中描述,
[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;
以上的方法聲明爲私有,對其進一步包裝,HandleRunningInstance靜態方法爲獲取應用程序句柄,設置應用程序爲前臺運行,並返回bool值。
public static bool HandleRunningInstance(Process instance)
{
//確保窗口沒有被最小化或最大化
ShowWindowAsync(instance.MainWindowHandle, WS_SHOWNORMAL);
//設置爲foreground window
return SetForegroundWindow(instance.MainWindowHandle);
}
對上面的方法創建一個重載版本,使調用代碼更加簡潔,
public static bool HandleRunningInstance()
{
Process p = GetRunningInstance();
if (p != null)
{
HandleRunningInstance(p);
return true;
}
return false;
}
上面的方法實現獲取已經運行的進程實例的句柄,並獲取其焦點顯示到前臺,這個很有用,在其他實現方式中也可以用到。
在Main函數中調用下面代碼實現單一應用程序實例,
Process p = SingleInstance.GetRunningInstance();
if (p != null) //已經有應用程序副本執行
{
SingleInstance.HandleRunningInstance(p);
}
else //啓動第一個應用程序
{
Application.Run(new MainForm());
}
簡潔的調用爲,
if (SingleInstance.HandleRunningInstance()== false)
{
Application.Run(new MainForm());
}
可見,在上面的實現過程中,由於關鍵信息採用應用程序的完整文件名,因此在文件名稱或路徑名稱修改後,以上實現就會失效。
進程互斥
在這個實現方式中需要定義一個進程同步基元,可以理解爲臨界資源,該資源只允許一個進程使用。根據這一點實現應用程序唯一運行實例就比較簡單了。
實現步驟如下,
1.應用程序初始化訪問該同步基元;
2.可以訪問,說明該同步基元未被使用,也就是說沒有應用程序實例運行,使用同步基元,可以繼續初始化成爲第一個運行實例。
3.不可以訪問,說明該同步基元已被使用,也就是說已有應用程序實例運行,停止當前程序初始化,提示已有應用程序運行。
4.應用程序實例退出釋放同步基元佔用。
在代碼中筆者使用System.Threading.Mutex類實現同步基元,實現應用程序實例之間互斥功能。Mutex默認名字取Assembly.GetEntryAssembly().FullName。
在類成員中聲明同步基元,
private static Mutex mutex = null;
CreateMutex靜態方法創建應用程序進程Mutex,返回創建結果爲true表示創建成功,false失敗。
public static bool CreateMutex()
{
return CreateMutex(Assembly.GetEntryAssembly().FullName);
}
實現其重載方法,讓用戶可以自定義Mutex名字,
public static bool CreateMutex(string name)
{
bool result = false;
mutex = new Mutex(true, name, out result);
return result;
}
對應的釋放Mutex資源方法爲,
public static void ReleaseMutex()
{
if (mutex != null)
{
mutex.Close();
}
}
在Main函數中調用下面代碼實現單一應用程序實例,
if (SingleInstance.CreateMutex())
{
Application.Run(new MainForm());
SingleInstance.ReleaseMutex();
}
else
{
MessageBox.Show("程序已經運行!");
}
可見,在上面的實現過程中,Mutex名字是同步基元的唯一標識,如果剛好有不同的應用程序使用了相同名稱的Mutex,那不同的應用程序實例也會出現互斥現象。
運行標誌
使用應用程序運行標誌簡單來講就是在程序初始化的時候設置一個標誌表示程序已運行,在程序運行結束的時候刪除該標誌。
基本步驟如下,
1.應用程序初始化檢查運行標誌是否已經設置;
2.發現已經設置,說明已有應用程序實例運行,停止當前程序初始化,提示已有應用程序運行。 3.發現沒有設置,說明沒有應用程序實例運行,繼續當前程序初始化。
4.退出應用程序時刪除該運行標誌。
對於標誌存儲載體可以使用註冊表、數據庫或外部文件等,這裏的代碼使用外部文件實現。對存放標誌的文件目錄選擇C:/Documents and Settings/All Users/Application Data,也可以是C:/Program Files/Common Files。
聲明類成員標誌文件名稱變量,
private static string runFlagFullname = null;
初始化程序運行標誌,如果設置成功,返回true,已經設置返回false,設置失敗將拋出異常,
public static bool InitRunFlag()
{
if (File.Exists(RunFlag))
{
return false;
}
using (FileStream fs = new FileStream(RunFlag, FileMode.Create))
{
}
return true;
}
釋放初始化程序運行標誌,如果釋放失敗將拋出異常,
public static void DisposeRunFlag()
{
if (File.Exists(RunFlag))
{
File.Delete(RunFlag);
}
}
獲取或設置程序運行標誌,必須符合Windows文件命名規範,
public static string RunFlag
{
get
{
if(runFlagFullname == null)
{
string assemblyFullName = Assembly.GetEntryAssembly().FullName;
string path = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
runFlagFullname = Path.Combine(path, assemblyFullName);
}
return runFlagFullname;
}
set
{
runFlagFullname = value;
}
}
在Main函數中調用下面代碼實現單一應用程序實例,
if (SingleInstance.InitRunFlag())
{
Application.Run(new MainForm());
SingleInstance.DisposeRunFlag();
}
else
{
MessageBox.Show("程序已經運行!");
}
可見,在上面的實現過程中,需要訪問文件IO,因此有可能會出現異常,對異常需要進行具體處理。如果不同應用程序使用了相同的運行標誌,也會出現進程互斥實現中存在的問題。由於運行標誌存在外部載體中,如果筆者把啓動的應用程序進程實例直接在Windows管理器進程列表中結束或使其產生異常,那設置的運行標誌就不會銷燬,應用程序就沒法再次運行。
功能測試
這一節對上面的三個功能進行測試,以分析之間的區別。功能測試類別包括下面五類,
1.本地系統同一應用程序目錄;
2.本地系統同一應用程序修改運行文件名稱使兩次運行名稱不同;
3.本地系統兩次運行程序目錄不同,不修改文件名稱;
4.本地系統不同會話用戶登錄啓動應用程序;
5.遠程計算機程序訪問啓動應用程序(一個程序在遠程另一個在本地)。
根據代碼實現細節不同,對測試的結果可能會有所不同,這裏的測試結果以筆者上面幾節中實現的代碼爲準。爲了測試簡單化,通過給應用程序傳入測試參數,決定使用哪種方式,入口函數調用代碼爲,
[STAThread]
static void Main(string[] args)
{
if (args.Length == 0) //沒有傳送參數
{
Process p = SingleInstance.GetRunningInstance();
if (p != null) //已經有應用程序副本執行
SingleInstance.HandleRunningInstance(p);
else //啓動第一個應用程序
Application.Run(new MainForm());
}
else //有多個參數
{
switch (args[0].ToLower())
{
case "-api":
if (SingleInstance.HandleRunningInstance() == false)
Application.Run(new MainForm());
break;
case "-mutex":
if (args.Length >= 2) //參數中傳入互斥體名稱
{
if ( SingleInstance.CreateMutex(args[1]) )
{
Application.Run(new MainForm());
SingleInstance.ReleaseMutex();
}
else
//調用SingleInstance.HandleRunningInstance()方法顯示到前臺。
MessageBox.Show("程序已經運行!");
}
else
{
if (SingleInstance.CreateMutex())
{
Application.Run(new MainForm());
SingleInstance.ReleaseMutex();
}
else
//調用SingleInstance.HandleRunningInstance()方法顯示到前臺。
MessageBox.Show("程序已經運行!");
}
break;
case "-flag"://使用該方式需要在程序退出時調用
if (args.Length >= 2) //參數中傳入運行標誌文件名稱
SingleInstance.RunFlag = args[1];
try
{
if (SingleInstance.InitRunFlag())
{
Application.Run(new MainForm());
SingleInstance.DisposeRunFlag();
}
else
//調用SingleInstance.HandleRunningInstance()方法顯示到前臺。
MessageBox.Show("程序已經運行!");
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString());
}
break;
default:
MessageBox.Show("應用程序參數設置失敗。");
break;
}
}
}
運行CMD命令行,
第一種調用爲, WindowsApplication1.exe –api 或 WindowsApplication1.exe
第二種調用爲, WindowsApplication1.exe –mutex 或WindowsApplication1.exe –mutex {F140AE26-626C-42f8-BD49-45025742205E}
第三種調用爲, WindowsApplication1.exe –flag 或WindowsApplication1.exe –flag c:/blog.csdn.net.zhzuo
測試結果,
匹配/互斥/標誌 | 1同一目錄 | 2修改名稱 | 3不同目錄 | 4不同用戶 | 5遠程訪問 |
1同一目錄 | O/O/O | ||||
2修改名稱 | X/O/O | ||||
3不同目錄 | X/O/O | ||||
4不同用戶 | #/X/O | ||||
5遠程訪問 | X/O/O |
備註:O - 表示成功,X – 表示失敗,# - 程序第二個運行沒有反應
針對遠程訪問的測試,需要在系統管理工具的.NET Framework 2.0 Configuration中進行設置授權該局域網路徑允許訪問,否則會拋出System.Security.SecurityException異常。根據測試結果可見三種實現方式適用範圍不同,理想的實現是結合他們的優點進行多點判斷。