// // Copyright 2014 Perforce Software Inc. // using Perforce.P4; using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Threading; using System.Timers; [assembly: InternalsVisibleTo("Perforce_test")] namespace Perforce.Helper { public struct ActionMatchCount { public string Action; public int MatchCount; public ActionMatchCount(string action) { Action = action; MatchCount = 0; } } public class FileSystemSyncActionMatcher { // Dictonary indexed by localPath. Records tuple of Action/Match Count // Match count can be > 1 for an add/edit of same file. private Dictionary _p4FileActions = new Dictionary(StringComparer.OrdinalIgnoreCase); // Mainly intended for testing public void Reset() { _p4FileActions.Clear(); } // For use by othe threads to notify us of changes to actions in filesystem. // E.g. p4 commands such as: sync, delete, submit // path may contain a wildcard public void SyncStart(string path) { } public void SyncComplete(string path) { } // Record an action to a particular file - as reported by Perforce private void RecordSyncProgressAction(string originalPath, SyncMetaData smd) { _p4FileActions.Add(smd.Action + "-" + smd.ClientPath.Path, 1); } public void SyncRecordProgress(string originalPath, IList smdList) { if (smdList == null) return; foreach (var smd in smdList) { RecordSyncProgressAction(originalPath, smd); } } private bool FileMatched(string localPath, string action) { if (_p4FileActions.Count == 0) return false; string key = action + "-" + localPath; return (_p4FileActions.ContainsKey(key)); } public bool MatchDeletedFile(string localPath) { return FileMatched(localPath, "deleted"); } public bool MatchAddedFile(string localPath) { return FileMatched(localPath, "added"); } // Decide if we can match an action public void RecordActionMatched(string clientPath, string fsAction) { if (_p4FileActions.Count == 0) return; string key = fsAction + "-" + clientPath; if (_p4FileActions.ContainsKey(key)) { _p4FileActions.Remove(key); } } // Tidy up actions public void RemoveCompletedSyncActions() { } } public class WorkspaceWatcher { private FileSystemWatcher _parentFolderWatcher = null, _subfolderWatcher = null; private static System.Object _lockThis = new System.Object(); private static System.Object _processChangesLock = new System.Object(); private System.Timers.Timer _changeNotifier; private List _createdList = new List(); private List _deletedList = new List(); private List _renamedList = new List(); private List _changedList = new List(); private String _watchPath = null; private int _watchInterval = 1000; public delegate void RefreshSelectorDelegate(ViewModel.SELECTOR_TYPE type); private static RefreshSelectorDelegate _refreshSelector = null; private static FileSystemSyncActionMatcher _fsSyncActionMatcher = new FileSystemSyncActionMatcher(); private static PerforceHelper _p4 = null; // For testing purposes public static void SetRefreshSelectorFuntion(RefreshSelectorDelegate refreshFunction) { _refreshSelector = refreshFunction; } public WorkspaceWatcher() { try { var helper = Utility.GetPerforceHelper(); if (helper != null && helper.ClientEnabled) { this._watchPath = helper.CurrentClient.Root; } } catch (Exception e) { Log.Exception(e); } } public void SyncStart(string path) { lock (_lockThis) { _fsSyncActionMatcher.SyncStart(path); } } public void SyncRecordProgress(string originalPath, IList smdList) { lock (_lockThis) { _fsSyncActionMatcher.SyncRecordProgress(originalPath, smdList); } } public void SyncComplete(string path) { lock (_lockThis) { _fsSyncActionMatcher.SyncComplete(path); } } public void SyncMatcherReset() { lock (_lockThis) { _fsSyncActionMatcher.Reset(); } } public int WatchInterval { get { return _watchInterval; } set { _watchInterval = value; } } public PerforceHelper PerforceHelper { set { _p4 = value; } } public string WatchPath { get { return _watchPath; } set { _watchPath = value; } } public bool Start() { Log.TraceFunction(); try { if (!String.IsNullOrEmpty(_watchPath)) { Log.Debug(string.Format("Watching: '{0}'", _watchPath)); _subfolderWatcher = new FileSystemWatcher(); _subfolderWatcher.Path = _watchPath; _subfolderWatcher.IncludeSubdirectories = true; _subfolderWatcher.NotifyFilter = NotifyFilters.CreationTime | NotifyFilters.FileName | NotifyFilters.DirectoryName; // Add event handlers. _subfolderWatcher.Created += new FileSystemEventHandler(HandleCreateEvent); _subfolderWatcher.Deleted += new FileSystemEventHandler(HandleDeleteEvent); _subfolderWatcher.Renamed += new RenamedEventHandler(HandleRenameEvent); //Begin watching _subfolderWatcher.EnableRaisingEvents = true; _parentFolderWatcher = new FileSystemWatcher(); _parentFolderWatcher.Path = Path.GetDirectoryName(_watchPath); _parentFolderWatcher.IncludeSubdirectories = true; _parentFolderWatcher.NotifyFilter = NotifyFilters.Size; // Add event handlers. _parentFolderWatcher.Changed += new FileSystemEventHandler(HandleChangeEvent); //Begin watching _parentFolderWatcher.EnableRaisingEvents = true; _changeNotifier = new System.Timers.Timer(_watchInterval); _changeNotifier.Elapsed += new ElapsedEventHandler(HandleFileSystemChange); return true; } } catch (Exception e) { Log.Exception(e); } return false; } public bool Stop() { Log.TraceFunction(); try { _parentFolderWatcher.EnableRaisingEvents = false; _subfolderWatcher.EnableRaisingEvents = false; _parentFolderWatcher.Dispose(); _subfolderWatcher.Dispose(); _parentFolderWatcher = null; _subfolderWatcher = null; return true; } catch (Exception e) { Log.Exception(e); } return false; } private void HandleChangeEvent(Object sender, FileSystemEventArgs args) { Log.TraceFunction(); Log.Debug(string.Format("change event {0}|{1}", args.ChangeType, args.FullPath)); Thread.Sleep(100); lock (_lockThis) { if (args.FullPath.StartsWith(_watchPath)) { _changeNotifier.Enabled = false; if (System.IO.File.Exists(args.FullPath)) { _changedList.Add(new FSOPChangeVO(args.FullPath)); } _changeNotifier.Enabled = true; } } } private void HandleCreateEvent(Object sender, FileSystemEventArgs args) { Log.TraceFunction(); Log.Debug(string.Format("create event {0}", args.FullPath)); var filename = Path.GetFileName(args.FullPath); if (!filename.StartsWith("~$")) { Thread.Sleep(100); lock (_lockThis) { _changeNotifier.Enabled = false; if (System.IO.File.Exists(args.FullPath)) { _createdList.Add(new FSOPCreateVO(args.FullPath)); } _changeNotifier.Enabled = true; } } } private void HandleDeleteEvent(Object sender, FileSystemEventArgs args) { Log.TraceFunction(); Log.Debug(string.Format("delete event {0}", args.FullPath)); Thread.Sleep(100); lock (_lockThis) { _changeNotifier.Enabled = false; if (!System.IO.File.Exists(args.FullPath)) { _deletedList.Add(new FSOPDeleteVO(args.FullPath)); } _changeNotifier.Enabled = true; } } private void HandleRenameEvent(Object sender, RenamedEventArgs args) { Log.TraceFunction(); Log.Debug(string.Format("rename event {0}", args.FullPath)); Thread.Sleep(100); lock (_lockThis) { _changeNotifier.Enabled = false; if (!System.IO.File.Exists(args.OldFullPath)) { _renamedList.Add(new FSOPRenameVO(args.OldFullPath, args.FullPath)); } _changeNotifier.Enabled = true; } } private void HandleFileSystemChange(Object sender, ElapsedEventArgs args) { _changeNotifier.Enabled = false; ProcessChanges(); } // Processing to decide what changes need to be handled and how. private void GatherFileSystemChanges(out List pathsToRevert, out List pathsToAdd, out List pathsToEdit, out List pathsToDelete, out List> pathsToRename, out List> foldersToRename) { pathsToRevert = new List(); pathsToAdd = new List(); pathsToEdit = new List(); pathsToDelete = new List(); pathsToRename = new List>(); foldersToRename = new List>(); Log.Debug(string.Format("Lists - created:{0}, changed:{1}, deleted:{2}, renamed:{3}", _createdList.Count, _changedList.Count, _deletedList.Count, _renamedList.Count)); // Wait to process things for a period after they have occurred _changedList.Sort((x, y) => DateTime.Compare(x.EventTime, y.EventTime)); _createdList.Sort((x, y) => DateTime.Compare(x.EventTime, y.EventTime)); _deletedList.Sort((x, y) => DateTime.Compare(x.EventTime, y.EventTime)); _renamedList.Sort((x, y) => DateTime.Compare(x.EventTime, y.EventTime)); const int milliSecondsDelay = 500; while (true) { FSOPCreateVO createItem = null; FSOPDeleteVO deleteItem = null; var currentTime = DateTime.Now; if (_createdList.Count > 0) { if (currentTime.Subtract(_createdList[0].EventTime).TotalMilliseconds < milliSecondsDelay) { continue; } if ((createItem = _createdList[0]) != null && _deletedList.Count > 0 && (deleteItem = GetDeleteItemByName(Path.GetFileName(createItem.path))) != null) { _createdList.Remove(createItem); _deletedList.Remove(deleteItem); _renamedList.Add(new FSOPRenameVO(deleteItem.path, createItem.path)); } else { if (createItem.type == FSType.FILE) { Log.Debug(string.Format("Create considering: {0}", createItem.path)); if (System.IO.File.Exists(createItem.path)) { if (_fsSyncActionMatcher.MatchAddedFile(createItem.path)) { Log.Debug(string.Format("Ignoring add as matched: {0}", createItem.path)); _fsSyncActionMatcher.RecordActionMatched(createItem.path, "added"); } else { pathsToAdd.Add(createItem.path); } } } else if (createItem.type == FSType.FOLDER) { Log.Debug(string.Format("Create considering folder: {0}", createItem.path)); var allFiles = Directory.GetFiles(createItem.path, "*.*", SearchOption.AllDirectories); if (allFiles != null && allFiles.Length > 0) { foreach (var f in allFiles) { if (_fsSyncActionMatcher.MatchAddedFile(f)) { Log.Debug(string.Format("Ignoring add as matched: {0}", f)); _fsSyncActionMatcher.RecordActionMatched(f, "added"); } else { pathsToAdd.Add(f); } } } } _createdList.Remove(createItem); DeleteItemFromChangeList(createItem.path); } } else if (_deletedList.Count > 0) { deleteItem = GetFirstValidDeleteItem(); if (deleteItem != null) { if (currentTime.Subtract(deleteItem.EventTime).TotalMilliseconds < milliSecondsDelay) { continue; } // check to see if this is a file by trying to get its metadata -- even if it is // an added file in the changelist, it will have an fstat entry if (_fsSyncActionMatcher.MatchDeletedFile(deleteItem.path)) { Log.Debug(string.Format("Ignoring delete as matched: {0}", deleteItem.path)); _fsSyncActionMatcher.RecordActionMatched(deleteItem.path, "deleted"); } else { var md = _p4.RunFstat(deleteItem.path); if (md != null) { // if we have metadata, then the file exists in Perforce if (md.Action == FileAction.Add || md.Action == FileAction.MoveAdd) { // if file has been marked for add, we need to revert it pathsToRevert.Add(deleteItem.path); } else { if (md.Action != FileAction.None) { pathsToRevert.Add(deleteItem.path); } if (!System.IO.File.Exists(deleteItem.path)) { pathsToDelete.Add(deleteItem.path); } } } else { // if the deleted item is a directory, then we need to do some more work var dirPath = deleteItem.path + "/..."; var list = new List(); list.Add(dirPath); var moreMd = _p4.GetFileMetaData(list); if (moreMd != null && moreMd.Count > 0) { foreach (var m in moreMd) { if (m != null) { if (m.Action == FileAction.Add || m.Action == FileAction.MoveAdd) { pathsToRevert.Add(m.DepotPath.Path); } else { if (m.Action != FileAction.None) { pathsToRevert.Add(m.DepotPath.Path); } if (!System.IO.File.Exists(m.LocalPath.Path)) { pathsToDelete.Add(m.DepotPath.Path); } } } } } } } _deletedList.Remove(deleteItem); } else { ClearDeleteList(); } } else if (_renamedList.Count > 0) { FSOPRenameVO renameItem = GetFirstValidRenameItem(); if (renameItem != null) { if (currentTime.Subtract(renameItem.EventTime).TotalMilliseconds < milliSecondsDelay) { continue; } if (renameItem.type == FSType.FILE) { pathsToRename.Add(new Tuple(renameItem.oldPath, renameItem.newPath)); } else { // need to rename directory foldersToRename.Add(new Tuple(renameItem.oldPath, renameItem.newPath)); } _renamedList.Remove(renameItem); } else { ClearRenameList(); } } else if (_changedList.Count > 0) { FSOPChangeVO changeItem = _changedList[0]; if (currentTime.Subtract(changeItem.EventTime).TotalMilliseconds < milliSecondsDelay) { continue; } // Note that files being added (via a sync) can also result in a change event being raised if (_fsSyncActionMatcher.MatchAddedFile(changeItem.path)) { Log.Debug(string.Format("Ignoring edit as matched add: {0}", changeItem.path)); _fsSyncActionMatcher.RecordActionMatched(changeItem.path, "added"); } else { pathsToEdit.Add(changeItem.path); } _changedList.Remove(changeItem); } else break; } Log.Debug(string.Format("Lists - add:{0}, edit:{1}, delete:{2}, rename:{3}", pathsToAdd.Count, pathsToEdit.Count, pathsToDelete.Count, pathsToRename.Count)); } private void ProcessChanges() { Log.TraceFunction(); if (_p4 == null) _p4 = Utility.GetPerforceHelper(); lock (_processChangesLock) { // simple string collection s to hold the paths to revert, add, delete, edit, or rename List pathsToRevert = null; List pathsToAdd = null; List pathsToEdit = null; List pathsToDelete = null; List> pathsToRename = null; List> foldersToRename = null; try { _fsSyncActionMatcher.RemoveCompletedSyncActions(); GatherFileSystemChanges(out pathsToRevert, out pathsToAdd, out pathsToEdit, out pathsToDelete, out pathsToRename, out foldersToRename); if (pathsToRevert.Count > 0) { _p4.RevertFiles(serverOnly: true, paths: pathsToRevert.ToArray()); } if (pathsToAdd.Count > 0) { _p4.ReconcileFiles(paths: pathsToAdd.ToArray()); } if (pathsToEdit.Count > 0) { _p4.ReconcileFiles(paths: pathsToEdit.ToArray(), recArgs: new List() {"-ef"}); } if (pathsToDelete.Count > 0) { _p4.ReconcileFiles(paths: pathsToDelete.ToArray(), recArgs: new List() { "-df" }); } if (pathsToRename.Count > 0) { foreach (var t in pathsToRename) { _p4.RenameFile(t.Item1, t.Item2, serverOnly: true); } } if (foldersToRename.Count > 0) { foreach (var t in foldersToRename) { _p4.RenameFolder(t.Item1, t.Item2, serverOnly: true); } } if (_refreshSelector != null) { _refreshSelector(ViewModel.SELECTOR_TYPE.PENDING); _refreshSelector(ViewModel.SELECTOR_TYPE.TRASH); } else { UIHelper.RefreshSelectorAsync(ViewModel.SELECTOR_TYPE.PENDING); UIHelper.RefreshSelectorAsync(ViewModel.SELECTOR_TYPE.TRASH); } } catch (Exception ex) { Log.Exception(ex); ClearDeleteList(); ClearRenameList(); ClearCreateList(); ClearChangeList(); if (ex.Message.StartsWith("P4Command requires a P4Server")) { Utility.HandlePerforceError(); } } } } private void ClearCreateList() { _createdList = null; _createdList = new List(); } private void ClearChangeList() { _changedList = null; _changedList = new List(); } private void DeleteItemFromChangeList(String path) { if (!String.IsNullOrEmpty(path)) { FSOPChangeVO changeVO = null; foreach (FSOPChangeVO changeItem in _changedList) { if (changeItem.path.Equals(path, StringComparison.CurrentCultureIgnoreCase)) { changeVO = changeItem; break; } } if (changeVO != null) _changedList.Remove(changeVO); } } private void ClearRenameList() { _renamedList = null; _renamedList = new List(); } private FSOPRenameVO GetFirstValidRenameItem() { foreach (FSOPRenameVO renameItem in _renamedList) { return renameItem; } return null; } private void ClearDeleteList() { _deletedList = null; _deletedList = new List(); } private FSOPDeleteVO GetFirstValidDeleteItem() { foreach (FSOPDeleteVO deleteItem in _deletedList) { return deleteItem; } return null; } private FSOPDeleteVO GetDeleteItemByName(String name) { if (!String.IsNullOrEmpty(name)) { foreach (FSOPDeleteVO deleteItem in _deletedList) { String itemName = Path.GetFileName(deleteItem.path); if (itemName.Equals(name, StringComparison.CurrentCultureIgnoreCase)) return deleteItem; } } return null; } } enum FSType { FILE, FOLDER } class FSOPBase { public DateTime EventTime; public FSOPBase() { EventTime = DateTime.Now; } } class FSOPCreateVO : FSOPBase { public FSType type; private String _path; public FSOPCreateVO(String path) { this.path = path; } public String path { set { if (System.IO.File.Exists(value)) type = FSType.FILE; else if (Directory.Exists(value)) type = FSType.FOLDER; else if (Path.GetExtension(value).Equals(".tmp", StringComparison.CurrentCultureIgnoreCase)) type = FSType.FILE; _path = value; } get { return _path; } } public void printData() { Log.Debug("******************************Create*********************************\n"); Log.Debug("Type: " + type.ToString() + "\n Path: " + path); } } class FSOPRenameVO : FSOPBase { public FSType type; public String oldPath; private String _newPath; public FSOPRenameVO(String oldPath, String newPath) { this.newPath = newPath; this.oldPath = oldPath; } public String newPath { set { if (System.IO.File.Exists(value)) type = FSType.FILE; else type = FSType.FOLDER; _newPath = value; } get { return _newPath; } } public void printData() { Log.Debug("***************************Rename**************************************\n"); Log.Debug("Type: " + type + "\n Old Path: " + oldPath + "\n New Path: " + newPath); } } class FSOPDeleteVO : FSOPBase { public String path; public FSOPDeleteVO(String path) { this.path = path; } public void printData() { Log.Debug("**********************Delete****************************************\n"); Log.Debug("Delete Path: " + path); String itemName = Path.GetFileName(path); Log.Debug("Delete Name: " + itemName); } } class FSOPChangeVO : FSOPBase { public String path; public FSOPChangeVO(String path) { this.path = path; } } }