Страницы

Поиск по вопросам

суббота, 14 декабря 2019 г.

Зависшие процессы Office interop

#c_sharp #office #office_interop


Предположим, в приложении необходимо использовать Office interop.(без вариантов)
Всем известно, что он создает приложения office без интерфейса и через них выполняет
работу.

Ну как быть, если пользователь некорректно завершил работу с программой? Например,
убил через диспетчер задач. Получается, что в процессах остаются висеть приложения офиса.

Можно ли это предотвратить? Если нет, то можно ли понять при запуске(повторном) приложения,
что в процессах висит мусор, который остался от прошлого запуска и грохнуть эти процессы,
не затронув реальные приложения офиса(например, юзер запустил Excel.)
    


Ответы

Ответ 1



Как вариант, средствами Win32 можно объединить приложение и запущенные из него процессы в задание (Job) тогда процессы Interop будут завершаться вместе с родительским приложением. Для этого нужно будет создать задание с помощью CreateJobObject, а затем добавлять созданные процессы в задание с помощью AssignProcessToJobObject Есть похожее обсуждение на английском языке: «Kill child process when parent process is killed». Из ответа @Ron можно взять код класса-обертки ChildProcessTracker: /// /// Allows processes to be automatically killed if this parent process unexpectedly quits. /// This feature requires Windows 8 or greater. On Windows 7, nothing is done. /// References: /// https://stackoverflow.com/a/4657392/386091 /// https://stackoverflow.com/a/9164742/386091 public static class ChildProcessTracker { /// /// Add the process to be tracked. If our current process is killed, the child processes /// that we are tracking will be automatically killed, too. If the child process terminates /// first, that's fine, too. /// public static void AddProcess(int processHandle) { if (s_jobHandle != IntPtr.Zero) { bool success = AssignProcessToJobObject(s_jobHandle, new IntPtr(processHandle)); if (!success) throw new Win32Exception(); } } static ChildProcessTracker() { // This feature requires Windows 8 or later. To support Windows 7 requires // registry settings to be added if you are using Visual Studio plus an // app.manifest change. // https://stackoverflow.com/a/4232259/386091 // https://stackoverflow.com/a/9507862/386091 if (Environment.OSVersion.Version < new Version(6, 2)) return; // The job name is optional (and can be null) but it helps with diagnostics. // If it's not null, it has to be unique. Use SysInternals' Handle command-line // utility: handle -a ChildProcessTracker string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id; s_jobHandle = CreateJobObject(IntPtr.Zero, jobName); var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION { LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE }; // This is the key flag. When our process is killed, Windows will automatically // close the job handle, and when that happens, we want the child processes to // be killed, too. var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); extendedInfo.BasicLimitInformation = info; int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); try { Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length)) { throw new Win32Exception(); } } finally { Marshal.FreeHGlobal(extendedInfoPtr); } } [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); [DllImport("kernel32.dll")] static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); // Windows will automatically close any open job handles when our process terminates. // This can be verified by using SysInternals' Handle utility. When the job handle // is closed, the child processes will be killed. private static readonly IntPtr s_jobHandle; } public enum JobObjectInfoType { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, BasicUIRestrictions = 4, EndOfJobTimeInformation = 6, ExtendedLimitInformation = 9, SecurityLimitInformation = 5, GroupInformation = 11 } [StructLayout(LayoutKind.Sequential)] public struct JOBOBJECT_BASIC_LIMIT_INFORMATION { public Int64 PerProcessUserTimeLimit; public Int64 PerJobUserTimeLimit; public JOBOBJECTLIMIT LimitFlags; public UIntPtr MinimumWorkingSetSize; public UIntPtr MaximumWorkingSetSize; public UInt32 ActiveProcessLimit; public Int64 Affinity; public UInt32 PriorityClass; public UInt32 SchedulingClass; } [Flags] public enum JOBOBJECTLIMIT : uint { JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 } [StructLayout(LayoutKind.Sequential)] public struct IO_COUNTERS { public UInt64 ReadOperationCount; public UInt64 WriteOperationCount; public UInt64 OtherOperationCount; public UInt64 ReadTransferCount; public UInt64 WriteTransferCount; public UInt64 OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION { public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; public IO_COUNTERS IoInfo; public UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } , и регистрировать с помощью него процессы Excel как дочерние: [DllImport("user32.dll")] static extern int GetWindowThreadProcessId(int hWnd, out int lpdwProcessId); var app = new Microsoft.Office.Interop.Excel.Application(); int processId; GetWindowThreadProcessId(app.Hwnd, out processId); ChildProcessTracker.AddProcess(processId); , и тогда ОС будет убивать их вместе с родительским процессом. Рекоммендую ознакомиться с документацией и почитать комментарии в обсуждении по ссылке, могут возникнуть нюансы с поддержкой разных версий ОС и Visual Studio, при запуске с ограниченными правами, при запуске родительского приложения из других процессов. Если нет, то можно ли понять при запуске(повторном) приложения, что в процессах висит мусор, который остался от прошлого запуска и грохнуть эти процессы, не затронув реальные приложения офиса(например, юзер запустил Excel.) Сомневаюсь в существовании надежного метода. Процесс Excel запущенный старым процессом ничем не отличается от процесса запущенного текущим/другими приложениями/пользователем. Можно, как предложил @VadimTagil, написать код, который будет сохранять запущенные процессы в постоянное хранилище (диск/БД/реестр) и убивать их по определенной логике. Еще один вариант, вынести формирование Excel в отдельные процессы-задания, не зависящие от основного приложения. Вообще, если в нормальном ходе работы приложения мусор не остается, то проблемы будут возникать лишь в исключительных случаях (убитый извне процесс), обработку которых можно будет оставить пользователю. Убедитесь, что для всех объектов вызывается Marshal.ReleaseComObject и что все процессы завершаются и, возможно, без задания можно будет обойтись.

Ответ 2



Я полагаю, суть вопроса в методике отделения "мусора" от нормальных процессов Excel. Умея это, уже можно что-то смастерить: удалять мусорные процессы при запуске основной программы, или периодически - фоновым сервисом, когда основная программа закрыта. Наиболее правильное решение: добавлять Id всех создаваемых Interop-процессов в БД, при корректном их завершении - удалять. Соответственно, при некорректном завершении приложения в БД останутся Id мертвых процессов, которые можно на следующем запуске прибить (предварительно убедившись, что это все еще существующие процессы Excel, так как они могли быть прибиты чем-то другим и те же Id уже переиспользованы системой для другой программы). Но, если хочется метод попроще, можно считать "мусором" любой процесс Excel, не имеющий видимого главного окна: using System.Diagnostics; using System.Runtime.InteropServices; static class Program { [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] static extern bool IsWindowVisible(IntPtr hWnd); public static bool IsProcessDead(Process pr) { IntPtr hwnd = pr.MainWindowHandle; if (hwnd == IntPtr.Zero) return true; return !IsWindowVisible(hwnd); } void ClearProcesses() { Process[] prs=Process.GetProcessesByName("excel"); foreach (Process proc in prs) { if(IsProcessDead(proc))proc.Kill(); } } } Если на целевой машине нет других программ, использующих невидимые эксели для своих целей, это можно считать нормальным допущением.

Комментариев нет:

Отправить комментарий