Cómo reemplazar un exe o una DLL en uso — Restart Manager y el problema del «archivo en uso» en las actualizaciones automáticas

· Actualizado el: · · Restart Manager, Actualización automática, Instalador, MSI, Windows API, C#, P/Invoke, Distribución

«Cada vez que actualizamos la aplicación, avisamos por megafonía a todo el edificio para que la cierren con la X» o «programamos un lote nocturno que reemplaza el exe en el servidor de archivos, pero por la mañana vemos que falló con “el proceso no puede acceder al archivo”» — en las consultas sobre distribución y actualización de aplicaciones empresariales, este problema del «archivo en uso» aparece con muchísima frecuencia. El mensaje «se requiere reiniciar» que muestran los instaladores al final tiene la misma raíz: la causa de todo es que no se puede reemplazar un archivo que alguien tiene abierto.

Lo complicado es que este problema «solo ocurre de vez en cuando». En la máquina de desarrollo no se reproduce, porque uno mismo cierra la aplicación antes de actualizar, pero en un entorno compartido de producción basta con que una sola persona se haya ido a casa dejando la aplicación abierta para que fracase por completo la actualización nocturna. Y el mensaje de error no dice quién tiene el archivo abierto.

En realidad, Windows tiene un mecanismo estándar del sistema operativo pensado precisamente para este problema: la API Restart Manager, que enumera «quién tiene el archivo abierto», lo cierra de forma ordenada y llega incluso a reiniciarlo después de la actualización. Además, aprovechando que «un exe en ejecución se puede renombrar», es posible diseñar actualizaciones que cambien a la versión nueva en el próximo inicio sin detener el proceso. Este artículo está dirigido a los desarrolladores que sufren el «archivo en uso» en actualizadores automáticos e instaladores de aplicaciones empresariales, y repasa desde el mecanismo del bloqueo hasta el uso de Restart Manager, las buenas prácticas del lado de la aplicación y los patrones de implementación de actualización automática, con el respaldo de la documentación oficial y código de diagnóstico en C#.

1. Conclusiones principales

  • Un exe en ejecución o una DLL cargada no se pueden «sobrescribir» ni «eliminar», pero sí se pueden «renombrar» (mover) dentro del mismo volumen. Renombrar el archivo antiguo con un nombre de reserva y colocar el nuevo en su lugar —el patrón rename-then-replace— es la forma básica de un actualizador propio.
  • Restart Manager es una API estándar del sistema operativo desde Windows Vista, creada específicamente para el «problema del archivo en uso». Al registrar el archivo que se va a actualizar, enumera las aplicaciones y servicios que lo tienen abierto, y se encarga de cerrarlos e incluso de reiniciarlos. Su propósito es reducir o eliminar la necesidad de reiniciar el sistema operativo.1
  • El flujo de la API es RmStartSession → RmRegisterResources → RmGetList → RmShutdown → (actualización) → RmRestart → RmEndSession. El cierre se hace en el orden aplicaciones GUI → aplicaciones de consola → servicios → Explorador, y el reinicio en el orden inverso.12
  • Con solo RmGetList ya se puede construir una herramienta de diagnóstico que muestre «quién tiene el archivo abierto». Con P/Invoke desde C# son apenas unas decenas de líneas (véase el código más adelante).3
  • MSI (Windows Installer 4.0 y posteriores) usa Restart Manager automáticamente. Si se incluye el cuadro de diálogo MsiRMFilesInUse en el paquete, se le puede ofrecer al usuario la opción de «cerrar la aplicación automáticamente y reiniciarla».4
  • La aplicación también tiene su propio protocolo que cumplir. Registrar la línea de comandos de reinicio con RegisterApplicationRestart, responder a WM_QUERYENDSESSION (lParam=ENDSESSION_CLOSEAPP) y, en WM_ENDSESSION, guardar los datos sin guardar antes de cerrar. Una aplicación que implemente estos tres puntos «se cierra para la actualización, pero vuelve a levantarse tal como estaba» después de ella.56
  • Para evitar bucles de reinicio, una aplicación que lleve menos de 60 segundos iniciada no se reinicia. Además, el tiempo de espera para el cierre forzado de Restart Manager es de 30 segundos para las aplicaciones y 20 segundos para los servicios.62
  • Cuando de ninguna manera se puede reemplazar el archivo, el último recurso es MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT (una reserva de reemplazo para el próximo reinicio del sistema operativo). Requiere privilegios de administrador y la reserva se registra en PendingFileRenameOperations, dentro del Registro.7

2. Por qué no se puede reemplazar un archivo en uso — y la salida de que «el renombrado sí funciona»

Cuando Windows ejecuta un exe o carga una DLL, el sistema operativo asigna ese archivo como un archivo mapeado en memoria (una sección de imagen). Por cómo funciona la paginación, el contenido real del archivo sigue siendo referenciado mientras se ejecuta, así que se bloquean tanto la reescritura del contenido como su eliminación. Es el comportamiento típico: el Explorador dice «otro proceso está usando el archivo» al intentar copiarlo, File.Copy devuelve IOException (violación de uso compartido) y File.Delete devuelve UnauthorizedAccessException.

Aquí entra en juego una propiedad de Windows poco conocida. Aunque el «contenido» del archivo esté bloqueado, el «nombre» en el directorio se puede cambiar. Incluso con un exe en ejecución, renombrarlo (moverlo) dentro del mismo volumen tiene éxito, porque lo que retiene la imagen en ejecución es el contenido del archivo, no su ruta o nombre.

De esta asimetría se deriva el patrón rename-then-replace, la forma básica de una actualización propia.

1. Renombrar MyApp.exe (en ejecución) a MyApp.exe.old   ← funciona aunque esté en ejecución
2. Colocar el nuevo MyApp.exe en la ruta original        ← funciona porque el nombre quedó libre
3. Desde el próximo inicio se usa el nuevo MyApp.exe
4. Tras finalizar el proceso anterior, eliminar el .old en algún momento
   (no se puede eliminar mientras está en ejecución; se limpia en la próxima actualización o al iniciar)

El valor de este patrón está en lograr «la versión nueva desde el próximo inicio» sin detener el proceso, y actualizadores como el de Chrome o marcos de trabajo como Squirrel/Velopack se apoyan, en esencia, en esta misma propiedad. Hay tres puntos a tener en cuenta: el renombrado solo funciona dentro del mismo volumen (mover a otro volumen se convierte en copiar + eliminar, y la eliminación falla); hay que incluir en el diseño la limpieza del archivo .old; y el «proceso antiguo que sigue en ejecución» continúa siendo la versión anterior, por lo que existe un período en el que conviven procesos nuevos y antiguos.

Además, si lo que se necesita es averiguar manualmente «quién tiene abierto el archivo» como parte de una investigación de incidentes, el camino más rápido es buscar identificadores y DLL cargadas con Process Explorer o el comando Handle. El procedimiento está reunido en el artículo hermano publicado el mismo día, «Rastrear cuelgues y fugas con Process Explorer, Handle y VMMap». En este artículo usamos Restart Manager desde el propio programa, es decir, como un método que se integra en el propio actualizador.

3. El funcionamiento de Restart Manager — de RmStartSession a RmRestart

Restart Manager es el conjunto de API (rstrtmgr.dll) incluido de forma estándar desde Windows Vista / Windows Server 2008, y su propósito está escrito con claridad al inicio de la documentación oficial: «la razón principal por la que una instalación o actualización exige reiniciar el sistema es que el archivo que se va a actualizar está siendo usado por una aplicación o un servicio en ejecución, y Restart Manager reduce o elimina esa necesidad cerrando y reiniciando las aplicaciones y servicios no críticos».1 Seguramente ha visto, durante la instalación de un paquete MSI, un cuadro de diálogo con una lista de procesos que dice «las siguientes aplicaciones tienen archivos en uso…»; productos de gran tamaño como Visual Studio u Office también son usuarios de este mecanismo en sus instaladores y actualizaciones.

El flujo de la API es lineal.

Paso Función Qué hace
1 RmStartSession Inicia la sesión y obtiene el identificador y la clave de sesión (cadena GUID)
2 RmRegisterResources Registra las rutas de archivo a actualizar (también admite nombres de proceso y servicio)
3 RmGetList Enumera las aplicaciones y servicios que están usando los recursos registrados3
4 RmShutdown Los cierra (de forma normal ordenada, o forzada si se indica)2
5 En este intervalo se reemplaza el archivo
6 RmRestart Reinicia las aplicaciones que están registradas para reiniciarse
7 RmEndSession Cierra la sesión

«En qué momento se reemplaza el archivo» es el punto donde más fácil es equivocarse, así que lo ordenamos en una línea de tiempo. Solo se puede reemplazar el archivo en el breve instante después de que RmShutdown retorna y antes de llamar a RmRestart.

Aplicación en uso(GUI/servicio)Restart Manager(rstrtmgr.dll)ActualizadorAplicación en uso(GUI/servicio)Restart Manager(rstrtmgr.dll)ActualizadorAquí se determina «quién lo tiene abierto»Si RebootReasons≠0, se necesita reiniciar el SO⑤ Aquí se reemplaza el archivo← el bloqueo solo se libera en este intervalo① RmStartSessionIdentificador de sesión + clave de sesión② RmRegisterResources(archivo a actualizar)③ RmGetListLista de procesos en uso + RebootReasons④ RmShutdownWM_QUERYENDSESSION / WM_ENDSESSION(en consola: CTRL_C_EVENT)Guarda y finalizaCierre completado⑥ RmRestartReinicia las apps registradas en orden inverso⑦ RmEndSession

Hay varias especificaciones importantes que conviene tener presentes.

  • Orden de cierre y de reinicio. Se detiene en el orden aplicaciones GUI → aplicaciones de consola → servicios → Explorador, y tras la actualización se reinician las aplicaciones registradas en orden inverso.1
  • La forma de cerrar sigue un «orden de buenos modales». A las aplicaciones GUI se les envía WM_QUERYENDSESSION/WM_ENDSESSION (lParam=ENDSESSION_CLOSEAPP), y a las que no responden también se les envía WM_CLOSE. A las aplicaciones de consola se les envía CTRL_C_EVENT, y los servicios se detienen a través del SCM. Incluso al especificar RmForceShutdown, primero se intenta un cierre ordenado y luego, a los 30 segundos (20 para los servicios), se fuerza el cierre de las aplicaciones que no respondieron.52
  • Solo se pueden reiniciar las aplicaciones registradas con RegisterApplicationRestart. Si se especifica RmShutdownOnlyRegistered en RmShutdown, se obtiene un comportamiento más seguro: «cerrar únicamente cuando todas las aplicaciones estén registradas para reiniciarse».25
  • No se puede cerrar aplicaciones que estén en otra sesión. Un instalador que se ejecuta como servicio con LocalSystem no puede cerrar ni reiniciar aplicaciones que corren en una sesión de usuario. Este es el punto donde más se tropieza al diseñar actualizaciones nocturnas sin supervisión (las soluciones concretas se ven en el siguiente apartado).2
  • Los servicios críticos del sistema y los procesos críticos quedan fuera del alcance. En ese caso se devuelve un resultado que indica que se necesita reiniciar el sistema operativo (RM_REBOOT_REASON).13

3.1 Cómo cruzar el límite de sesión

Esta es la restricción con la que siempre se choca al diseñar «lanzar la actualización desde un servicio durante la noche». Desde LocalSystem (sesión 0) no se pueden cerrar ni reiniciar las aplicaciones de una sesión de usuario que haya iniciado sesión.2 La solución no consiste en «extender la mano desde la sesión 0», sino, en definitiva, en colocar manos y pies dentro de la sesión de destino. Hay tres opciones realistas.

Método Qué hace Para qué sirve / no sirve
(A) Iniciar con el Programador de tareas como el usuario objetivo Registra la tarea del agente de actualización con la cuenta del usuario objetivo, como «ejecutar solo cuando el usuario haya iniciado sesión», y la lanza con la señal de actualización Es lo más sencillo. Adecuado para equipos internos donde se puede identificar al usuario que tiene la sesión iniciada
(B) Iniciar un agente residente al iniciar sesión Ejecuta un pequeño proceso residente en cada sesión de usuario, que espera la señal de actualización mediante una carpeta compartida o un evento Funciona aunque el usuario objetivo sea indeterminado o haya varias sesiones simultáneas. Tiene el costo de mantenimiento del proceso residente
(C) Que el servicio levante un proceso en la sesión del usuario El servicio con LocalSystem obtiene el token de la sesión objetivo y usa ese token para iniciar el actualizador Es el de mayor libertad, pero también el más pesado en implementación y gestión de privilegios

La opción (A) se registra con schtasks de esta manera. El punto clave es indicar el usuario objetivo en /RU y añadir /IT para que la tarea se ejecute de forma interactiva únicamente cuando ese usuario tenga la sesión iniciada.

:: Registra una tarea que ejecuta el agente de actualización "dentro de la sesión del usuario objetivo"
:: Ajuste la especificación de credenciales (/RP) o la sobrescritura de tareas existentes (/F) según la política del entorno
schtasks /create /TN "MyApp Update Agent" /TR "C:\App\Updater.exe /shutdown-and-update" ^
         /SC ONCE /ST 02:00 /RU CORP\taro /IT

:: Desde el lote nocturno también se puede ejecutar de inmediato, sin esperar la hora programada
schtasks /run /TN "MyApp Update Agent"

Si se opta por (C), se obtiene el ID de la sesión objetivo con WTSGetActiveConsoleSessionId o WTSEnumerateSessions, se toma el token principal de ese usuario con WTSQueryUserToken y se inicia el actualizador con CreateProcessAsUser. Llamar a WTSQueryUserToken requiere estar ejecutándose con la cuenta LocalSystem y contar con el privilegio SE_TCB_NAME, y la documentación oficial deja claro que está pensado «para servicios de alta confianza» y que hay que «tener cuidado de no filtrar el token y cerrar siempre el identificador cuando se termine de usar».8 Un manejo incorrecto puede convertirse en un agujero de escalamiento de privilegios, así que no hace falta forzar esta opción si (A) o (B) resultan suficientes.

Con cualquiera de los métodos, la estructura es la misma: el proceso que corre dentro de la sesión es el que se encarga de todo, desde RmStartSession hasta RmRestart. El servicio de la sesión 0 se limita a «distribuir el archivo de actualización» y «dar la señal».

También conviene aclarar la relación con MSI. A partir de Windows Installer 4.0, el MSI usa Restart Manager automáticamente. El comportamiento predeterminado es «en lugar de reiniciar el sistema operativo, cerrar y reiniciar la aplicación si es posible». Lo que puede hacer el autor del paquete es: añadir el cuadro de diálogo MsiRMFilesInUse, que en la instalación con interfaz completa le ofrece al usuario la opción de «cerrar la aplicación automáticamente y reiniciarla» (en versiones antiguas de Installer se recurre al cuadro de diálogo tradicional FilesInUse); controlar el comportamiento con propiedades como MSIRESTARTMANAGERCONTROL; y, desde una acción personalizada, registrar recursos adicionales llamando a RmJoinSession a través de la propiedad MsiRestartManagerSessionKey (esta acción personalizada debe colocarse antes de la acción InstallValidate, donde se detectan los archivos en uso). En las instalaciones silenciosas siempre se usa Restart Manager y la aplicación se cierra automáticamente.4

En otras palabras, si se distribuye mediante MSI, casi nunca hace falta escribir «código que llame a Restart Manager»: lo esencial es preparar las buenas prácticas del lado de la aplicación (que se ven dos secciones más adelante). Llamar directamente a esta API solo cobra sentido cuando se escribe un actualizador propio. La elección del propio método de distribución se trata en «Cómo elegir el método de distribución de una aplicación de Windows: MSI/MSIX/ClickOnce/xcopy/actualizador propio».

4. Mostrar «quién tiene el archivo abierto» con C# — código de diagnóstico con RmGetList

Dentro de Restart Manager, RmGetList es útil incluso para quien no va a escribir la lógica de actualización. Como la API permite obtener «la lista de procesos y servicios que están usando este archivo», es posible cambiar el mensaje de error del actualizador de «el archivo está en uso» a «lo tiene abierto MyApp.exe (PID 4132) en el equipo de contabilidad».

4.1 El esqueleto — solo se llaman cuatro funciones

Si se dejan de lado las declaraciones P/Invoke y las definiciones de estructuras, el cuerpo del proceso se reduce a esto. Conviene tener presente esta forma antes de leer la versión completa que sigue.

// [Esqueleto] Las definiciones de estructuras y las declaraciones P/Invoke están en la versión completa de 4.3
var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);

// ① Iniciar la sesión
int rc = RmStartSession(out uint session, 0, sessionKey);
if (rc != 0) throw new Win32Exception(rc);
try
{
    // ② Registrar los archivos a comprobar (deben ser rutas completas)
    var fullPaths = Array.ConvertAll(args, Path.GetFullPath);
    rc = RmRegisterResources(session, (uint)fullPaths.Length, fullPaths, 0, null, 0, null);
    if (rc != 0) throw new Win32Exception(rc);

    // ③ Enumerar los procesos/servicios que están usando los archivos
    //    Si el búfer no alcanza, se devuelve ERROR_MORE_DATA con el tamaño necesario; hay que reservar y reintentar
    uint count = 0;
    RM_PROCESS_INFO[] apps = null;
    while (true)
    {
        rc = RmGetList(session, out uint needed, ref count, apps, out uint reasons);
        if (rc == 0) break;
        if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc);
        count = needed;
        apps = new RM_PROCESS_INFO[needed];
    }

    // apps[0] a apps[count-1] contienen "quién lo tiene abierto"
}
finally
{
    RmEndSession(session);   // ④ Cerrar siempre la sesión
}

4.2 Qué devuelve — la forma de la salida y RM_APP_TYPE

Al ejecutar la versión completa (4.3) como WhoLocks.exe C:\App\MyApp.exe, la salida tiene esta forma. Los valores son un ejemplo con fines explicativos; el contenido real varía según el entorno.

Procesos/servicios en uso: 2 (RebootReasons=0)
  PID=4132   Tipo=1 Reiniciable=True  Sesión=2 Nombre=MyApp Servicio=
  PID=6284   Tipo=3 Reiniciable=False Sesión=0 Nombre=MyAppAgent Servicio=MyAppAgent

Se lee en tres claves. El Tipo (RM_APP_TYPE) determina cómo se cierra, si Reiniciable (bRestartable) es False no se levantará automáticamente después de la actualización, y si la Sesión (TSSessionId) es distinta de la propia, se choca con la restricción del capítulo 3 de “no se puede cruzar la sesión”.

Los valores numéricos del tipo y su significado son los siguientes.9

Valor Nombre Significado
0 RmUnknownApp Aplicación que no encaja en ninguna otra categoría. Solo se puede detener con un cierre forzado
1 RmMainWindow Aplicación de Windows que corre como proceso independiente y tiene una ventana de nivel superior
2 RmOtherWindow Aplicación de Windows que no es un proceso independiente ni tiene ventana de nivel superior
3 RmService Servicio de Windows
4 RmExplorer Explorador de Windows
5 RmConsole Aplicación de consola independiente
1000 RmCritical No se puede detener, así que la instalación necesita un reinicio del sistema operativo para completarse. Puede deberse a un proceso crítico, a privilegios insuficientes o a que es el propio proceso que inició Restart Manager

En la práctica, en cuanto aparece 0 o 1000 se puede concluir que «no es posible una actualización ordenada». Con 1 o 5 se puede cerrar mediante el protocolo del capítulo 3 (WM_QUERYENDSESSION / CTRL_C_EVENT), y con 3 se detiene a través del Administrador de control de servicios.

4.3 Versión completa

Escrito con P/Invoke desde C#, queda así (funciona tanto en .NET Framework 4.8 como en .NET 8).

// WhoLocks.cs ── enumera con Restart Manager "quién tiene abierto" el archivo indicado
// Uso: WhoLocks.exe C:\App\MyApp.exe C:\App\MyLib.dll
using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;

internal static class Program
{
    private const int CCH_RM_SESSION_KEY = 32;      // la clave de sesión es una cadena GUID (32 caracteres + terminador)
    private const int CCH_RM_MAX_APP_NAME = 255;
    private const int CCH_RM_MAX_SVC_NAME = 63;
    private const int ERROR_MORE_DATA = 234;

    [StructLayout(LayoutKind.Sequential)]
    private struct RM_UNIQUE_PROCESS
    {
        public uint dwProcessId;
        public System.Runtime.InteropServices.ComTypes.FILETIME ProcessStartTime;
    }

    // RM_APP_TYPE: 1=app GUI, 2=otra ventana, 3=servicio, 4=Explorer, 5=consola, 1000=crítico

    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
    private struct RM_PROCESS_INFO
    {
        public RM_UNIQUE_PROCESS Process;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_APP_NAME + 1)]
        public string strAppName;
        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = CCH_RM_MAX_SVC_NAME + 1)]
        public string strServiceShortName;
        public int ApplicationType;
        public uint AppStatus;
        public uint TSSessionId;
        [MarshalAs(UnmanagedType.Bool)]
        public bool bRestartable;
    }

    // Las API de Restart Manager devuelven el código de error de Win32 como valor de retorno, no vía GetLastError
    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    private static extern int RmStartSession(
        out uint pSessionHandle, int dwSessionFlags, StringBuilder strSessionKey);

    [DllImport("rstrtmgr.dll")]
    private static extern int RmEndSession(uint dwSessionHandle);

    [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)]
    private static extern int RmRegisterResources(uint dwSessionHandle,
        uint nFiles, string[] rgsFileNames,
        uint nApplications, RM_UNIQUE_PROCESS[] rgApplications,
        uint nServices, string[] rgsServiceNames);

    [DllImport("rstrtmgr.dll")]
    private static extern int RmGetList(uint dwSessionHandle,
        out uint pnProcInfoNeeded, ref uint pnProcInfo,
        [In, Out] RM_PROCESS_INFO[] rgAffectedApps, out uint lpdwRebootReasons);

    private static void Main(string[] args)
    {
        if (args.Length == 0)
        {
            Console.Error.WriteLine("Uso: WhoLocks <ruta de archivo> ...");
            Environment.Exit(2);
        }

        var sessionKey = new StringBuilder(CCH_RM_SESSION_KEY + 1);
        int rc = RmStartSession(out uint session, 0, sessionKey);
        if (rc != 0) throw new Win32Exception(rc, $"Fallo en RmStartSession (rc={rc})");
        try
        {
            // Por contrato de la API, los nombres de archivo registrados deben ser rutas completas.
            // Se normalizan antes de registrar para que funcione también con rutas relativas
            var fullPaths = Array.ConvertAll(args, Path.GetFullPath);
            rc = RmRegisterResources(session, (uint)fullPaths.Length, fullPaths, 0, null, 0, null);
            if (rc != 0) throw new Win32Exception(rc, $"Fallo en RmRegisterResources (rc={rc})");

            // Se consulta el tamaño necesario → se reserva el arreglo → se obtiene la lista. Si entre
            // ambas llamadas aparecen más procesos, vuelve a devolver ERROR_MORE_DATA; por eso se reintenta en bucle
            uint count = 0;
            uint rebootReasons;
            RM_PROCESS_INFO[] apps = null;
            while (true)
            {
                rc = RmGetList(session, out uint needed, ref count, apps, out rebootReasons);
                if (rc == 0) break;
                if (rc != ERROR_MORE_DATA) throw new Win32Exception(rc, $"Fallo en RmGetList (rc={rc})");
                count = needed;
                apps = new RM_PROCESS_INFO[needed];
            }

            Console.WriteLine($"Procesos/servicios en uso: {count} (RebootReasons={rebootReasons})");
            for (int i = 0; i < count; i++)
            {
                RM_PROCESS_INFO a = apps[i];
                Console.WriteLine(
                    $"  PID={a.Process.dwProcessId,-6} Tipo={a.ApplicationType} " +
                    $"Reiniciable={a.bRestartable} Sesión={a.TSSessionId} " +
                    $"Nombre={a.strAppName} Servicio={a.strServiceShortName}");
            }
        }
        finally
        {
            RmEndSession(session);   // la sesión debe liberarse siempre en el finally
        }
    }
}

Hay tres puntos clave. Primero, las API de Restart Manager devuelven directamente el código de error de Win32 como valor de retorno, así que no se usa GetLastError (no hace falta SetLastError = true en DllImport). Segundo, RmGetList sigue la convención de llamada de «si el búfer no alcanza, devuelve ERROR_MORE_DATA (234) junto con el tamaño necesario», por lo que se escribe como un bucle de consultar → reservar → obtener.3 Tercero, si lpdwRebootReasons es distinto de 0 (RmRebootReasonNone), significa que «cerrar la aplicación no basta y se necesita reiniciar el sistema operativo».3 Para la forma de serializar las cadenas de la estructura con ByValTStr y, en general, para escribir este tipo de P/Invoke de forma segura, consulte «Cómo llamar a la API de Win32 de forma segura desde C# — guía práctica de P/Invoke».

Si a este código se le añaden las dos llamadas P/Invoke de RmShutdown y RmRestart, se obtiene el esqueleto de un actualizador propio con el flujo «enumerar → cerrar de forma ordenada → reemplazar → reiniciar». Sin embargo, tenga en cuenta que RmShutdown solo debe llamarlo el proceso que hizo su propio RmStartSession, y que no debe llamarse desde una acción personalizada de MSI, porque es el propio MSI el que gestiona la sesión.4

5. Buenas prácticas del lado de la aplicación — el conjunto de tres puntos para «cerrarse con orden y volver a levantarse tal como estaba»

Ni Restart Manager ni MSI matan el proceso a la fuerza con terminate sin más trámite (salvo que se especifique el cierre forzado). Si la aplicación no responde, al final se termina molestando al usuario con el cuadro de diálogo del “archivo en uso”. Para que una aplicación empresarial «resista bien las actualizaciones», hay que implementar en ella el siguiente conjunto de tres puntos.5

(1) Registrar el reinicio con RegisterApplicationRestart. Se llama una vez, justo después de iniciar. Restart Manager solo puede reiniciar aplicaciones registradas, y esta es la única «forma de indicarle al sistema operativo la línea de comandos para el reinicio». Las especificaciones principales son: no incluir el nombre del exe en la línea de comandos (el sistema operativo lo añade), una longitud máxima de RESTART_MAX_CMD_LINE, y que durante los primeros 60 segundos tras el inicio no se reinicia, para evitar bucles de reinicio. Si no hace falta reiniciar ante un bloqueo o un cuelgue, se puede limitar a «reiniciar solo en caso de actualización» especificando RESTART_NO_CRASH RESTART_NO_HANG.6
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int RegisterApplicationRestart(string commandLine, int flags);

// Se registra al iniciar. El indicador "/restored" permite bifurcar hacia la restauración tras reiniciar
// RESTART_NO_CRASH(1) | RESTART_NO_HANG(2) = reiniciar solo si el cierre lo provocó la actualización
RegisterApplicationRestart("/restored", 1 | 2);

(2) Responder a WM_QUERYENDSESSION (lParam=ENDSESSION_CLOSEAPP). Antes de cerrar, Restart Manager pregunta con este mensaje si «se puede cerrar». En este punto todavía no hay que cerrar: si ya está todo listo, se devuelve TRUE (porque es posible que otras aplicaciones aún no lo estén). Devolver FALSE permite cancelar el apagado, pero como en el cierre forzado igualmente se termina cerrando, conviene evitar diseños que dependan de FALSE. Este momento es también la última oportunidad para volver a llamar a RegisterApplicationRestart durante una actualización. Si se graba en los argumentos de la línea de comandos el estado necesario para la restauración (por ejemplo, el ID del formulario que estaba abierto) y se vuelve a registrar, se puede regresar a «la pantalla original» tras el reinicio.56

(3) En WM_ENDSESSION, guardar los datos sin guardar y terminar. La orden real de cierre llega con WM_ENDSESSION (wParam=TRUE, lParam=ENDSESSION_CLOSEAPP). Dentro del tiempo de espera (30 segundos para la aplicación en el cierre forzado), hay que completar el guardado automático de los datos sin guardar, la serialización del estado de trabajo y el cierre de las conexiones antes de terminar. La guía oficial recomienda, ante todo, «guardar los datos del usuario de forma periódica». En el caso de una aplicación de consola, en lugar de WM_ llega CTRL_C_EVENT, así que se hace lo mismo con SetConsoleCtrlHandler (en C#, Console.CancelKeyPress).52

En WPF/WinForms, el punto de entrada son Application.SessionEnding o Form.FormClosing (CloseReason.WindowsShutDown), pero si se necesita distinguir el indicador ENDSESSION_CLOSEAPP (la diferencia entre un reinicio del sistema operativo y «cerrar solo la aplicación para reiniciarla después»), hace falta interceptar el procedimiento de ventana. Además, para que el proceso posterior al reinicio no choque con «su propia instancia anterior que aún está terminando», revise también el diseño del Mutex de prevención de instancias múltiples («Prevención de instancias múltiples en aplicaciones de Windows»).

Cuando se reúnen estos tres puntos, la experiencia de una actualización MSI pasa de «avisar por megafonía a todos para que cierren» a «cuando se ejecuta la actualización, la aplicación se cierra automáticamente y, tras la actualización, vuelve a la pantalla original de forma automática». Es el mismo mecanismo que hace que Office «vuelva a abrir el documento tras reiniciar».

6. Patrones de implementación de la actualización automática — proceso separado, rename-then-replace y reemplazo al reiniciar

Repasemos los patrones de diseño para construir una actualización automática propia. Hay una única premisa fundamental: un exe en ejecución no se puede sobrescribir a sí mismo, así que el proceso de actualización tiene que escapar en algún punto hacia «otro proceso» o «otro nombre».

Patrón A: actualizador en proceso separado + esperar y reemplazar. Cuando la aplicación principal detecta la actualización, inicia el exe del actualizador (o una copia temporal de él) y ella misma termina. El actualizador espera a que termine el proceso principal (Process.WaitForExit o esperar la liberación de un Mutex), reemplaza el archivo y reinicia la aplicación principal. Es un enfoque sencillo y confiable, pero para los casos en que «la aplicación principal tarda en terminar» o «hay muchos procesos que esperar en una configuración de varios procesos», resulta útil combinarlo con confirmar quién ocupa el archivo mediante el RmGetList de la sección anterior y cerrarlo con RmShutdown.

Patrón B: rename-then-replace (actualización que no detiene la aplicación). Como se vio en la sección 2, se renombra el exe/DLL en ejecución, se coloca la versión nueva y se logra «la versión nueva desde el próximo inicio». La ventaja es que no interrumpe el trabajo del usuario, por lo que es adecuado para aplicaciones empresariales de tipo residente o de funcionamiento prolongado. Marcos de trabajo de actualización para .NET como Squirrel o Velopack también aprovechan esta propiedad, mediante carpetas separadas por versión y el reemplazo del exe de entrada, y ofrecen todo el marco necesario para «aplicar la actualización en segundo plano y usar la versión nueva desde el próximo inicio». Si se implementa por cuenta propia, el procedimiento se reduce a: descargar → verificar → extraer → renombrar como reserva → colocar → limpiar el archivo de reserva en el siguiente inicio.

Patrón C: MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT (reemplazo al reiniciar). Para casos como el proceso host de un servicio o una extensión de shell, donde «de ninguna manera se puede detener ni escapar tampoco con el renombrado», el último recurso es reservar el reemplazo para el próximo reinicio del sistema operativo. El contenido de la reserva se registra en HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations, dentro del Registro, y se ejecuta en el orden de registro en una etapa temprana del siguiente inicio (justo después de AUTOCHK, antes de crear el archivo de paginación). Hay que usarlo entendiendo que solo se puede llamar desde el grupo de administradores o LocalSystem, que no se puede combinar con MOVEFILE_COPY_ALLOWED (es decir, se limita al mismo volumen), que si el archivo de destino ya existe hace falta combinarlo con MOVEFILE_REPLACE_EXISTING (de lo contrario, aunque la reserva se registre con éxito, el reemplazo no se ejecutará al reiniciar), y que el éxito de la función es «el éxito de la reserva», no el resultado real del reemplazo. Cuando un instalador dice «se requiere reiniciar», por dentro es justamente esto lo que se ha ido acumulando.7

Hay dos precauciones comunes a cualquier patrón. Una es la actualización de servicios: como un servicio de Windows no puede reemplazarse a sí mismo, lo habitual es separar un proceso de actualización (o un servicio dedicado a actualizar) que haga «Stop con el SCM → reemplazar → Start» (los fundamentos del diseño de servicios están en «Cómo crear y operar servicios de Windows»). La otra es la verificación del archivo de actualización. Construir por cuenta propia un mecanismo de reemplazo equivale a construir un «mecanismo para distribuir y ejecutar cualquier exe», y si se omite la verificación de firma o la protección de la vía de descarga, el propio mecanismo de actualización se convierte en una vía de ataque. Este tema se trata por separado en «Diseño de seguridad de la actualización automática: por qué HTTPS no basta».

7. Tabla de decisión de los métodos de actualización

Método Situación adecuada Puntos a tener en cuenta
MSI + aceptar el reinicio del SO Frecuencia de distribución baja (unas pocas veces al año), se puede reservar tiempo de mantenimiento en horario nocturno o festivo Es lo más fácil de implementar. Sin embargo, se sigue mostrando al usuario «se requiere reiniciar». Depende del reemplazo al reiniciar (PendingFileRenameOperations)7
MSI + integración con Restart Manager (MsiRMFilesInUse + el conjunto de tres puntos del lado de la aplicación) Se quiere mejorar la experiencia de actualización manteniendo la distribución por MSI. Aplicaciones estándar de distribución interna El lado del instalador es casi automático. El efecto depende de que la aplicación implemente RegisterApplicationRestart/WM_QUERYENDSESSION45
Actualizador propio (proceso separado + rename-then-replace, incluyendo Squirrel/Velopack) Frecuencia de actualización alta (semanal o más), el usuario no tiene privilegios de administrador, no se quiere interrumpir el trabajo Hay que asumir la construcción propia de la limpieza, la verificación de firma y la reversión en caso de fallo. Es más sólido si se añade una medida contra «quién lo tiene abierto» con RmGetList/RmShutdown3
ClickOnce Solo se necesita «actualización automática al iniciar» en clientes Windows internos Con un modelo de actualización al inicio, el problema del archivo en uso casi no se presenta. Las limitaciones se explican en «Qué es ClickOnce»
Autoactualización del lado del servicio (proceso de actualización separado) Entornos sin supervisión o PC de equipo que deben mantener el servicio funcionando 24 horas sin detenerlo El propio servicio no puede reemplazarse a sí mismo. Es imprescindible un proceso separado que haga Stop → reemplazar → Start. Cuidado con el límite de sesión (desde LocalSystem no se pueden cerrar aplicaciones de usuario)2

Si tiene dudas, considere primero la opción «MSI + integración con Restart Manager». Al apoyarse en el mecanismo estándar del sistema operativo, requiere el mínimo esfuerzo de implementación, y el conjunto de tres puntos del lado de la aplicación sigue siendo un activo reutilizable incluso si más adelante se migra a un actualizador propio.

8. Resumen

  • Un exe o DLL en uso no se puede sobrescribir ni eliminar, pero sí se puede renombrar dentro del mismo volumen. El patrón rename-then-replace, que aprovecha esta asimetría, es la forma básica de una «actualización que no detiene» la aplicación.
  • Restart Manager es una API estándar del sistema operativo que se encarga de enumerar «quién tiene el archivo abierto» (RmGetList), cerrarlo de forma ordenada (RmShutdown) y reiniciarlo tras la actualización (RmRestart), y el MSI de Windows Installer 4.0 en adelante la usa automáticamente.
  • Las buenas prácticas del lado de la aplicación son un conjunto de tres puntos: registrar el reinicio con RegisterApplicationRestart, devolver TRUE a WM_QUERYENDSESSION (ENDSESSION_CLOSEAPP) y, en WM_ENDSESSION, guardar los datos sin guardar antes de terminar. Con solo esto, la aplicación «vuelve a levantarse tal como estaba tras la actualización».
  • La regla de los 60 segundos (no se reinicia justo después de iniciar), el tiempo de espera del cierre forzado (30 segundos para las aplicaciones, 20 para los servicios) y el límite de sesión (desde LocalSystem no se pueden cerrar aplicaciones de usuario) son los puntos donde más se tropieza en la operación real.
  • Cuando de ninguna manera se puede reemplazar el archivo, se reserva el reemplazo para el reinicio del sistema operativo con MoveFileEx + MOVEFILE_DELAY_UNTIL_REBOOT. Requiere privilegios de administrador, se limita al mismo volumen, y el éxito o fracaso corresponde al de la reserva.
  • Construir por cuenta propia un mecanismo de actualización equivale a construir un «mecanismo para distribuir cualquier exe». Diséñelo incluyendo la verificación de firma, la protección de la vía de distribución y la reversión en caso de fallo.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC se ocupa del diseño e implementación de mecanismos de actualización automática para aplicaciones empresariales (integración con Restart Manager, actualizadores propios, adopción de Squirrel/Velopack), de mejorar la operación de «pedirle a todos que cierren la aplicación cada vez que hay una actualización» y de investigar y solucionar los problemas de «archivo en uso» o «se requiere reiniciar» en instaladores existentes.

Referencias

  1. Microsoft Learn, About Restart Manager. Sobre el propósito de Restart Manager (reducir o eliminar los reinicios causados por archivos en uso), el cierre en el orden aplicaciones GUI → aplicaciones de consola → servicios → Explorador y el reinicio en orden inverso, que el apagado que cruza sesiones no está soportado, que Windows Installer 4.0 usa Restart Manager automáticamente, y que los servicios críticos del sistema no se pueden detener sin reiniciar el sistema operativo.  2 3 4 5

  2. Microsoft Learn, RmShutdown function. Sobre que, incluso con RmForceShutdown especificado, las aplicaciones que no responden se cierran a la fuerza a los 30 segundos y los servicios a los 20 segundos, que RmShutdownOnlyRegistered permite «cerrar únicamente cuando todas las aplicaciones están registradas para reiniciarse», que un servicio con LocalSystem no puede cerrar ni reiniciar aplicaciones de otra sesión de usuario, y sobre valores de retorno como ERROR_FAIL_NOACTION_REBOOT.  2 3 4 5 6 7 8 9

  3. Microsoft Learn, RmGetList function. Sobre que devuelve en un arreglo RM_PROCESS_INFO las aplicaciones y servicios que están usando los recursos registrados, la convención de llamada que devuelve ERROR_MORE_DATA (234) junto con el tamaño necesario cuando el búfer no alcanza, que lpdwRebootReasons devuelve el motivo (RM_REBOOT_REASON) por el que se necesita reiniciar el sistema operativo, y que rstrtmgr.dll está disponible desde Windows Vista en adelante.  2 3 4 5 6

  4. Microsoft Learn, Using Windows Installer with Restart Manager. Sobre que Windows Installer 4.0 usa Restart Manager automáticamente y, de forma predeterminada, prioriza cerrar y reiniciar la aplicación en lugar de reiniciar el sistema operativo; que el cuadro de diálogo MsiRMFilesInUse permite ofrecer, en la instalación con interfaz completa, la opción de cierre y reinicio automáticos (en entornos antiguos se recurre a FilesInUse); que en la instalación silenciosa siempre se usa Restart Manager y la aplicación se cierra; que las acciones personalizadas deben colocarse antes de InstallValidate y deben llamar a RmJoinSession a través de MsiRestartManagerSessionKey, sin llamar a RmShutdown ni similares; y sobre propiedades de control como MSIRESTARTMANAGERCONTROL.  2 3 4

  5. Microsoft Learn, Guidelines for Applications (Restart Manager). Sobre que a las aplicaciones GUI se les envía WM_QUERYENDSESSION (lParam=ENDSESSION_CLOSEAPP) y, si están listas, deben devolver TRUE sin cerrarse todavía en ese momento; que el cierre real se realiza con WM_ENDSESSION; que a las aplicaciones que no responden también se les envía WM_CLOSE; que a las aplicaciones de consola se les envía CTRL_C_EVENT; y que el reinicio requiere estar registrado mediante RegisterApplicationRestart.  2 3 4 5 6 7

  6. Microsoft Learn, RegisterApplicationRestart function. Sobre el registro de la línea de comandos para el reinicio (sin incluir el nombre del exe, longitud máxima RESTART_MAX_CMD_LINE), los indicadores RESTART_NO_CRASH/NO_HANG/NO_PATCH/NO_REBOOT, que el reinicio por actualización es automático mientras que en caso de bloqueo o cuelgue se requiere el consentimiento del usuario, que para evitar bucles no se reinicia si no han pasado al menos 60 segundos desde el inicio, y que la última oportunidad de volver a registrar durante una actualización es al procesar WM_QUERYENDSESSION.  2 3 4

  7. Microsoft Learn, MoveFileExW function. Sobre que MOVEFILE_DELAY_UNTIL_REBOOT escribe la reserva en PendingFileRenameOperations (REG_MULTI_SZ) de HKLM\SYSTEM\CurrentControlSet\Control\Session Manager, y que se ejecuta en el orden de registro tras ejecutarse AUTOCHK y antes de crear el archivo de paginación; que se requieren privilegios del grupo de administradores o de LocalSystem; que no se puede combinar con MOVEFILE_COPY_ALLOWED; que si el archivo de destino ya existe se requiere especificar MOVEFILE_REPLACE_EXISTING; que el valor de retorno indica el éxito de la reserva, no el éxito real del movimiento; y que pasar NULL en lpNewFileName provoca la eliminación al reiniciar.  2 3

  8. Microsoft Learn, WTSQueryUserToken function (wtsapi32.h). Sobre que es una función que obtiene el token de acceso principal del usuario con la sesión iniciada indicando el ID de sesión; que la llamada requiere ejecutarse en el contexto de la cuenta LocalSystem y contar con el privilegio SE_TCB_NAME; que está pensada para servicios de alta confianza y hay que tener cuidado de no filtrar el token y cerrar siempre con CloseHandle el identificador obtenido; y que WTSEnumerateSessions puede usarse para enumerar los ID de sesión. 

  9. Microsoft Learn, RM_APP_TYPE enumeration (restartmanager.h). Sobre que, como tipo de aplicación indicado por la estructura RM_PROCESS_INFO, se definen RmUnknownApp (0, no se clasifica en ninguna otra categoría y solo se puede detener con un cierre forzado), RmMainWindow (1, aplicación de Windows como proceso independiente con ventana de nivel superior), RmOtherWindow (2, aplicación de Windows que no es proceso independiente ni tiene ventana de nivel superior), RmService (3, servicio de Windows), RmExplorer (4, Explorador de Windows), RmConsole (5, aplicación de consola independiente) y RmCritical (1000, no se puede detener el proceso, por lo que la instalación necesita reiniciar el sistema; el motivo puede ser un proceso crítico, privilegios insuficientes, o que es el propio proceso del instalador que inició Restart Manager). 

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.

¿Por qué no se puede sobrescribir un exe en ejecución o una DLL cargada, pero sí se puede renombrar?
Windows retiene el exe en ejecución o la DLL cargada como un mapa de memoria (una sección de imagen), por lo que reescribir o eliminar el contenido del archivo produce un error (violación de uso compartido o acceso denegado). En cambio, el «nombre» en el directorio es independiente del contenido del archivo, así que renombrarlo (moverlo) dentro del mismo volumen tiene éxito incluso mientras está en ejecución. El patrón rename-then-replace aprovecha esta asimetría: renombra el exe antiguo con un nombre de reserva y coloca el nuevo exe con el nombre original, de modo que la versión nueva se use desde el próximo inicio. Las actualizaciones automáticas de Chrome y de la familia Squirrel/Velopack también se basan, en esencia, en esta misma propiedad.
¿Qué hace la API Restart Manager?
Es una API estándar de Windows (desde Windows Vista) que, al registrar el archivo que se quiere actualizar, enumera «la lista de aplicaciones y servicios que lo están usando en ese momento», los cierra si es posible y llega incluso a reiniciarlos tras la actualización. El flujo es: crear la sesión con RmStartSession, registrar el archivo objetivo con RmRegisterResources, enumerar los procesos que lo ocupan con RmGetList, cerrarlos con RmShutdown, reiniciarlos con RmRestart y, por último, RmEndSession. Se detiene en el orden aplicaciones GUI → aplicaciones de consola → servicios → Explorador, y el reinicio se hace en orden inverso. Es un mecanismo para reducir el mensaje de «se requiere reiniciar», y el MSI de Windows Installer 4.0 en adelante lo usa automáticamente.
¿Qué ocurre si se llama a RegisterApplicationRestart?
La aplicación puede registrar ante el sistema operativo su propia línea de comandos de reinicio, y tras un apagado provocado por una actualización, Restart Manager reinicia automáticamente la aplicación con esa línea de comandos (Restart Manager solo puede reiniciar aplicaciones registradas). También se reinicia en caso de bloqueo o cuelgue, pero en ese caso interviene el consentimiento del usuario, mientras que el reinicio por actualización se realiza de forma automática. Tenga en cuenta que, para evitar bucles, no se reinicia si no han pasado al menos 60 segundos desde el inicio, y que la línea de comandos no debe incluir el nombre del exe. Lo correcto es volver a registrar la línea de comandos incluyendo en los argumentos el estado necesario para la restauración (por ejemplo, los archivos que estaban abiertos).
¿Cuándo se usa MOVEFILE_DELAY_UNTIL_REBOOT de MoveFileEx?
Es el último recurso cuando de ninguna manera se puede detener el proceso y tampoco se puede usar rename-then-replace: reserva el movimiento o la eliminación del archivo para cuando se reinicie el sistema operativo. La reserva se escribe en PendingFileRenameOperations, dentro del Registro (HKLM\SYSTEM\CurrentControlSet\Control\Session Manager), y se ejecuta en el orden de registro en una etapa temprana del siguiente inicio (después de AUTOCHK, antes de crear el archivo de paginación). La llamada requiere privilegios del grupo de administradores o de LocalSystem, y como no se puede combinar con MOVEFILE_COPY_ALLOWED, no sirve para mover archivos a otro volumen. También conviene tener presente que el éxito de la función indica «el éxito de la reserva», no el resultado real del reemplazo.
¿Cómo se maneja de forma ordenada el «archivo en uso» en un instalador MSI?
Desde Windows Installer 4.0, el instalador se integra automáticamente con Restart Manager y, de forma predeterminada, prioriza cerrar y reiniciar la aplicación antes que reiniciar el sistema operativo. Si se añade el cuadro de diálogo MsiRMFilesInUse al paquete, durante una instalación con interfaz completa se le ofrece al usuario la opción de «cerrar la aplicación automáticamente y reiniciarla». Si se ejecuta con una versión antigua de Windows Installer, se recurre al cuadro de diálogo tradicional FilesInUse, por lo que lo habitual es incluir ambos. El comportamiento se puede controlar con propiedades como MSIRESTARTMANAGERCONTROL, y en la instalación silenciosa siempre se usa Restart Manager y la aplicación se cierra automáticamente. Si la aplicación implementa RegisterApplicationRestart y responde a WM_QUERYENDSESSION, se logra una «actualización casi sin interrupciones», en la que la aplicación vuelve a levantarse tal como estaba después de la actualización.

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