diff --git a/src/FileLockFinder/FileLockFinder.csproj b/src/FileLockFinder/FileLockFinder.csproj new file mode 100644 index 00000000..907b8abc --- /dev/null +++ b/src/FileLockFinder/FileLockFinder.csproj @@ -0,0 +1,26 @@ + + + net8.0 + FileLockFinder + FileLockFinder + FileLockFinder + Library + true + ..\Solution Items\Key.snk + $(SolutionDir)..\bin\$(Configuration)\plugins + $(DefineConstants) + + + + False + + + True + + + + + Key.snk + + + diff --git a/src/FileLockFinder/LockFinder.cs b/src/FileLockFinder/LockFinder.cs new file mode 100644 index 00000000..b6f09767 --- /dev/null +++ b/src/FileLockFinder/LockFinder.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; + +// Expanded with some helpers from: https://code.msdn.microsoft.com/windowsapps/How-to-know-the-process-704839f4/ +// Uses Windows Restart Manager. +// A more involved and cross platform solution to this problem is here: https://github.com/cklutz/LockCheck + + +namespace FileLockFinder +{ + public class LockFinder + { + + /// + /// Method FindLockedProcessName Retrieve the first process name + /// that is locking the file at the specified path + /// + /// The path of a file with a write lock held by a + /// process + /// The name of the first process found with a lock + /// + /// Thrown when the file path is not locked + /// + static public string FindLockedProcessName(string path) + { + var list = FindLockProcesses(path); + if (list.Count == 0) + { + throw new Exception( + "No processes are locking the path specified"); + } + return list[0].ProcessName; + } + + /// + /// Method CheckIfFileIsLocked Check if the file specified has a + /// write lock held by a process + /// + /// The path of a file being checked if a write lock + /// held by a process + /// true when one or more processes with lock + static public bool CheckIfFileIsLocked(string path) + { + var list = FindLockProcesses(path); + if (list.Count > 0) { return true; } + return false; + } + + /// + /// Used to find processes holding a lock on the file. This would cause + /// other usage, such as file truncation or write opretions to throw + /// IOException if an exclusive lock is attempted. + /// + /// Path being checked + /// List of processes holding file lock to path + /// + static public List FindLockProcesses(string path) + { + var key = Guid.NewGuid().ToString(); + var processes = new List(); + + int res = RmStartSession(out uint handle, 0, key); + if (res != 0) + { + throw new Exception("Could not begin restart session. " + + "Unable to determine file locker."); + } + + try + { + uint pnProcInfo = 0; + uint lpdwRebootReasons = RmRebootReasonNone; + string[] resources = [path]; + + res = RmRegisterResources(handle, (uint)resources.Length, + resources, 0, null, 0, null); + if (res != 0) + { + throw new Exception("Could not register resource."); + } + res = RmGetList(handle, out uint pnProcInfoNeeded, ref pnProcInfo, null, + ref lpdwRebootReasons); + const int ERROR_MORE_DATA = 234; + if (res == ERROR_MORE_DATA) + { + RM_PROCESS_INFO[] processInfo = + new RM_PROCESS_INFO[pnProcInfoNeeded]; + pnProcInfo = pnProcInfoNeeded; + // Get the list. + res = RmGetList(handle, out pnProcInfoNeeded, ref pnProcInfo, processInfo, ref lpdwRebootReasons); + if (res == 0) + { + processes = new List((int)pnProcInfo); + for (int i = 0; i < pnProcInfo; i++) + { + try + { + processes.Add(Process.GetProcessById(processInfo[i]. + Process.dwProcessId)); + } + catch (ArgumentException) { } + } + } + else + { + throw new Exception("Could not list processes locking resource"); + } + } + else if (res != 0) + { + throw new Exception("Could not list processes locking resource." + + "Failed to get size of result."); + } + } + catch (Exception exception) + { + Trace.WriteLine(exception.Message); + } + finally + { + Trace.WriteLine($"RmEndSession: {RmEndSession(handle)}"); + } + + return processes; + } + private const int RmRebootReasonNone = 0; + private const int CCH_RM_MAX_APP_NAME = 255; + private const int CCH_RM_MAX_SVC_NAME = 63; + + [StructLayout(LayoutKind.Sequential)] + struct RM_UNIQUE_PROCESS + { + public int dwProcessId; + public System.Runtime.InteropServices. + ComTypes.FILETIME ProcessStartTime; + } + [DllImport("rstrtmgr.dll", + CharSet = CharSet.Auto, SetLastError = true)] + static extern int RmGetList(uint dwSessionHandle, + out uint pnProcInfoNeeded, + ref uint pnProcInfo, + [In, Out] RM_PROCESS_INFO[] rgAffectedApps, + ref uint lpdwRebootReasons); + [StructLayout(LayoutKind.Sequential, + CharSet = CharSet.Auto)] + 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 RM_APP_TYPE ApplicationType; + public uint AppStatus; + public uint TSSessionId; + [MarshalAs(UnmanagedType.Bool)] + public bool bRestartable; + } + + enum RM_APP_TYPE + { + RmUnknownApp = 0, + RmMainWindow = 1, + RmOtherWindow = 2, + RmService = 3, + RmExplorer = 4, + RmConsole = 5, + RmCritical = 1000 + } + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern int RmRegisterResources( + uint pSessionHandle, + UInt32 nFiles, + string[] rgsFilenames, + UInt32 nApplications, + [In] RM_UNIQUE_PROCESS[] rgApplications, + UInt32 nServices, string[] rgsServiceNames); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern int RmStartSession( + out uint pSessionHandle, + int dwSessionFlags, + string strSessionKey); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto, SetLastError = true)] + static extern int RmEndSession(uint pSessionHandle); + } +} diff --git a/src/LogExpert/Controls/LogTabWindow/LogTabWindow.designer.cs b/src/LogExpert/Controls/LogTabWindow/LogTabWindow.designer.cs index 8f879281..129c0aeb 100644 --- a/src/LogExpert/Controls/LogTabWindow/LogTabWindow.designer.cs +++ b/src/LogExpert/Controls/LogTabWindow/LogTabWindow.designer.cs @@ -6,25 +6,25 @@ namespace LogExpert.Controls.LogTabWindow { - partial class LogTabWindow - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; + partial class LogTabWindow + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } #region Windows Form Designer generated code @@ -155,6 +155,7 @@ private void InitializeComponent() tabRenameToolStripMenuItem = new ToolStripMenuItem(); copyPathToClipboardToolStripMenuItem = new ToolStripMenuItem(); findInExplorerToolStripMenuItem = new ToolStripMenuItem(); + truncateFileToolStripMenuItem = new ToolStripMenuItem(); dragControlDateTime = new DateTimeDragControl(); statusStrip.SuspendLayout(); mainMenuStrip.SuspendLayout(); @@ -1053,7 +1054,7 @@ private void InitializeComponent() // tabContextMenuStrip.ForeColor = System.Drawing.SystemColors.ControlText; tabContextMenuStrip.ImageScalingSize = new System.Drawing.Size(24, 24); - tabContextMenuStrip.Items.AddRange(new ToolStripItem[] { closeThisTabToolStripMenuItem, closeOtherTabsToolStripMenuItem, closeAllTabsToolStripMenuItem, tabColorToolStripMenuItem, tabRenameToolStripMenuItem, copyPathToClipboardToolStripMenuItem, findInExplorerToolStripMenuItem }); + tabContextMenuStrip.Items.AddRange(new ToolStripItem[] { closeThisTabToolStripMenuItem, closeOtherTabsToolStripMenuItem, closeAllTabsToolStripMenuItem, tabColorToolStripMenuItem, tabRenameToolStripMenuItem, copyPathToClipboardToolStripMenuItem, findInExplorerToolStripMenuItem, truncateFileToolStripMenuItem }); tabContextMenuStrip.Name = "tabContextMenuStrip"; tabContextMenuStrip.Size = new System.Drawing.Size(197, 158); // @@ -1114,6 +1115,14 @@ private void InitializeComponent() findInExplorerToolStripMenuItem.ToolTipText = "Opens an Explorer window and selects the log file"; findInExplorerToolStripMenuItem.Click += OnFindInExplorerToolStripMenuItemClick; // + // truncateFileToolStripMenuItem + // + this.truncateFileToolStripMenuItem.Name = "truncateFileToolStripMenuItem"; + this.truncateFileToolStripMenuItem.Size = new System.Drawing.Size(196, 22); + this.truncateFileToolStripMenuItem.Text = "Truncate File"; + this.truncateFileToolStripMenuItem.ToolTipText = "Try to truncate the file opened in tab"; + this.truncateFileToolStripMenuItem.Click += new System.EventHandler(this.truncateFileToolStripMenuItem_Click); + // // dragControlDateTime // dragControlDateTime.Anchor = AnchorStyles.Bottom | AnchorStyles.Right; @@ -1236,6 +1245,7 @@ private void InitializeComponent() private ToolStripButton toolStripButtonBubbles; private ToolStripMenuItem copyPathToClipboardToolStripMenuItem; private ToolStripMenuItem findInExplorerToolStripMenuItem; + private ToolStripMenuItem truncateFileToolStripMenuItem; private ToolStripMenuItem exportBookmarksToolStripMenuItem; private ToolStripComboBox groupsComboBoxHighlightGroups; private ToolStripMenuItem debugToolStripMenuItem; diff --git a/src/LogExpert/Controls/LogTabWindow/LogTabWindowEventHandlers.cs b/src/LogExpert/Controls/LogTabWindow/LogTabWindowEventHandlers.cs index 8debf55d..b25eb0f0 100644 --- a/src/LogExpert/Controls/LogTabWindow/LogTabWindowEventHandlers.cs +++ b/src/LogExpert/Controls/LogTabWindow/LogTabWindowEventHandlers.cs @@ -811,6 +811,11 @@ private void OnFindInExplorerToolStripMenuItemClick(object sender, EventArgs e) explorer.Start(); } + private void truncateFileToolStripMenuItem_Click(object sender, EventArgs e) + { + CurrentLogWindow?.TryToTruncate(); + } + private void OnExportBookmarksToolStripMenuItemClick(object sender, EventArgs e) { CurrentLogWindow?.ExportBookmarkList(); @@ -1018,4 +1023,4 @@ private void OnTabRenameToolStripMenuItemClick(object sender, EventArgs e) #endregion } -} \ No newline at end of file +} diff --git a/src/LogExpert/Controls/LogWindow/LogWindowsPublic.cs b/src/LogExpert/Controls/LogWindow/LogWindowsPublic.cs index d1298c77..e0564d3f 100644 --- a/src/LogExpert/Controls/LogWindow/LogWindowsPublic.cs +++ b/src/LogExpert/Controls/LogWindow/LogWindowsPublic.cs @@ -1,4 +1,5 @@ -using LogExpert.Classes; +using FileLockFinder; +using LogExpert.Classes; using LogExpert.Classes.Bookmark; using LogExpert.Classes.Columnizer; using LogExpert.Classes.Filter; @@ -579,6 +580,26 @@ public void GotoLine(int line) } } + public void TryToTruncate() + { + try + { + if (LockFinder.CheckIfFileIsLocked(Title)) + { + var name = LockFinder.FindLockedProcessName(Title); + var status = string.Format("Truncate failed: file is locked by {0}", name); + StatusLineText(status); + return; + } + + File.WriteAllText(Title, ""); + }catch(Exception E) + { + StatusLineText("Unexpected issue truncating file"); + throw E; + } + } + public void StartSearch() { _guiStateArgs.MenuEnabled = false;