Prevención de la ejecución múltiple en aplicaciones de Windows — Mutex con nombre y activación en el segundo inicio

· Actualizado el: · · Prevención de ejecución múltiple, Mutex, Windows, .NET, C#, SetForegroundWindow, Remote Desktop, Canalizaciones con nombre, Desarrollo en Windows, Consultoría técnica

«Inicié por error la aplicación de trabajo una segunda vez, edité el mismo archivo en dos ventanas y perdí los cambios de una de ellas»; «una herramienta residente se lanzó por duplicado y terminó procesando el mismo evento dos veces». La ejecución múltiple de una aplicación de escritorio es un tema poco vistoso, pero en la práctica termina siendo, sorprendentemente, causa de incidentes. La base de la solución es sencilla: basta con usar un Mutex con nombre (abreviatura de mutual exclusion, o exclusión mutua; es el objeto del núcleo de Windows que actúa como una «llave» que solo un hilo puede sostener a la vez) para determinar si uno mismo es la primera instancia.

Sin embargo, esta implementación tan simple viene acompañada de trampas colaterales: la trampa del espacio de nombres que hace que la prevención deje de funcionar en un entorno de Escritorio remoto, las reglas de liberación propias del Mutex —distintas de las de otros objetos de sincronización— y el problema de diseño de «cómo poner en primer plano la instancia existente que se detectó», que involucra las restricciones de la ventana en primer plano de Win32. En este artículo repasamos, de principio a fin, desde los fundamentos de la detección de ejecución múltiple con un Mutex con nombre hasta las decisiones de diseño colaterales que resultan necesarias en la práctica.

1. Conclusión principal

  • El patrón básico de la detección de ejecución múltiple es new Mutex(true, name, out bool createdNew). Aunque ya exista un Mutex con el mismo nombre no se produce ninguna excepción: simplemente createdNew pasa a ser false, así que la ramificación se hace según ese valor.1
  • Si no se añade un prefijo al nombre, este se crea de forma predeterminada en el espacio de nombres Local\ (limitado a la sesión). En un entorno de Escritorio remoto donde el mismo usuario tiene varias sesiones, cada sesión trata el Mutex como un objeto distinto y la prevención de ejecución múltiple no funciona. Para limitarlo a una sola instancia a través de las sesiones es imprescindible el prefijo Global\.23
  • Mutex solo puede liberarse desde el mismo hilo que lo adquirió. Si se llama a ReleaseMutex desde otro hilo se produce un ApplicationException. Si el proceso propietario termina sin liberarlo, al siguiente hilo que lo adquiera se le lanza AbandonedMutexException, lo cual es una señal de que la espera en sí se completó correctamente; lo correcto es no ignorarla, sino verificar la integridad del estado antes de usarlo.45
  • Una vez que se sabe que «ya está en ejecución», el verdadero problema es poner en primer plano la ventana de la instancia existente. El sistema operativo restringe las llamadas a SetForegroundWindow que no provienen del proceso en primer plano, así que llamarlo sin más suele fallar y solo consigue que el botón de la barra de tareas parpadee.6 El patrón habitual es ceder, mediante AllowSetForegroundWindow, el permiso «para establecer la ventana en primer plano» que posee el segundo proceso recién iniciado, entregándoselo a la instancia existente.7
  • Para pasar los argumentos de inicio (como la ruta del archivo que debe abrirse) a la instancia existente, el patrón habitual es transferirlos mediante una canalización con nombre. La comparación de los distintos mecanismos queda para «Cómo elegir la comunicación entre procesos en Windows»; este artículo se centra en el diseño de «detección → notificación → activación».
  • Las aplicaciones de consola y los servicios no son el objetivo directo del diseño de este artículo. Un servicio sigue una lógica distinta, porque el SCM ya se encarga de que solo se inicie un servicio con el mismo nombre (capítulo 8).

2. Fundamentos de la detección con un Mutex con nombre

En su uso original, un Mutex es un objeto del núcleo destinado al control de exclusión, que evita que varios hilos o procesos usen el mismo recurso al mismo tiempo. Al crearlo con un nombre, cualquier otro proceso que conozca ese nombre puede hacer referencia al mismo objeto. La detección de ejecución múltiple no aprovecha el control de exclusión en sí, sino únicamente este «nombre compartido».

using var mutex = new Mutex(initiallyOwned: true, name: MutexName, out bool createdNew);
if (!createdNew)
{
    // Ya está en ejecución
    return;
}
// Solo cuando createdNew es true, este hilo posee el Mutex

Conviene tener claros los dos puntos siguientes.

  • Que ya exista un Mutex con el mismo nombre no produce ninguna excepción. createdNew recibe false y simplemente se devuelve una referencia al objeto existente. La ramificación siempre debe hacerse a partir de createdNew.1
  • initiallyOwned: true solo tiene efecto cuando el Mutex se acaba de crear. Si ya existía (createdNew == false), este hilo no se convierte automáticamente en su propietario. Este efecto secundario no supone ningún problema para la detección de ejecución múltiple, pero para evitar el accidente de que «el segundo proceso llame a ReleaseMutex por error», lo más seguro es que, en la rama donde createdNew es false, no se toque el Mutex en absoluto y se termine el procesamiento de inmediato.1

Como norma, el nombre debe ser una cadena que incorpore un GUID propio del producto, para evitar colisiones con aplicaciones de otras empresas (algo como "KomuraSoft.MyApp.SingleInstance.{3F1E2B10-...}"). Igual que ocurre con la ocupación (squatting) de nombres de canalización, el espacio de nombres de los objetos del núcleo con nombre es visible para otros procesos de la máquina, así que tiene sentido usar un nombre difícil de adivinar.

3. Global\ y Local\ — qué ocurre al cruzar sesiones

Los espacios de nombres de objetos del núcleo de Windows se dividen en un espacio independiente para cada sesión y un espacio de nombres global compartido por todo el sistema. Al crear un Mutex con nombre, si no se especifica ningún prefijo, se crea de forma predeterminada en el espacio de nombres de la sesión que lo invoca (Local\), y solo cuando se añade Global\ se crea en el espacio común a todas las sesiones.3 La clase Mutex de .NET reproduce este mismo comportamiento, y la documentación indica explícitamente que «un Mutex con nombre sin prefijo tiene, de forma predeterminada, el prefijo Local\».2

Esto se convierte en un problema en situaciones como las siguientes:

  • Un usuario inicia sesión directamente por consola en un equipo de administración de uso empresarial y, además, se conecta a sí mismo mediante Escritorio remoto en otra sesión (algo habitual en operaciones).
  • El mismo usuario desconecta y vuelve a conectar repetidamente su sesión RDP, y cada vez se le asigna un nuevo ID de sesión.

Si el Mutex se crea manteniendo Local\ (sin prefijo), estas situaciones se tratan como sesiones distintas, y la prevención de ejecución múltiple funciona de forma independiente en cada sesión. Es decir, se produce el defecto de que «aunque sea el mismo usuario, desde la segunda sesión la aplicación se puede iniciar con total normalidad», y la prevención de ejecución múltiple no funciona como se pretendía. Dicho de otro modo, en el uso de escritorio habitual, donde solo se contempla una única sesión, mantener Local\ (el valor predeterminado) no causa ningún problema real.

El diseño general de los equipos donde conviven varios usuarios también se trata en «Introducción a los perfiles de usuario de Windows», pero, centrándonos en el tema de este artículo, hay que tener cuidado. Global\ es un único espacio de nombres compartido por todos los usuarios y todas las sesiones del equipo, y no implica automáticamente «una instancia por usuario». Si simplemente se añade Global\ manteniendo el nombre fijo, como en Global\KomuraSoft.MyApp.SingleInstance, mientras el usuario A tiene la aplicación en ejecución, el inicio del usuario B también quedará bloqueado por el mismo Mutex, lo que en la práctica se convierte en «una instancia para toda la máquina» (la tercera fila de la tabla del capítulo 5). Para lograr «una instancia por usuario, pero agrupando las distintas sesiones de ese mismo usuario», además de Global\ hace falta incrustar en el nombre un identificador propio del usuario (como su SID). Por el contrario, si lo que se busca es limitar a una sola instancia para toda la máquina (compartida entre todos los usuarios, tal como se pretende), basta con mantener Global\ sin incluir el SID.

4. Reglas de liberación del Mutex — el hilo propietario y AbandonedMutexException

El Mutex tiene una restricción que no comparten otros objetos de sincronización como Semaphore o AutoResetEvent: la regla, que impone la identidad del hilo, de que solo puede liberarse desde el mismo hilo que lo adquirió.2 Llamar a ReleaseMutex desde otro hilo produce un ApplicationException («el hilo que llama no posee el mutex»).4

En código que usa async/await de .NET, el procesamiento posterior a un await puede reanudarse en un hilo distinto del grupo de subprocesos. Tenga cuidado, porque un código que «adquiere el Mutex y, justo después, intercala procesamiento asíncrono para liberarlo en la continuación» puede violar esta restricción en silencio. Para el uso de detección de ejecución múltiple, lo más seguro es mantener tanto la adquisición como la liberación dentro de un código corto y síncrono.

Otro punto a tener en cuenta es el abandono (abandoned). Si el hilo propietario de un Mutex termina sin llamar a ReleaseMutex (porque el proceso se bloqueó, cayó por una excepción no controlada, etc.), ese Mutex queda en estado abandonado. Al siguiente hilo que lo adquiera se le lanza AbandonedMutexException, pero esta excepción indica que la espera en sí se completó correctamente y que quien la recibe ya obtuvo la propiedad del Mutex.5 En el uso exclusivo de detección de ejecución múltiple esto normalmente no ocurre (porque, una vez adquirido, se mantiene sin liberar hasta que el proceso termina), pero si el mismo Mutex también se reutiliza para otro control de exclusión, hay que tratarlo de la siguiente manera, teniendo en cuenta que el estado del recurso protegido podría estar corrupto.

try
{
    if (mutex.WaitOne(TimeSpan.FromSeconds(5)))
    {
        // Procesamiento normal
    }
}
catch (AbandonedMutexException)
{
    // La espera se completó correctamente y este hilo ya posee el Mutex.
    // Verifique el estado del recurso protegido antes de usarlo, o reinicialícelo de forma segura
}

La decisión entre «ignorar la excepción, verificar el estado y continuar, o desistir y terminar de forma anómala» sigue el mismo criterio que planteamos en «Tabla de decisión: terminar o continuar ante una excepción inesperada» de este blog. AbandonedMutexException es una excepción que indica explícitamente «el rango que podría estar corrupto», así que la línea base es no ignorarla y verificar únicamente ese rango (los datos protegidos).

Además, en las rutas de finalización normal conviene llamar explícitamente a ReleaseMutex dentro de un finally. Cuando el hilo propietario desaparece porque el proceso termina, el Mutex se trata como «abandonado», así que si no se libera explícitamente, se producirá un AbandonedMutexException innecesario en el siguiente inicio.

5. Tabla de decisión — en qué ámbito usar el Mutex

Según «en qué unidad» se quiera prevenir la ejecución múltiple, el diseño del espacio de nombres y de la canalización cambia.

5.1 Correspondencia entre los tres ámbitos

La forma de asignar el nombre determina «cuántos Mutex se crean», y ese número es, directamente, el límite máximo de instancias que pueden ejecutarse a la vez. Tomando como ejemplo una situación en la que, en el mismo equipo, el usuario A tiene 2 sesiones (consola y RDP) y el usuario B tiene 1 sesión iniciadas, el resultado es el siguiente:

Por máquina — solo prefijo GlobalUn único Mutex del equipoUsuario A, sesión 1Usuario A, sesión 2Usuario B, sesión 3Por usuario — prefijo Global y SID de usuarioMutex del usuario AUsuario A, sesión 1Usuario A, sesión 2Usuario B, sesión 3Mutex del usuario BPor sesión — sin prefijoMutex n.º 1Usuario A, sesión 1Usuario A, sesión 2Mutex n.º 2Usuario B, sesión 3Mutex n.º 3

El lugar donde convergen las flechas hacia la misma casilla es el ámbito que queda limitado a una sola instancia. En este ejemplo, se pueden iniciar hasta 3 instancias en total por sesión, 2 instancias en total por usuario, y 1 sola instancia en total por máquina. La fila que conviene elegir depende de si el requisito es «que la misma persona no pueda abrirla dos veces» o «que en el equipo solo funcione una».

5.2 Tabla de decisión

Ámbito Escenario típico Espacio de nombres del Mutex Transferencia de los argumentos de inicio Puntos a tener en cuenta
Por sesión (predeterminado) Uso de escritorio habitual sin RDP, o cuando basta con «una instancia por sesión» Sin prefijo (= Local\) Además de CurrentUserOnly, incluir el ID de sesión en el nombre de la canalización En un entorno con RDP simultáneo, no se puede evitar el inicio desde otra sesión. Si el mismo usuario tiene varias sesiones, no incluir el ID de sesión en el nombre de la canalización provoca colisiones de nombre fijo (las canalizaciones con nombre quedan fuera del espacio de nombres de sesión del Mutex)
Por usuario (entre sesiones) El mismo usuario alterna entre consola y RDP, o repite conexiones RDP Global\ + SID del usuario incluido en el nombre + ACL explícita con MutexSecurity CurrentUserOnly (al basarse en el SID del usuario, funciona aunque se cruce de sesión) El caso más solicitado en la práctica. Si se usa solo Global\ sin incluir el SID, el comportamiento pasa, sin querer, a ser el de ámbito de máquina (fila siguiente). Como el SID no es información secreta del usuario, en entornos de PC compartido o RDS conviene proteger con ACL previendo también la ocupación del nombre por parte de otros usuarios
Por máquina (entre todos los usuarios) Cuando la licencia limita a una instancia por máquina, o se quiere excluir mutuamente un recurso compartido entre todos los usuarios Global\ (sin información propia del usuario) + ACL explícita con MutexSecurity Se quita CurrentUserOnly y se indican los usuarios permitidos mediante PipeSecurity En entornos de Servicios de Escritorio remoto donde varios usuarios inician sesión al mismo tiempo, este comportamiento suele ser indeseable desde el punto de vista del negocio, así que conviene verificar que coincide con el requisito. No reenvíe sin más los argumentos de inicio de otro usuario a la ventana del primer usuario. Hacerlo puede filtrar rutas de archivo o provocar el incidente de que se abra el documento de otra persona en la sesión de un usuario que no lo esperaba; lo correcto es rechazar las solicitudes de otros usuarios o rediseñar el sistema con un intermediario (broker) sin interfaz de usuario

5.3 Alinear el ámbito de la canalización de notificación con el del Mutex

Las canalizaciones con nombre no están sujetas al espacio de nombres de sesión Local\/Global\ como sí lo está el Mutex, y por defecto son alcanzables entre sesiones. Es decir, aunque no se añada CurrentUserOnly, basta con que coincida el nombre de la canalización para que la conexión llegue a una instancia existente en otra sesión. Aquí, CurrentUserOnly no es una cuestión de alcance, sino de autorización: es un control de acceso que restringe «a quién se le permite conectarse» al usuario actual (y al mismo nivel de elevación). Si no se añade, el descriptor de seguridad predeterminado otorga acceso de lectura incluso a Everyone, de modo que la conexión llega hasta usuarios no previstos. En un diseño que use un Mutex con Global\ + SID para lograr un ámbito «por usuario, entre sesiones», es razonable añadir también CurrentUserOnly a la canalización de notificación, para restringir el origen de la conexión al mismo «solo el usuario objetivo» que el Mutex. Por suerte, PipeOptions.CurrentUserOnly se evalúa por el SID del usuario (y el nivel de elevación), no por el ID de sesión, así que puede combinarse sin problema con la segunda fila de la tabla anterior (por usuario, entre sesiones).

5.4 Ocupación del nombre (squatting) y los límites de la ACL

Cuando se usa un Mutex Global\ de nombre fijo en el ámbito de máquina (tercera fila de la tabla), tenga cuidado con la ocupación del nombre (squatting). Si se intenta limitar la aplicación a una sola instancia mediante un Mutex con nombre, un usuario malicioso puede adelantarse y crear un Mutex con el mismo nombre, impidiendo así el inicio de la aplicación.8 Definir explícitamente la ACL en el momento de la creación con MutexSecurity (MutexAcl.Create) evita que, cuando uno mismo logra crearlo primero, otros usuarios secuestren ese Mutex o lo retengan de forma indebida. Sin embargo, tenga en cuenta que esto es una defensa contra «que lo molesten después», y no impide en sí mismo «que lo ocupen antes». La ACL solo se aplica cuando uno mismo crea el Mutex por primera vez, así que si un usuario malicioso ya había creado un Mutex con ese nombre antes que uno, la llamada propia (por muchas ACL que se hayan preparado) simplemente abrirá el objeto existente que configuró la otra parte, y quedará sujeta a la ACL que esa otra parte haya definido. Que el nombre en sí sea difícil de adivinar sí tiene valor por sí solo, pero si se quiere excluir por completo a un usuario malicioso con capacidad de ejecutar código local, no confíe únicamente en la ocupación del nombre del Mutex y considere combinarlo con otro control de exclusión, como un archivo de bloqueo dentro de un directorio protegido por usuario.9

5.5 La trampa del nivel de elevación — CurrentUserOnly y el nivel de integridad

CurrentUserOnly tiene una restricción fácil de pasar por alto. En Windows, la conexión solo se permite si, además de la cuenta de usuario, coincide también el nivel de elevación (si se está ejecutando o no como administrador).10 Es habitual dar por sentado que «el nombre del Mutex se construye solo a partir del SID del usuario, así que la propia detección funciona sin importar si hay elevación», pero si se usa un Mutex con ACL explícita, este aspecto también se ve afectado por la elevación. Por defecto, Windows asigna una etiqueta de nivel de integridad alto a los objetos creados por un proceso con nivel de integridad alto (ejecutándose como administrador), y deniega el acceso de escritura a procesos con un nivel de integridad más bajo. Como Mutex/MutexAcl.Create de .NET solicitan internamente, además de SYNCHRONIZE y MUTEX_MODIFY_STATE, también DELETE/READ_CONTROL/WRITE_DAC/WRITE_OWNER (STANDARD_RIGHTS_REQUIRED), si el primero se inicia «como administrador» y el segundo con permisos normales, la propia llamada de creación/apertura del Mutex puede lanzar UnauthorizedAccessException. Es decir, se rompe la premisa planteada en el capítulo 7 de que «solo la canalización de notificación es rechazada por la ACL, mientras que la prevención de ejecución múltiple en sí tiene éxito», y la excepción puede saltar en un punto anterior a la detección. Trate también este camino como una señal de que «ya hay otra instancia en ejecución, o el nivel de elevación difiere y no se puede determinar por los medios habituales»: envuelva la propia llamada a Mutex/MutexAcl.Create en un try/catch (UnauthorizedAccessException) y, ante la excepción, renuncie al inicio y termine en silencio (o, igual que en el capítulo 7 con «la notificación puede no llegar», opte por el lado seguro de la prevención de ejecución múltiple). Si además se necesita notificar entre distintos niveles de elevación, no use CurrentUserOnly y cambie a un diseño que arme explícitamente la ACL basada en el SID del usuario mediante PipeSecurity.

6. Poner en primer plano la instancia existente — las limitaciones de SetForegroundWindow

Una vez que, gracias al Mutex, se sabe que «ya está en ejecución», la mayoría de las aplicaciones querrán poner en primer plano la ventana de la instancia existente. Aquí, si simplemente se llama a SetForegroundWindow sobre el proceso existente, en muchos casos falla.

Windows restringe estrictamente qué procesos pueden establecer la ventana en primer plano. Según la documentación oficial, a menos que el proceso que llama cumpla alguna de las siguientes condiciones, SetForegroundWindow no llega a poner realmente la ventana en primer plano, y se limita a hacer parpadear el botón de la barra de tareas.6

  • El propio proceso que llama es actualmente el proceso en primer plano
  • El proceso que llama fue iniciado por el proceso en primer plano
  • El proceso que llama recibió el evento de entrada más reciente
  • No existe actualmente ninguna ventana en primer plano
  • El proceso en primer plano o el proceso que llama se está depurando

En el escenario de detección de ejecución múltiple, la instancia existente (el primer proceso, que se ejecuta en segundo plano) normalmente no cumple ninguna de estas condiciones. En cambio, el segundo proceso —el que el usuario acaba de iniciar con un doble clic— suele encontrarse justo después de recibir el evento de entrada más reciente, y por eso sí tiene el permiso para establecer la ventana en primer plano. Aprovechar esta asimetría es el patrón habitual en la práctica.

AllowSetForegroundWindow es la API mediante la cual un proceso con permiso para establecer la ventana en primer plano cede ese permiso al destinatario que se especifique por ID de proceso.7 Si el segundo proceso cede su propio permiso a la instancia existente y luego solicita la activación mediante una canalización con nombre, la llamada a SetForegroundWindow en el lado de la instancia existente pasa a tener éxito.

No pase aquí ASFW_ANY (-1). La referencia define que, «si este parámetro es ASFW_ANY, todos los procesos podrán establecer la ventana en primer plano».7 Aunque el destinatario deseado es uno solo y ya conocido, se terminaría repartiendo el permiso a todos los procesos. Como este permiso es válido «hasta que el usuario genere la siguiente entrada, o hasta que algún proceso vuelva a llamar a AllowSetForegroundWindow»,7 se abre una ventana justo en el instante en que el usuario inicia la aplicación, en la que un proceso residente sin relación alguna podría robar el foco. En otras palabras, para poner en primer plano la propia aplicación se estaría desactivando temporalmente una protección de todo el sistema.

El destinatario al que hay que pasar el permiso —el ID de proceso de la instancia existente— se puede obtener a partir de la canalización ya conectada. Al pasar el identificador de canalización del lado cliente a GetNamedPipeServerProcessId, se devuelve el ID de proceso del lado servidor de esa canalización.11 Como es «la otra parte con la que uno ya está conectado en este momento», no hace falta buscarla por nombre ni por ventana.

[DllImport("user32.dll")]
private static extern bool AllowSetForegroundWindow(uint dwProcessId);

[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetNamedPipeServerProcessId(
    SafePipeHandle pipe, out uint serverProcessId);

// Se obtiene el ID de proceso del extremo desde la canalización ya conectada
// y se cede el permiso únicamente a ese proceso. Si no se puede obtener,
// se renuncia a poner la ventana en primer plano (la notificación de todos
// modos llega, así que el objetivo principal ya se cumplió)
if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
{
    AllowSetForegroundWindow(serverProcessId);
}

Aun así, quedan casos que no se pueden resolver (por ejemplo, un inicio a través del Programador de tareas, en el que el propio segundo proceso tampoco tiene permiso de primer plano). En esos casos, lo razonable es limitarse a notificar mediante el parpadeo de la barra de tareas y no forzar el paso a primer plano. Como medio de notificación al usuario, conviene siempre llevar a cabo, al menos, una notificación tipo toast o devolver el WindowState de la ventana de Minimized a Normal, y dejar el paso final a primer plano como algo que se hace «si es posible», sin depender de que ocurra siempre.

7. Pasar los argumentos de inicio a la instancia existente — canalizaciones con nombre

Además de la mera detección de ejecución múltiple, es habitual el requisito de que, «si se inicia la aplicación con un argumento indicando el archivo que hay que abrir, la instancia existente abra ese archivo». Para esto también existe la opción de WM_COPYDATA (el mecanismo clásico de enviar datos mediante un mensaje de ventana), pero, frente a la molestia de obtener el identificador de ventana y de serializar el mensaje, lo que se gana es poco, así que en un diseño nuevo lo natural es usar una canalización con nombre.

El diseño es simple: el segundo proceso, que gracias al Mutex ya sabe que «ya está en ejecución», se conecta a la canalización con nombre en la que espera la instancia existente y le envía los argumentos de la línea de comandos serializados, por ejemplo, en JSON. Las precauciones propias de la implementación de las canalizaciones con nombre —como la protección contra la ocupación (squatting) del nombre de la canalización o el control de acceso con CurrentUserOnly— están recogidas en la sección de canalizaciones con nombre de «Cómo elegir la comunicación entre procesos en Windows»; siga esas recomendaciones. El único punto propio del contexto de la detección de ejecución múltiple es el siguiente.

  • Aunque falle el envío de la notificación, se puede considerar que la prevención de ejecución múltiple tuvo éxito. Puede ocurrir que la notificación no llegue por un problema de sincronización, por ejemplo, porque la instancia existente estuviera cerrando el servidor de la canalización durante su finalización. Aun en ese caso, el objetivo principal —«no dejar que se inicie el segundo proceso»— ya se cumplió, así que no hace falta mostrar un cuadro de diálogo de error solo porque falle la notificación.

8. Diferencias con las aplicaciones de consola y los servicios

El diseño de este artículo se basa en aplicaciones de escritorio con ventana. En las aplicaciones de consola o las herramientas por lotes, suele ser más realista diseñar pensando en «que se pueda coexistir de forma segura aunque se ejecute múltiples veces», antes que en «impedir la ejecución múltiple», y esto termina reduciéndose, en esencia, al control de exclusión sobre archivos compartidos.

Los servicios de Windows son un caso todavía distinto. El Administrador de control de servicios (SCM) nunca inicia dos servicios a la vez con el mismo nombre, así que la detección mediante Mutex de este artículo normalmente no es necesaria. En configuraciones del tipo «aplicación de UI + servicio residente», solo la parte de UI usa el diseño de este artículo, y el criterio de ejecución múltiple del lado del servicio es un tema aparte. La forma de construir y diseñar un servicio queda para otro artículo.

9. Ejemplo de implementación — detección con Mutex y solicitud de activación

9.1 Requisitos previos

Antes de entrar en el código, dejemos explícitos los requisitos previos.

Elemento Contenido
Framework de destino Un TFM para Windows de .NET 8 o posterior (por ejemplo, net8.0-windows). Como se usa WPF, el archivo de proyecto necesita <UseWPF>true</UseWPF>
Paquete NuGet System.Threading.AccessControl. MutexAcl.Create y MutexSecurity están en el ensamblado System.Threading.AccessControl.dll de este paquete, que no forma parte de las referencias predeterminadas12
Espacios de nombres usados System.IO.Pipes / System.Runtime.InteropServices / System.Security.AccessControl / System.Security.Principal / System.Text.Json
Sistema operativo de destino Solo Windows. El espacio de nombres Global\, AllowSetForegroundWindow y CurrentUserOnly son mecanismos específicos de Windows
En el caso de WinForms Sustituyendo Application.Current.Dispatcher.Invoke por Control.Invoke y Application.Current.MainWindow por la referencia al formulario correspondiente, el código se puede usar casi sin cambios

El paquete se agrega con dotnet add package System.Threading.AccessControl. Si se opta por prescindir de la ACL explícita y usar directamente new Mutex(...) (primera fila de la tabla de la sección 5.2, ámbito por sesión), este paquete no es necesario.

9.2 Estructura básica — detección, notificación e inicio del servidor

Como el código es largo, primero mostramos solo el flujo general. En esencia, no son más que los siguientes 4 pasos. El contenido de NotifyRunningInstanceAsync, ShowMainWindow y StartActivationServer está en la sección 9.3.

// args son los argumentos de línea de comandos de Main
string mutexName = $@"Global\KomuraSoft.MyApp.SingleInstance.{WindowsIdentity.GetCurrent().User}";
var security = new MutexSecurity();
security.AddAccessRule(new MutexAccessRule(
    WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));

// 1. Detección: determinar mediante createdNew si esta es la primera instancia
var mutex = MutexAcl.Create(
    initiallyOwned: true, name: mutexName, createdNew: out bool createdNew, mutexSecurity: security);

if (!createdNew)
{
    // 2. Notificación: si es la segunda instancia, se envían los argumentos a la existente y se termina
    NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
    return;
}

// 3. Inicio: si es la primera, se muestra la ventana y luego se levanta el servidor de la canalización
ShowMainWindow();
StartActivationServer();

// 4. Cierre: se libera desde el mismo hilo que lo adquirió antes de terminar
mutex.ReleaseMutex();

En el orden, el único punto que no admite excepciones es el paso 3: mostrar la ventana antes de iniciar el servidor de la canalización. Si se invierte el orden, el destinatario de la solicitud de activación que reciba el servidor todavía no existirá, y la notificación desaparecerá en silencio.

9.3 Versión completa

El siguiente código es la estructura básica anterior con todos los puntos señalados en los capítulos 2 a 7 ya reflejados. Está pensado para WPF, pero, tal como indica la tabla de la sección 9.1, con solo hacer las sustituciones ahí descritas también se puede usar casi sin cambios en WinForms.

using System.IO.Pipes;
using Microsoft.Win32.SafeHandles;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text.Json;

public static class Program
{
    // Se incrusta un GUID propio del producto para evitar colisiones de nombre con otras aplicaciones.
    // Como se quiere limitar a 1 instancia "por usuario, entre sesiones", además de Global\ se
    // incrusta el SID del usuario en el nombre. Si se usa solo Global\ sin el SID, todos los usuarios
    // terminan compartiendo el mismo Mutex y el comportamiento pasa a ser "1 instancia para toda la máquina" (capítulos 3 y 5.1)
    private static readonly string MutexName =
        $@"Global\KomuraSoft.MyApp.SingleInstance.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";
    // La canalización de notificación también se alinea con el mismo ámbito que el Mutex (por usuario). Si no se
    // incluye el SID y se deja un nombre fijo, cuando otro usuario levante un servidor con el mismo
    // nombre puede haber colisión o mezcla de mensajes (capítulo 5; CurrentUserOnly solo restringe la ACL, no separa el nombre)
    private static readonly string PipeName =
        $"KomuraSoft.MyApp.Activate.{{3F1E2B10-9C2E-4B7E-8B1E-8B4F2D6A5C10}}.{WindowsIdentity.GetCurrent().User}";

    [STAThread]
    private static void Main(string[] args)
    {
        // Definir explícitamente una ACL que solo otorgue control total al usuario actual
        // evita que, si se logra crear el Mutex primero, otro usuario lo secuestre u obstruya
        // (requiere el paquete NuGet System.Threading.AccessControl).
        // Sin embargo, esto no es una defensa contra "que alguien lo ocupe antes" (sección 5.4)
        var mutexSecurity = new MutexSecurity();
        mutexSecurity.AddAccessRule(new MutexAccessRule(
            WindowsIdentity.GetCurrent().User!, MutexRights.FullControl, AccessControlType.Allow));

        bool createdNew;
        Mutex mutex;
        try
        {
            // Solo cuando createdNew es true esta llamada obtuvo realmente el Mutex
            mutex = MutexAcl.Create(
                initiallyOwned: true, name: MutexName, createdNew: out createdNew, mutexSecurity: mutexSecurity);
        }
        catch (UnauthorizedAccessException)
        {
            // Aunque sea el mismo usuario, si el nivel de elevación difiere, la apertura del Mutex existente
            // puede ser rechazada (sección 5.5). Se interpreta como "ya hay otra instancia con un nivel de
            // elevación distinto" y se renuncia a la notificación, optando por el lado seguro (no iniciar)
            return;
        }
        using var _ = mutex;

        if (!createdNew)
        {
            // Ya está en ejecución. Se notifica a la instancia existente y esta finaliza
            NotifyRunningInstanceAsync(args).GetAwaiter().GetResult();
            return;
        }

        try
        {
            // El StartupUri de App.xaml debe eliminarse. Si se deja, además del MainWindow
            // generado manualmente aquí, la ventana del StartupUri también se genera y
            // muestra automáticamente (y app.MainWindow queda sobrescrito por esa otra), lo que
            // termina abriendo dos ventanas y dejando la solicitud de activación apuntando a la que no corresponde
            var app = new App();
            app.InitializeComponent();
            var mainWindow = new MainWindow();
            app.MainWindow = mainWindow;
            mainWindow.Show();

            // El servidor de la canalización se inicia después de crear y mostrar el MainWindow.
            // En el orden inverso, la solicitud de activación podría llegar cuando la ventana aún no
            // existe y la llamada a ActivateMainWindow podría fallar (la excepción se descarta en el
            // catch-all de más abajo y la notificación simplemente desaparece). Las solicitudes que
            // lleguen en ese intervalo caen en el diseño de mejor esfuerzo de NotifyRunningInstanceAsync
            // ("servidor aún no iniciado → falla de conexión", capítulo 7)
            StartActivationServer();

            app.Run();
        }
        finally
        {
            // Se libera explícitamente desde el hilo propietario (este mismo hilo) antes de terminar.
            // Si se termina sin liberarlo, en el próximo inicio se producirá un
            // AbandonedMutexException innecesario (capítulo 4)
            mutex.ReleaseMutex();
        }
    }

    [DllImport("user32.dll")]
    private static extern bool AllowSetForegroundWindow(uint dwProcessId);

    // Obtiene, a partir de la canalización conectada, el ID de proceso del lado servidor (= la instancia existente)
    [DllImport("kernel32.dll", SetLastError = true)]
    private static extern bool GetNamedPipeServerProcessId(
        SafePipeHandle pipe, out uint serverProcessId);

    private static async Task NotifyRunningInstanceAsync(string[] args)
    {
        try
        {
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
            using var pipe = new NamedPipeClientStream(
                ".", PipeName, PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
            await pipe.ConnectAsync(cts.Token);

            // Este proceso (el segundo que se acaba de iniciar) suele haber recibido un evento de entrada
            // reciente y, por eso, suele tener el permiso para establecer la ventana en primer plano.
            // Ese permiso se cede únicamente al proceso con el que se acaba de conectar —la instancia
            // existente— para que su llamada a SetForegroundWindow tenga éxito (capítulo 6).
            // Si se pasa ASFW_ANY (-1), el permiso se otorga a "todos los procesos"
            if (GetNamedPipeServerProcessId(pipe.SafePipeHandle, out uint serverProcessId))
            {
                AllowSetForegroundWindow(serverProcessId);
            }

            byte[] payload = JsonSerializer.SerializeToUtf8Bytes(new ActivateRequest(1, args));
            await pipe.WriteAsync(payload, cts.Token);
        }
        catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or OperationCanceledException)
        {
            // No se distingue el motivo por el que la notificación no llegó: la instancia existente no
            // respondió porque estaba en proceso de cierre (IOException), o el nivel de elevación
            // difería y la autorización de CurrentUserOnly falló (UnauthorizedAccessException;
            // sección 5.5). En cualquier caso, el objetivo principal de la prevención de ejecución
            // múltiple (no dejar iniciar al segundo proceso) ya se cumplió (capítulo 7)
        }
    }

    private static void StartActivationServer()
    {
        // Límite de un mensaje. Protege la memoria del servidor aunque un ayudante antiguo o un
        // cliente defectuoso del mismo usuario siga enviando datos sin parar
        const int MaxPayloadBytes = 64 * 1024;

        _ = Task.Run(async () =>
        {
            while (true)
            {
                try
                {
                    // La propia construcción también se incluye en el try. Como maxNumberOfServerInstances
                    // es 1, en momentos en que la limpieza de la conexión anterior aún no terminó,
                    // esta línea puede lanzar IOException; si se colocara fuera del try, este único
                    // fallo detendría toda la tarea en segundo plano
                    using var pipe = new NamedPipeServerStream(
                        PipeName, PipeDirection.In, 1,
                        PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);

                    await pipe.WaitForConnectionAsync();

                    // Como maxNumberOfServerInstances es 1, si esta conexión queda atascada con un
                    // cliente que no responde, ya no se podrán aceptar las solicitudes de inicio legítimas
                    // posteriores. Se fija un tiempo límite para cada conexión individual
                    using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
                    using var ms = new MemoryStream();
                    var buffer = new byte[4096];
                    int n;
                    while ((n = await pipe.ReadAsync(buffer, cts.Token)) > 0)
                    {
                        ms.Write(buffer, 0, n);
                        if (ms.Length > MaxPayloadBytes)
                            throw new IOException("El payload superó el tamaño máximo permitido.");
                    }

                    var req = JsonSerializer.Deserialize<ActivateRequest>(ms.ToArray());
                    // Se valida no solo Version sino también Args. Si un emisor de una versión anterior o
                    // un payload malformado hecho a mano envía un JSON sin Args, como {"Version":1},
                    // Args quedará deserializado como null
                    if (req is { Version: 1, Args: not null })
                    {
                        // Las operaciones de UI se realizan volviendo al hilo de la interfaz
                        Application.Current.Dispatcher.Invoke(() => ActivateMainWindow(req.Args));
                    }
                }
                catch (Exception)
                {
                    // Sin importar el motivo —la conexión se cortó, se superó el tiempo o el tamaño límite,
                    // el payload está corrupto, ocurrió una excepción inesperada durante el despacho, etc.—,
                    // se descarta como un fallo de esta única conexión. Si esta tarea muriera,
                    // dejarían de aceptarse todas las notificaciones futuras, así que el bucle siempre continúa.
                    // Sin embargo, para evitar un hot-spin cuando la propia construcción de la
                    // canalización falla de inmediato y de forma repetida antes del await
                    // (por ejemplo, si otro proceso ya tiene tomado el único cupo de instancia), se hace
                    // una pausa obligatoria antes de la siguiente vuelta del bucle
                    await Task.Delay(TimeSpan.FromSeconds(1));
                }
            }
        });
    }

    [DllImport("user32.dll")]
    private static extern bool SetForegroundWindow(IntPtr hWnd);

    private static void ActivateMainWindow(string[] args)
    {
        var window = Application.Current.MainWindow;
        if (window is null) return;

        if (window.WindowState == System.Windows.WindowState.Minimized)
            window.WindowState = System.Windows.WindowState.Normal;
        window.Show();
        window.Activate();

        // El Activate() de WPF llama internamente a SetForegroundWindow, pero puede fallar
        // por la restricción (capítulo 6), así que también se llama explícitamente una vez que ya se hizo AllowSetForegroundWindow
        var hwnd = new System.Windows.Interop.WindowInteropHelper(window).Handle;
        SetForegroundWindow(hwnd);

        if (args.Length > 0)
        {
            // Procesamiento propio de la aplicación, como tratar args[0] como la ruta del archivo que debe abrirse
        }
    }

    private sealed record ActivateRequest(int Version, string[] Args);
}

Añadimos tres aclaraciones sobre las decisiones de diseño.

  • El identificador de usuario que se añade a Global\ debe ser uno cuyo valor no cambie. El SecurityIdentifier que devuelve WindowsIdentity.GetCurrent().User, a diferencia del nombre de usuario, no se ve afectado por un cambio de nombre, y con ToString() se convierte en una cadena con el formato S-1-5-21-....13 Incrustar directamente el nombre de usuario puede provocar que la prevención de ejecución múltiple deje de funcionar tras un cambio de nombre de cuenta o una migración de dominio.
  • La liberación del Mutex y la detención del servidor de la canalización deben formar parte explícita del procesamiento de cierre de la aplicación. En el ejemplo anterior se llama a ReleaseMutex en un finally, pero en una aplicación real conviene, dentro del procesamiento de cierre de la ventana, usar también un token de cancelación para detener el bucle del servidor de la canalización.
  • Incluya el campo version desde el principio. Es bastante probable que, en el futuro, cambie el formato de los argumentos de inicio. Si desde el principio se incluye la regla de «ignorar las versiones desconocidas», el comportamiento seguirá siendo seguro incluso en equipos donde quede un ejecutable de una versión anterior.

10. Procedimiento de verificación

La prevención de ejecución múltiple es una funcionalidad en la que «es difícil notar cuando no está funcionando». Una vez implementada, verifique siempre en el siguiente orden. Los pasos 1 a 3 son obligatorios; a partir del 4, se ejecutan según el ámbito elegido en la tabla de la sección 5.2.

  1. Doble inicio dentro de la misma sesión — Con la aplicación ya en ejecución, haga doble clic otra vez sobre el ejecutable. Si no se abre una segunda ventana y la ventana existente pasa a primer plano, la prueba es correcta. Compruebe también, en la pestaña «Detalles» del Administrador de tareas, que solo queda una fila para el ejecutable en cuestión.
  2. Recuperación desde el estado minimizado — Repita el paso 1 con la primera instancia minimizada. Si permanece minimizada y no se recupera, es que no se está llamando al código de la sección 9.3 de ActivateMainWindow que devuelve el WindowState.
  3. Transferencia de los argumentos de inicio — Desde el símbolo del sistema, inicie una segunda instancia con un argumento, como MyApp.exe C:\temp\sample.txt, y verifique que la instancia existente procesa esa ruta. Si esto no funciona, sospeche de una discordancia en el nombre de la canalización o del orden de inicio del servidor de la canalización (sección 9.2).
  4. Verificación entre sesiones (si eligió el ámbito por usuario o por máquina) — Cree dos sesiones con el mismo usuario, una por consola y otra por Escritorio remoto, e inicie la aplicación desde ambas. El listado y los ID de las sesiones actuales se pueden consultar con el comando qwinsta.14 Si se mantiene sin prefijo, aquí se podrán iniciar dos instancias (capítulo 3). Este es el problema que más se reporta como fallo de la prevención de ejecución múltiple.
  5. Verificación entre usuarios (si eligió el ámbito por máquina) — Cree otra sesión con un usuario distinto y verifique que no puede iniciar la aplicación mientras el primer usuario ya la tiene en ejecución. El resultado depende de si se incluyó o no el SID en Global\, así que contrástelo con el diagrama de la sección 5.1.
  6. Verificación con distinto nivel de elevación — Con la primera instancia iniciada con permisos normales, inicie la segunda «como administrador». Puede caer en la ruta de UnauthorizedAccessException descrita en la sección 5.5; en ese caso, verifique que la segunda instancia termina en silencio (sin mostrar un cuadro de diálogo de error ni bloquearse).
  7. Verificación de un Mutex abandonado — Fuerce el cierre de la primera instancia desde «Finalizar tarea» en el Administrador de tareas y vuelva a iniciar la aplicación. Si se inicia con normalidad, es correcto. Si aquí no se puede iniciar, es que ha quedado algún hilo o proceso reteniendo el Mutex. Si el mismo Mutex también se reutiliza para otro control de exclusión, revise también el tratamiento de AbandonedMutexException (capítulo 4).

Si quiere comprobar visualmente con qué nombre existe realmente el Mutex creado, lo más fiable es abrir el espacio de nombres del Administrador de objetos con WinObj, de Sysinternals, y buscarlo por nombre.15 El descuido típico de «creía haber añadido Global\ pero no lo hice» se detecta así de inmediato.

11. Resumen

La prevención de ejecución múltiple en aplicaciones de Windows es sencilla si solo se mira new Mutex(true, name, out createdNew), unas pocas líneas. Pero para que funcione sin incidentes en la práctica, hace falta dominar todo el conocimiento colateral: la diferencia de visibilidad entre sesiones según Global\/Local\, la restricción de propiedad de hilo propia del Mutex y el tratamiento de AbandonedMutexException, y un diseño de activación que tenga en cuenta las limitaciones de SetForegroundWindow.

En cuanto al orden de implementación, primero decida «en qué unidad (sesión, usuario o máquina) quiere prevenir la ejecución múltiple» (capítulo 5) y, en función de eso, alinee el espacio de nombres del Mutex con el ámbito de la canalización de notificación. A partir de ahí, use el patrón habitual de ceder el permiso mediante AllowSetForegroundWindow para poner en primer plano la instancia existente, y asuma de antemano que, en los casos en que aun así falle, no conviene forzar el paso a primer plano y basta con la notificación; con ese planteamiento, la implementación no se rompe. Si duda entre si el requisito es «una instancia por usuario» o «una instancia para toda la máquina», le recomendamos verificar primero el entorno operativo (si se combina o no con RDP, si hay o no varios usuarios con sesión simultánea).

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa del diseño e implementación de aplicaciones de escritorio para Windows —incluida la prevención de ejecución múltiple y el control de ventanas—, de la investigación de causas de fallos específicos de entornos de Escritorio remoto, y de la revisión de diseño de aplicaciones existentes.

Referencias

  1. Microsoft Learn, Mutex Constructor. Sobre que, cuando ya existe un Mutex con nombre, createdNew pasa a ser false sin producir ninguna excepción, y que la propiedad inicial indicada por initiallyOwned solo tiene efecto cuando createdNew es true.  2 3

  2. Microsoft Learn, Mutex Class. Sobre que un Mutex con nombre sin prefijo especificado tiene, por defecto, el prefijo Local\, la diferencia de visibilidad entre sesiones de Servicios de Terminal Server según Global\/Local\, y que el Mutex impone la propiedad a nivel de hilo (a diferencia de otros objetos de sincronización).  2 3

  3. Microsoft Learn, Kernel Object Namespaces. Sobre la estructura del espacio de nombres independiente por sesión y del espacio de nombres global, y la forma de especificar el espacio de nombres mediante los prefijos Global\/Local.  2

  4. Microsoft Learn, Mutex.ReleaseMutex Method. Sobre que llamar a ReleaseMutex desde un hilo que no es propietario produce un ApplicationException, y que si un hilo termina sin liberar el Mutex, este queda en estado abandonado.  2

  5. Microsoft Learn, AbandonedMutexException Class. Sobre que al siguiente hilo que adquiere un Mutex abandonado se le lanza AbandonedMutexException, y que la espera en sí se completó correctamente, de modo que quien la recibe ya obtuvo la propiedad del Mutex.  2

  6. Microsoft Learn, SetForegroundWindow function. Sobre las condiciones que debe cumplir un proceso para poder establecer la ventana en primer plano, y que, si no las cumple, el efecto se limita a hacer parpadear el botón de la barra de tareas.  2

  7. Microsoft Learn, AllowSetForegroundWindow function. Sobre que un proceso con permiso para establecer la ventana en primer plano puede cederlo al proceso indicado por dwProcessId, que «si este parámetro es ASFW_ANY, todos los procesos podrán establecer la ventana en primer plano», y que el permiso cedido se pierde «cuando el usuario genera la siguiente entrada (salvo que esa entrada vaya dirigida a ese mismo proceso), o cuando algún proceso vuelve a llamar a AllowSetForegroundWindow (salvo que se indique el mismo proceso que la vez anterior)».  2 3 4

  8. Microsoft Learn, CreateMutexW function (synchapi.h). Sobre que, al limitar a una sola instancia mediante un Mutex con nombre, un usuario malicioso puede crear antes un Mutex con el mismo nombre e impedir el inicio de la aplicación, y sobre alternativas como usar un nombre aleatorio o, si se busca una instancia por usuario, un archivo de bloqueo bajo el perfil del usuario. 

  9. Microsoft Learn, Mutexes. Sobre que, como un Mutex de sistema con nombre es visible y global para todo el sistema operativo, se recomienda protegerlo desde su creación con seguridad de control de acceso, y sobre el control de acceso mediante MutexSecurity

  10. Microsoft Learn, PipeOptions Enum. Sobre que, en Windows, CurrentUserOnly verifica también el nivel de elevación además de la cuenta de usuario. 

  11. Microsoft Learn, GetNamedPipeServerProcessId function. Sobre la posibilidad de obtener el identificador de proceso del lado servidor de una canalización con nombre especificada (desde Windows Vista en adelante). 

  12. Microsoft Learn, MutexAcl.Create Method. Sobre que MutexAcl.Create se ofrece en el espacio de nombres System.Threading, el ensamblado System.Threading.AccessControl.dll y el paquete NuGet System.Threading.AccessControl; sobre la forma de especificar el espacio de nombres mediante los prefijos Global\/Local\ y que el valor predeterminado es Local\; y sobre que, como por defecto un Mutex con nombre puede abrirse también por procesos distintos del creador, es necesario pasar MutexSecurity para restringir el acceso. 

  13. Microsoft Learn, WindowsIdentity.User Property. Sobre que esta propiedad devuelve el identificador de seguridad (SID) del usuario, y que el SID identifica de forma única a un usuario o grupo en todas las implementaciones de Windows NT. 

  14. Microsoft Learn, qwinsta. Sobre el comando que muestra el listado de sesiones (nombre, ID y estado) en un host de sesión de Escritorio remoto. 

  15. Microsoft Learn, WinObj - Sysinternals. Sobre que WinObj es una herramienta que muestra el espacio de nombres del Administrador de objetos NT y permite examinar los objetos del núcleo con nombre. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cómo se puede evitar la ejecución múltiple de una aplicación en C#?
El patrón básico es new Mutex(true, name, out bool createdNew). Aunque ya exista un Mutex con el mismo nombre, no se produce ninguna excepción: simplemente createdNew pasa a ser false, así que hay que ramificar según ese valor para terminar el segundo proceso. Como norma, el nombre debe incluir un GUID propio del producto para evitar colisiones con aplicaciones de otras empresas. En la rama donde createdNew es false, lo más seguro es no tocar el Mutex en absoluto y terminar el procesamiento de inmediato.
¿Por qué la prevención de ejecución múltiple no funciona en un entorno de Escritorio remoto?
Es porque, si no se añade un prefijo al nombre del Mutex, este se crea de forma predeterminada en el espacio de nombres Local\ (limitado a la sesión). En un entorno donde el mismo usuario tiene varias sesiones a la vez —por consola y por RDP—, cada sesión trata el Mutex como un objeto distinto, y desde la segunda sesión la aplicación se puede iniciar con total normalidad. Para limitarlo a una sola instancia a través de las sesiones es imprescindible el prefijo Global\. Sin embargo, usar solo Global\ hace que el ámbito pase a ser el equipo completo, compartido por todos los usuarios, así que si se quiere una instancia por usuario también hay que incrustar el SID del usuario en el nombre.
¿Cómo se pone en primer plano la ventana de la instancia existente?
Llamar simplemente a SetForegroundWindow no suele funcionar debido a las restricciones del sistema operativo, y en la mayoría de los casos solo se consigue que parpadee el botón de la barra de tareas. El patrón habitual consiste en ceder, mediante AllowSetForegroundWindow, el permiso para establecer la ventana en primer plano que tiene el segundo proceso —el que el usuario acaba de iniciar con un doble clic— a la instancia existente, y luego solicitar la activación a través de una canalización con nombre. El destinatario se especifica por ID de proceso, y no se debe usar ASFW_ANY (que lo permite a todos los procesos). El ID de proceso de la otra parte se puede obtener con GetNamedPipeServerProcessId a partir de la canalización ya conectada. En los casos en que aun así falla (por ejemplo, un inicio a través del Programador de tareas), lo razonable es no forzar el paso a primer plano y limitarse a una notificación.
¿Cómo se debe manejar AbandonedMutexException?
Cuando el hilo propietario del Mutex termina sin llamar a ReleaseMutex (por ejemplo, si el proceso se bloquea), al siguiente hilo que lo adquiera se le lanza AbandonedMutexException. Esta excepción indica que la espera en sí se completó con éxito y que quien la recibe ya obtuvo la propiedad del Mutex. Lo correcto es no ignorarla, sino verificar la integridad del estado del recurso protegido antes de usarlo. En las rutas de finalización normal conviene llamar explícitamente a ReleaseMutex dentro de un finally, para evitar que se produzca esta excepción de forma innecesaria en el siguiente inicio.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog