Question 46

Comment empecher le lancement d'une application si celle-ci tourne deja ?

Nous exposerons dans cet article deux méthodes permettant d'empêcher plusieurs instances de votre application d'être exécutées en même temps : l'une employant App.PrevInstance, l'autre employant des mutex. Nous exposerons ensuite les dangers de telles solutions.

Les deux méthodes exposées sont classiques, mais de nombreuses autres peuvent être implémentées. Celles-ci incluent, par exemple, la recherche de fenêtres sur base de leur titre, l'ouverture d'un port TCP sur la machine locale, la création de clés de registre ou de fichiers, mais l'utilisation de variables partagées pour un serveur d'ActiveX. Il est important, lors de l'utilisation de tels procédés, d'en connaître les limites d'utilisation. Le choix de l'un ou l'autre de ces moyens dépendra principalement du degré de sécurité que vous souhaitez : empêcher deux instances d'un lecteur multimédia pour le confort de l'utilisateur est fort différent d'empêcher les écritures simultanées de données, par des processus concurrents, menant a des corruptions ou a la perte de ces mêmes données.

App.PrevInstance

Vous pouvez employer la propriete PrevInstance de l'objet App qui permet de savoir s'il existe deja une instance de l'application. Cette méthode présente l'avantage d'être particulièrement simple a mettre en oeuvre. Néanmoins, un utilisateur astucieux pourra exécuter plusieurs instances, par exemple en copiant l'exécutable d'un dossier vers un autre.

Sub Main()

    'S'il existe déjà une instance de l'application, on affiche un message
    'd'erreur et le programme se termine
    If App.PrevInstance = True Then
        MsgBox "Ce programme tourne déjà.", vbExclamation, "Erreur"
    Else 'Sinon on affiche le form principal du programme
        frmMain.Show
    End If

End Sub  

Mutex

Un mutex est un objet de synchronisation offert par le système d'exploitation. Lors de l'utilisation de tels objets, il est important de s'assurer que leur utilisation est correcte, faute de quoi la synchronisation pourrait ne pas être réalisée correctement. Pour commencer, nous allons créer un objet qui nous servira a manipuler le mutex plus facilement. Dans un module de classe, ajoutez le code suivant.

Option Explicit

Private Const SYNCHRONIZE = &H100000
Private Const STANDARD_RIGHTS_REQUIRED = &HF0000

Private Const MUTANT_QUERY_STATE = &H1
Private Const MUTANT_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED Or SYNCHRONIZE Or MUTANT_QUERY_STATE)

Private Const MUTEX_MODIFY_STATE = MUTANT_QUERY_STATE
Private Const MUTEX_ALL_ACCESS = MUTANT_ALL_ACCESS

Private Const STATUS_WAIT_0             As Long = &H0&
Private Const STATUS_ABANDONED_WAIT_0   As Long = &H80&
Private Const STATUS_TIMEOUT            As Long = &H102&

Private Const WAIT_FAILED = &HFFFFFFFF
Private Const WAIT_OBJECT_0             As Long = ((STATUS_WAIT_0) + 0)
Private Const WAIT_ABANDONED            As Long = ((STATUS_ABANDONED_WAIT_0) + 0)
Private Const WAIT_ABANDONED_0          As Long = ((STATUS_ABANDONED_WAIT_0) + 0)

Private Const INFINITE                  As Long = &HFFFF&

Private Const ERROR_ALREADY_EXISTS      As Long = &HB7&

Private Declare Function apiCreateMutex _
   Lib "kernel32" _
   Alias "CreateMutexA" _
   ( _
   lpMutexAttributes As Any, _
   ByVal bInitialOwner As Long, _
   ByVal lpName As String _
   ) _
   As Long
Private Declare Function apiReleaseMutex _
   Lib "kernel32" _
   Alias "ReleaseMutex" _
   ( _
   ByVal hMutex As Long _
   ) _
   As Long
Private Declare Function apiOpenMutex _
   Lib "kernel32" _
   Alias "OpenMutexA" _
   ( _
   ByVal dwDesiredAccess As Long, _
   ByVal bInheritHandle As Long, _
   ByVal lpName As String _
   ) _
   As Long
Private Declare Function CloseHandle _
   Lib "kernel32" _
   ( _
   ByVal hObject As Long _
   ) _
   As Long
Private Declare Function WaitForSingleObject _
   Lib "kernel32" _
   ( _
   ByVal hHandle As Long, _
   ByVal dwMilliseconds As Long _
   ) _
   As Long

Private mMutexHandle As Long
Private mMutexOwner As Boolean

'Ouvre un mutex existant et retourne si l'ouverture a ete possible
Public Function OpenMutex(ByVal Name As String) As Boolean
    CloseMutex
    
    mMutexHandle = apiOpenMutex(MUTEX_ALL_ACCESS Or SYNCHRONIZE, 0, Name)
    OpenMutex = (mMutexHandle = 0)
End Function

'Cree un nouveau mutex, ou ouvre un mutex existant du meme nom.
'Si OwnOnCreation est True, acquiert ce mutex si celui-ci est libre. Au retour,
'  OwnOnCreation indique si le mutex a pu etre acquis.
Public Function CreateMutex(Name As String, ByRef OwnOnCreation As Boolean) As Boolean
    CloseMutex

    If OwnOnCreation Then
        mMutexHandle = apiCreateMutex(ByVal 0&, 1, Name)
        
        If mMutexHandle <> 0 Then
            If (Err.LastDllError = ERROR_ALREADY_EXISTS) Then
                OwnOnCreation = False
            Else
                mMutexOwner = True
            End If
        End If
    Else
        mMutexHandle = apiCreateMutex(ByVal 0&, 0, Name)
    End If
    
    CreateMutex = (mMutexHandle <> 0)
End Function

'Acquiert le Mutex apres un temps Timeout (en msec).
'Attention, par defaut ce temps est Infini !
'Retourne si le mutex a pu etre acquis
Public Function Acquire(Optional Timeout As Long = INFINITE) As Boolean
    If IsAttached Then
        If IsOwner Then
            Acquire = True
        Else
            Select Case WaitForSingleObject(mMutexHandle, Timeout)
                Case WAIT_ABANDONED, WAIT_OBJECT_0
                    mMutexOwner = True
                Case Else 'WAIT_TIMEOUT
                    'failed
                    mMutexOwner = False
            End Select
            
            Acquire = mMutexOwner
        End If
    End If
End Function

'Determine si cette instance de la classe est liee (CreateMutex ou OpenMutex) a un mutex.
Public Function IsAttached() As Boolean
    IsAttached = (mMutexHandle <> 0)
End Function

'Determine si cette instance de la classe est en possession du mutex.
Public Function IsOwner() As Boolean
    IsOwner = mMutexOwner
End Function

'Supprime le lien au mutex attache a cette instance.
'Si le mutex a ete acquis, le relache.
Public Sub CloseMutex()
    If IsAttached Then
        If IsOwner Then
            ReleaseMutex
        End If
    
        CloseHandle mMutexHandle
        mMutexHandle = 0
        
    End If
End Sub

'Relache le mutex acquis.
Public Sub ReleaseMutex()
    If IsAttached And mMutexOwner Then
        apiReleaseMutex mMutexHandle
        mMutexOwner = False
    End If
End Sub

Private Sub Class_Terminate()
    CloseMutex
End Sub

Maintenant que cette classe est créée, nous allons l'utiliser dans la form principale de notre projet.

Option Explicit

Private mMutex As CMutex

Private Sub Form_Load()
    Const APP_NAME As String = "Mon Application"
    Set mMutex = New CMutex

    If Not mMutex.CreateMutex("Global\" & APP_NAME & " / multi-instances control mutex", True) Then
        MsgBox "Une erreur s'est produite. Impossible de determiner si l'application s'execute deja ! (" & Err.LastDllError & ")"
        Unload Me
    ElseIf Not mMutex.IsOwner Then
        MsgBox "Une autre instance de cette application est en cours d'execution !"
        Unload Me
    End If
End Sub

Private Sub Form_Unload(Cancel As Integer)
    mMutex.CloseMutex
End Sub

Le préfixe Global\ au nom n'est pas anodin. Celui-ci assure, sur un système sur lequel plusieurs sessions utilisateurs peuvent s'executer parallelement, que le mutex sera considère pour l'entièreté du système et pas uniquement pour la session courante.

La constante APP_NAME rend le nom du mutex unique. Ceci garanti que quel que soit le nom de l'exécutable, ou son chemin d'exécution, l'application n'aura au plus qu'une instance démarrée. On peut aussi envisager d'utiliser App.ExeName qui, bien qu'elle ne garantisse pas l'unicité, permet à l'utilisateur de contourner la protection si la première instance venait à ne plus s'exécuter correctement.

Inconvénients

Le principal inconvénient des applications dont plusieurs processus ne peuvent exister simultanement est qu'elles sont vulnérables aux dénis de service. N'importe quel logiciel malicieux peut prendre la place d'une telle application de sorte a en empêcher le démarrage. Il faut dans un premier temps s'assurer que le design de l'application est correct et requiert effectivement que seule une instance soit présente a la fois. Si c'est le cas, l'objet de synchronisation devrait, au minimum, disposer de droits de sécurité permettant raisonnablement d'éviter ce problème. Par exemple, si le test est réalise sur base de l'existence d'un fichier, seul l'administrateur de la machine, et non les utilisateurs, devrait y avoir accès. Ceci impliquerait aussi que seul l'administrateur puisse utiliser le logiciel. Il est a remarquer que notre implémentation de CMutex utilise le contexte de sécurité de l'utilisateur courant. Il est possible de spécifier plus finement les restrictions a l'aide de l'argument lpMutexAttributes de l'API CreateMutex.

De nombreuses méthodes reposent sur le contenu ou d'un fichier ou d'une clés de registre. Outre le fait que ces méthodes ne puissent pas, de manière inhérente, assurer que deux instances simultanées ne soient pas en cours d'exécution, dans le cas d'un crash, rien ne garanti de pouvoir exécuter a nouveau l'application sans éditer le fichier ou la clé en question. Il faut donc faire attention a l'usage de telles méthodes.

Aller plus loin

Voir aussi :

Date de publication : 07 juillet 2002
Dernière modification : 11 septembre 2008
Rubriques : Généralités
Mots-clés : lancement, empecher, executer, PrevInstance, instance, precedente, unique, unicite, mutex