// // Copyright 2014 Perforce Software Inc. // using Perforce.P4; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Timers; namespace Perforce.Helper { public class WorkspaceWatcher { private FileSystemWatcher parentFolderWatcher = null, subfolderWatcher = null; private System.Object lockThis = new System.Object(), processChangesLock = new System.Object(); private System.Timers.Timer changeNotifier; private List createList = new List(); private List deleteList = new List(); private List renameList = new List(); private List changeList = new List(); private String watchPath = null; public WorkspaceWatcher() { try { var helper = Utility.GetPerforceHelper(); if (helper != null && helper.ClientEnabled) { this.watchPath = helper.CurrentClient.Root; } } catch (Exception e) { Console.Error.WriteLine(e.StackTrace); } } public string WatchPath { get { return watchPath; } set { watchPath = value; } } public bool Start() { try { if (!String.IsNullOrEmpty(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(1000); changeNotifier.Elapsed += new ElapsedEventHandler(handleFileSystemChange); return true; } } catch (Exception e) { Console.Error.WriteLine(e.StackTrace); } return false; } public bool Stop() { try { parentFolderWatcher.EnableRaisingEvents = false; subfolderWatcher.EnableRaisingEvents = false; parentFolderWatcher.Dispose(); subfolderWatcher.Dispose(); parentFolderWatcher = null; subfolderWatcher = null; return true; } catch (Exception e) { Console.Error.WriteLine(e.StackTrace); } return false; } private void handleChangeEvent(Object sender, FileSystemEventArgs args) { Log.Debug(string.Format("change event {0}", args.FullPath)); Thread.Sleep(100); lock (lockThis) { if (args.FullPath.StartsWith(watchPath)) { changeNotifier.Enabled = false; if (System.IO.File.Exists(args.FullPath)) { changeList.Add(new FSOPChangeVO(args.FullPath)); } changeNotifier.Enabled = true; } } } private void handleCreateEvent(Object sender, FileSystemEventArgs args) { 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)) { createList.Add(new FSOPCreateVO(args.FullPath)); } changeNotifier.Enabled = true; } } } private void handleDeleteEvent(Object sender, FileSystemEventArgs args) { Log.Debug(string.Format("delete event {0}", args.FullPath)); Thread.Sleep(100); lock (lockThis) { changeNotifier.Enabled = false; if (!System.IO.File.Exists(args.FullPath)) { deleteList.Add(new FSOPDeleteVO(args.FullPath)); } changeNotifier.Enabled = true; } } private void handleRenameEvent(Object sender, RenamedEventArgs args) { Log.Debug(string.Format("rename event {0}", args.FullPath)); Thread.Sleep(100); lock (lockThis) { changeNotifier.Enabled = false; if (!System.IO.File.Exists(args.OldFullPath)) { renameList.Add(new FSOPRenameVO(args.OldFullPath, args.FullPath)); } changeNotifier.Enabled = true; } } private void handleFileSystemChange(Object sender, ElapsedEventArgs args) { changeNotifier.Enabled = false; processChanges(); } private void processChanges() { var helper = Utility.GetPerforceHelper(); lock (processChangesLock) { // simple string collections to hold the paths to revert, add, delete, edit, or rename List pathsToRevert = new List(); List pathsToAdd = new List(); List pathsToEdit = new List(); List pathsToDelete = new List(); List> pathsToRename = new List>(); List> foldersToRename = new List>(); try { while (true) { FSOPCreateVO createItem = null; FSOPDeleteVO deleteItem = null; if (createList.Count > 0) { if ((createItem = createList[0]) != null && deleteList.Count > 0 && (deleteItem = getDeleteItemByName(Path.GetFileName(createItem.path))) != null) { createList.Remove(createItem); deleteList.Remove(deleteItem); renameList.Add(new FSOPRenameVO(deleteItem.path, createItem.path)); } else { if (createItem.type == FSType.FILE) { if (System.IO.File.Exists(createItem.path)) { pathsToAdd.Add(createItem.path); //var results = helper.MarkFileForAdd(createItem.path); } } else if (createItem.type == FSType.FOLDER) { var allFiles = Directory.GetFiles(createItem.path, "*.*", SearchOption.AllDirectories); if (allFiles != null && allFiles.Length > 0) { //helper.MarkFileForAdd(allFiles); pathsToAdd.AddRange(allFiles); } } createList.Remove(createItem); deleteItemFromChangeList(createItem.path); } } else if (deleteList.Count > 0) { deleteItem = getFirstValidDeleteItem(); if (deleteItem != null) { // 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 var md = helper.GetFileMetaData(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 //helper.RevertFiles(serverOnly: true, paths: deleteItem.path); pathsToRevert.Add(deleteItem.path); } else { if (md.Action != FileAction.None) { //helper.RevertFiles(serverOnly: true, paths: deleteItem.path); pathsToRevert.Add(deleteItem.path); } if (!System.IO.File.Exists(deleteItem.path)) { //helper.DeleteFiles(serverOnly: true, paths: 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 = helper.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) { //helper.RevertFiles(serverOnly: true, paths: m.DepotPath.Path); pathsToRevert.Add(m.DepotPath.Path); } else { if (m.Action != FileAction.None) { //helper.RevertFiles(serverOnly: true, paths: m.DepotPath.Path); pathsToRevert.Add(m.DepotPath.Path); } if (!System.IO.File.Exists(m.LocalPath.Path)) { //helper.DeleteFiles(serverOnly: true, paths: m.DepotPath.Path); pathsToDelete.Add(m.DepotPath.Path); } } } } } } deleteList.Remove(deleteItem); } else { clearDeleteList(); } } else if (renameList.Count > 0) { FSOPRenameVO renameItem = getFirstValidRenameItem(); if (renameItem != null) { if (renameItem.type == FSType.FILE) { pathsToRename.Add(new Tuple(renameItem.oldPath, renameItem.newPath)); //helper.RenameFile(renameItem.oldPath, renameItem.newPath, serverOnly: true); } else { // need to rename directory //helper.RenameFolder(renameItem.oldPath, renameItem.newPath, serverOnly: true); foldersToRename.Add(new Tuple(renameItem.oldPath, renameItem.newPath)); } renameList.Remove(renameItem); } else { clearRenameList(); } } else if (changeList.Count > 0) { FSOPChangeVO changeItem = changeList[0]; //helper.CheckoutFiles(false, changeItem.path); pathsToEdit.Add(changeItem.path); changeList.Remove(changeItem); } else break; } if (pathsToRevert.Count > 0) { helper.RevertFiles(serverOnly: true, paths: pathsToRevert.ToArray()); } if (pathsToEdit.Count > 0) { helper.CheckoutFiles(serverOnly: false, paths: pathsToEdit.ToArray()); } if (pathsToAdd.Count > 0) { helper.MarkFileForAdd(paths: pathsToAdd.ToArray()); } if (pathsToDelete.Count > 0) { helper.DeleteFiles(serverOnly: true, paths: pathsToDelete.ToArray()); } if (pathsToRename.Count > 0) { foreach (var t in pathsToRename) { helper.RenameFile(t.Item1, t.Item2, serverOnly: true); } } if (foldersToRename.Count > 0) { foreach (var t in foldersToRename) { helper.RenameFolder(t.Item1, t.Item2, serverOnly: true); } } UIHelper.RefreshSelectorAsync(ViewModel.SELECTOR_TYPE.PENDING); UIHelper.RefreshSelectorAsync(ViewModel.SELECTOR_TYPE.TRASH); } catch (Exception ex) { Log.Error(ex.Message); clearDeleteList(); clearRenameList(); clearCreateList(); clearChangeList(); } } } private void clearCreateList() { createList = null; createList = new List(); } private void clearChangeList() { changeList = null; changeList = new List(); } private void deleteItemFromChangeList(String path) { if (!String.IsNullOrEmpty(path)) { FSOPChangeVO changeVO = null; foreach (FSOPChangeVO changeItem in changeList) { if (changeItem.path.Equals(path, StringComparison.CurrentCultureIgnoreCase)) { changeVO = changeItem; break; } } if (changeVO != null) changeList.Remove(changeVO); } } private void clearRenameList() { renameList = null; renameList = new List(); } private FSOPRenameVO getFirstValidRenameItem() { foreach (FSOPRenameVO renameItem in renameList) { return renameItem; } return null; } private void clearDeleteList() { deleteList = null; deleteList = new List(); } private FSOPDeleteVO getDeleteItem(String path) { if (!String.IsNullOrEmpty(path)) { foreach (FSOPDeleteVO deleteItem in deleteList) { if (deleteItem.path.Equals(path, StringComparison.CurrentCultureIgnoreCase)) return deleteItem; } } return null; } private FSOPDeleteVO getFirstValidDeleteItem() { foreach (FSOPDeleteVO deleteItem in deleteList) { return deleteItem; } return null; } private FSOPDeleteVO getDeleteItemByName(String name) { if (!String.IsNullOrEmpty(name)) { foreach (FSOPDeleteVO deleteItem in deleteList) { String itemName = Path.GetFileName(deleteItem.path); if (itemName.Equals(name, StringComparison.CurrentCultureIgnoreCase)) return deleteItem; } } return null; } private FSOPRenameVO getRenameItem(String oldPath) { if (!String.IsNullOrEmpty(oldPath)) { foreach (FSOPRenameVO renameItem in renameList) { if (renameItem.oldPath.Equals(oldPath, StringComparison.CurrentCultureIgnoreCase)) return renameItem; } } return null; } private void deleteItemFromCreateList(String path) { if (!String.IsNullOrEmpty(path)) { FSOPCreateVO createVO = null; foreach (FSOPCreateVO createItem in createList) { if (createItem.path.Equals(path, StringComparison.CurrentCultureIgnoreCase)) { createVO = createItem; break; } } if (createVO != null) createList.Remove(createVO); } } private void deleteItemFromDeleteList(String path) { if (!String.IsNullOrEmpty(path)) { FSOPDeleteVO deleteVO = null; foreach (FSOPDeleteVO deleteItem in deleteList) { if (deleteItem.path.Equals(path, StringComparison.CurrentCultureIgnoreCase)) { deleteVO = deleteItem; break; } } if (deleteVO != null) deleteList.Remove(deleteVO); } } } enum FSType { FILE, FOLDER } class FSOPCreateVO { 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() { Console.WriteLine("******************************Create*********************************\n"); Console.WriteLine("Type: " + type.ToString() + "\n Path: " + path); } } class FSOPRenameVO { 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() { Console.WriteLine("***************************Rename**************************************\n"); Console.WriteLine("Type: " + type + "\n Old Path: " + oldPath + "\n New Path: " + newPath); } } class FSOPDeleteVO { public String path; public FSOPDeleteVO(String path) { this.path = path; } public void printData() { Console.WriteLine("**********************Delete****************************************\n"); Console.WriteLine("Delete Path: " + path); String itemName = Path.GetFileName(path); Console.WriteLine("Delete Name: " + itemName); } } class FSOPChangeVO { public String path; public FSOPChangeVO(String path) { this.path = path; } } }