using UnityEditor; using UnityEngine; using System; using System.Threading; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using Perforce.P4; using log4net; namespace P4Connect { /// /// Stores and periodically updates the Perforce status of all the assets /// public class AssetStatusCache { private static readonly ILog log = LogManager.GetLogger(typeof(AssetStatusCache)); /// /// Simple asset status struct /// Stores the file state (checked out, modified, etc...) and revision state /// public struct AssetStatus { public DateTime TimeStamp; public FileState LocalState; public FileState OtherState; public DepotState DepotState; public RevisionState RevisionState; public ResolvedState ResolvedState; public StorageType StorageType; public LockState LockState; // Use this to create an AssetStatus for a local file (perforce would return null from the fstat call) public AssetStatus(string assetPath) { TimeStamp = default(DateTime); LocalState = FileState.None; OtherState = FileState.None; DepotState = DepotState.None; RevisionState = P4Connect.RevisionState.None; ResolvedState = P4Connect.ResolvedState.None; StorageType = StorageType.Text; LockState = LockState.None; } public override string ToString() { StringBuilder sb = new StringBuilder(); if (TimeStamp == default(DateTime)) { sb.AppendFormat("Dummy AssetStatus"); } else { sb.AppendFormat("TimeStamp: {0} ", TimeStamp); sb.AppendFormat("LocalState: {0} ", LocalState); sb.AppendFormat("OtherState: {0} ", OtherState); sb.AppendFormat("DepotState: {0} ", DepotState); sb.AppendFormat("Revision: {0} ", RevisionState); sb.AppendFormat("Resolved: {0} ", ResolvedState); sb.AppendFormat("Storage: {0} ", StorageType); sb.AppendFormat("Lockstate: {0} ", LockState); } return (sb.ToString()); } public bool IsOnServer() { return (LocalState != FileState.None && LocalState != FileState.MarkedForAdd); } public bool IsLocked() { return (LockState == LockState.OurLock || LockState == LockState.TheirLock); } public bool IsOpened() { return (! (LocalState == FileState.None || LocalState == FileState.InDepot)); } public bool IsInitialized() { return (this.TimeStamp != default(DateTime)); } } /// /// This class is used to store the cached results for each "node" we keep track of in this project /// public class AssetEntry { public AssetStatus status; public FileMetaData meta; public DateTime update; public AssetEntry() { status = new AssetStatus(); meta = new FileMetaData(); status.TimeStamp = update = default(DateTime); } } /// /// This delegate is triggered when the status of an asset changes /// public delegate void OnAssetStatusChangedDelegate(PerforceConnection aConnection); public static event OnAssetStatusChangedDelegate OnAssetStatusChanged { add { if (_OnAssetStatusChanged == null) { // We had no reason to get updated before, so do it now Engine.OnOperationPerformed += OnEngineOperationPerformed; } _OnAssetStatusChanged += value; } remove { _OnAssetStatusChanged -= value; if (_OnAssetStatusChanged == null) { // We no longer have any reason to be updated Engine.OnOperationPerformed -= OnEngineOperationPerformed; } } } // The internal event that is actually registered with static event OnAssetStatusChangedDelegate _OnAssetStatusChanged; // The caches static Dictionary _Entries; public static void Initialize() { _Entries = new Dictionary(); PendingDirty = true; } /// /// Generic method that fetches the cached AssetEntry which includes /// AssetStatus (checked out, out of date, etc...) /// FileMetaData (server "fstat" result) /// public static AssetEntry GetAssetEntry(string aAssetPath) { AssetEntry entry; string path = GetEffectivePath(aAssetPath); if (! _Entries.TryGetValue(path, out entry)) { //log.Debug("Creating DEFAULT: " + path); entry = new AssetEntry(); _Entries[path] = entry; } return entry; } public static AssetStatus GetAssetStatus(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.status; } public static FileMetaData GetMetaData(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.meta; } public static FileMetaData GetMetaData(FileSpec spec) { string path = Utils.LocalPathToAssetPath(spec.LocalPath.Path); AssetEntry entry = GetAssetEntry(path); return entry.meta; } public static DateTime GetUpdated(string aAssetPath) { AssetEntry entry = GetAssetEntry(aAssetPath); return entry.update; } // A flag for pending changes to check if something has changedIgnore file name public static bool PendingDirty { get; set; } /// /// Called from the Icons code. /// update the status of each file in the list /// Check for cached entries, if any are not, ask the server for them. /// /// List of Asset Paths public static void UpdateAssetData(List aAssetPaths) { if (! Config.ValidConfiguration) return; // Remember the list HashSet assets = new HashSet(aAssetPaths); // Check each asset for a valid status foreach(string asset in aAssetPaths) { AssetStatus status = GetAssetStatus(asset); if (status.IsInitialized()) { assets.Remove(asset); } } if (assets.Any()) { // Get any left-overs from the server Engine.PerformConnectionOperation(con => { UpdateAssetMetaData(con, GetFileSpecs(assets).ToList()); }); } } /// /// Given the meta data, figures out the P4Connect icon representation for the file /// There is only a subset of all the metadata that P4Connect actually displays /// static AssetStatus CreateAssetStatus(FileMetaData aMeta) { //Debug.Log("CreateAssetStatus: " + Logger.FileMetaDataToString(aMeta)); AssetStatus retData = new AssetStatus(); if (aMeta == null) return retData; // Fill out the struct switch (aMeta.Type.BaseType) { case BaseFileType.Text: retData.StorageType = StorageType.Text; break; case BaseFileType.Binary: retData.StorageType = StorageType.Binary; break; default: retData.StorageType = StorageType.Other; break; } retData.LocalState = Engine.ParseFileAction(aMeta.Action); if (retData.LocalState == FileState.None) { // Check to see if Perforce knows this file so we can override the localState to InDepot. if (aMeta.IsMapped || aMeta.HeadAction != FileAction.None) retData.LocalState = FileState.InDepot; } if (aMeta.OtherActions != null) { retData.OtherState = Engine.ParseFileAction(aMeta.OtherActions[0]); } if (aMeta.HaveRev >= aMeta.HeadRev) retData.RevisionState = RevisionState.HasLatest; else retData.RevisionState = RevisionState.OutOfDate; if (aMeta.Unresolved) retData.ResolvedState = ResolvedState.NeedsResolve; else retData.ResolvedState = ResolvedState.None; if (aMeta.HeadAction == FileAction.MoveDelete || aMeta.HeadAction == FileAction.Delete) retData.DepotState = DepotState.Deleted; else retData.DepotState = DepotState.None; retData.TimeStamp = DateTime.Now; retData.LockState = Engine.GetLockState(aMeta); // Debug.Log("AssetStatus: " + retData.ToString()); return retData; } /// /// This functions gets called when P4Connect does *something* to files /// In this case we update the files status /// static void OnEngineOperationPerformed(PerforceConnection aConnection, List aFilesAndMetas) { if (aFilesAndMetas.Count > 0) { List allSpecs = new List(); foreach (var fileAndMeta in aFilesAndMetas) { if (fileAndMeta.File != null) { allSpecs.Add(fileAndMeta.File); } if (fileAndMeta.Meta != null) { allSpecs.Add(fileAndMeta.Meta); } } log.Debug("files changed: " + Logger.FileAndMetaListToString(aFilesAndMetas)); // Only update entries with version numbers allSpecs = allSpecs.ValidSpecs().ToList(); // But we can't pass the versions to fstat UpdateAssetMetaData(aConnection, allSpecs.UnversionedSpecs().ToList()); // Refresh the cache if (_OnAssetStatusChanged != null) { _OnAssetStatusChanged(aConnection); } } } /// /// Given an asset path, mark this asset as Dirty /// So it will be re-fstated later. /// /// static public void MarkAsDirty(string assetPath) { //log.Debug("DIRTY: " + assetPath); var entry = GetAssetEntry(assetPath); entry.update = default(DateTime); entry.status.TimeStamp = entry.update; } /// /// Mark the contents of a List of FileAndMetas as dirty /// /// FileAndMetas Collection static public void MarkAsDirty(IEnumerable fam) { if (fam == null) return; foreach(var f in fam) { if (f.File != null) { MarkAsDirty(f.File.ToAssetPath()); } if (f.Meta != null) { MarkAsDirty(f.Meta.ToAssetPath()); } } } /// /// Using the Perforce connection, get a matching AssetStatus for each Assetpath /// /// Perforce Connection /// A List of asset paths to retrieve /// A list of statuses, one per aFile static public void GetAssetStatuses(PerforceConnection aConnection, List aFiles, List aOutStatuses) { GetAssetStatuses(aConnection, GetFileSpecs(aFiles).ToList(), aOutStatuses); } /// /// Using Perforce connection, get a matching AssetStatus for each Assetpath /// /// Perforce Connection /// A List of asset paths to retrieve /// A list of statuses, one per aFile< static public List GetAssetStatuses(PerforceConnection aConnection, List aFiles) { List aOutStatuses = new List(); GetAssetStatuses(aConnection, aFiles, aOutStatuses); return aOutStatuses; } /// /// Using the Perforce connection, get a matching AssetStatus for each FileSpec /// If the metadata for a file is not in Perforce, we provide a "local" AssetStatus instead. /// /// Perforce Connection /// Files to look up /// AssetStatus List returned, one for each File static void GetAssetStatuses(PerforceConnection aConnection, List aFiles, List aOutStatuses) { IEnumerable asset_names = GetAssetPathsFromFileSpecs(aFiles); string[] files = asset_names.ToArray(); //Debug.Log("retrieving: " + Logger.StringArrayToString(files)); List not_initialized = new List(); // Collect results here for return later AssetStatus[] stats = new AssetStatus[files.Count()]; // Get The Asset Status for each file, if not initialized add to "not_initialized" list for (int i = 0; i < files.Count(); i++ ) { stats[i] = GetAssetStatus(files[i]); if (! stats[i].IsInitialized()) { not_initialized.Add(files[i]); } } // Now Ask for any uninitialized AssetStatus's from the server if (not_initialized.Count > 0) { log.Debug("Going to server for: " + Logger.StringArrayToString(not_initialized.ToArray())); Engine.PerformConnectionOperation(con => { UpdateAssetMetaData(aConnection, GetFileSpecs(not_initialized).ToList()); }); } // Finally update the "holes" in the statuses originally returned for (int i = 0; i < files.Count(); i++) { if (!stats[i].IsInitialized()) { stats[i] = GetAssetStatus(files[i]); } } aOutStatuses.AddRange(stats); //log.Debug("assetstatuses: " + Logger.AssetStatusListToString(aOutStatuses)); } /// /// Goes to the server, updates cache with latest about the files specified /// /// Connection to use /// Files to retrieve info about static void UpdateAssetMetaData(PerforceConnection aConnection, List aFiles) { if (aFiles != null && aFiles.Count > 0) { aFiles = aFiles.UnversionedSpecs().ToList(); //log.Debug("aFiles: " + Logger.FileSpecListToString(aFiles)); HashSet original = new HashSet(GetAssetPathsFromFileSpecs(aFiles)); HashSet checked_list = new HashSet(); DateTime startTimestamp = DateTime.Now; Options fsopts = new Options(); fsopts["-Op"] = null; // Return "real" clientpaths var mdlist = Engine.GetFileMetaData(aConnection, aFiles, fsopts); if (mdlist != null) { // with a good response, we store the retrieved meta data in the AssetCache StorePerforceMetaData(mdlist); foreach (var md in mdlist) { checked_list.Add(GetEffectivePath(Utils.LocalPathToAssetPath(md.LocalPath.Path))); } } // For "leftover" files not recognized by the server, we provide an updated AssetEntry IEnumerable left_overs = original.Except(checked_list); StoreLocalMetaData(left_overs); if (Config.DisplayP4Timings) { double deltaInnerTime = (DateTime.Now - startTimestamp).TotalMilliseconds; aConnection.AppendTimingInfo("UpdateAssetMetaData " + deltaInnerTime.ToString() + " ms"); } } } /// /// We have fresh metadata from the server /// Lets store it in the Asset Cache /// /// List of Metadata to store static public void StorePerforceMetaData(IList mdlist) { if (mdlist == null) return; foreach (var md in mdlist) { // Create the AssetStatus and FileMetaData entries for each file known by the server string fname = GetEffectivePath(Utils.LocalPathToAssetPath(md.LocalPath.Path)); AssetEntry entry = new AssetEntry(); entry.meta = md; entry.status = CreateAssetStatus(md); entry.update = DateTime.Now; _Entries[fname] = entry; //log.Debug("AssetEntry: " + fname + " CREATED " + entry.status.ToStringNullSafe()); } } /// /// The metadata from the server came back, and did not include these files /// So we need to make valid Cache entries as if they are Local files only /// /// Asset Paths of files to cache static public void StoreLocalMetaData(IEnumerable files) { if (files == null) return; // For "leftover" files not known by the server, we provide a default // AssetEntry with the update field set. foreach(string f in files) { AssetEntry entry = new AssetEntry(); entry.meta = new FileMetaData(); entry.status = new AssetStatus(); entry.status.TimeStamp = entry.update = DateTime.Now; _Entries[f] = entry; //log.Debug("AssetEntry: " + f + " CREATED Local " + entry.status.ToStringNullSafe()); } } /// /// Returns a proper list of file spec given a list of paths, accounting for directories /// /// Asset Path strings /// FileSpecs static IEnumerable GetFileSpecs(IEnumerable aAssetPaths) { foreach (var path in aAssetPaths) { yield return FileSpec.LocalSpec(Utils.AssetPathToLocalPath(GetEffectivePath(path))); } yield break; } /// /// Return a collection of Asset paths given a list of FileSpecs /// (using the LocalPath of the FileSpec) /// /// FileSpecs /// strings static IEnumerable GetAssetPathsFromFileSpecs(IEnumerable fs) { foreach(var f in fs) { yield return Utils.LocalPathToAssetPath(f.LocalPath.Path); } yield break; } /// /// Return a collection of Asset paths given a list of FileMetaData /// /// FileMetaData collection /// string collection static IEnumerable GetAssetPathsFromFileMetaData(IEnumerablefmd_list) { foreach(var fm in fmd_list) { yield return Utils.LocalPathToAssetPath(fm.LocalPath.Path); } yield break; } /// /// Returns the path to use when printing the name, or looking up status info /// static string GetEffectivePath(string aAssetPath) { if (Utils.IsDirectory(aAssetPath)) { return Utils.MetaFromAsset(aAssetPath); } else { return aAssetPath; } } } }