// Copyright(c) 2019 pypy. All rights reserved. // // This work is licensed under the terms of the MIT license. // For a copy, see . using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Threading; namespace VRCX { public class LogWatcher { private class LogContext { public long Length; public long Position; public string RecentWorldName; } public static readonly LogWatcher Instance; private readonly DirectoryInfo m_LogDirectoryInfo; private readonly Dictionary m_LogContextMap; // private readonly ReaderWriterLockSlim m_LogListLock; private readonly List m_LogList; private Thread m_Thread; private bool m_ResetLog; // NOTE // FileSystemWatcher() is unreliable static LogWatcher() { Instance = new LogWatcher(); } public LogWatcher() { var logPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"Low\VRChat\VRChat"; m_LogDirectoryInfo = new DirectoryInfo(logPath); m_LogContextMap = new Dictionary(); m_LogListLock = new ReaderWriterLockSlim(); m_LogList = new List(); m_Thread = new Thread(ThreadLoop) { IsBackground = true }; } internal void Init() { m_Thread.Start(); } internal void Exit() { var thread = m_Thread; m_Thread = null; thread.Interrupt(); thread.Join(); } private void ThreadLoop() { while (m_Thread != null) { Update(); try { Thread.Sleep(1000); } catch { // ThreadInterruptedException } } } private void Update() { if (m_ResetLog == true) { m_ResetLog = false; m_LogContextMap.Clear(); m_LogListLock.EnterWriteLock(); try { m_LogList.Clear(); } finally { m_LogListLock.ExitWriteLock(); } } var deletedNameSet = new HashSet(m_LogContextMap.Keys); m_LogDirectoryInfo.Refresh(); if (m_LogDirectoryInfo.Exists == true) { var fileInfos = m_LogDirectoryInfo.GetFiles("output_log_*.txt", SearchOption.TopDirectoryOnly); // sort by creation time Array.Sort(fileInfos, (a, b) => a.CreationTimeUtc.CompareTo(b.CreationTimeUtc)); var utcNow = DateTime.UtcNow; var minLimitDateTime = utcNow.AddDays(-7d); var minRefreshDateTime = utcNow.AddMinutes(-3d); foreach (var fileInfo in fileInfos) { var lastWriteTimeUtc = fileInfo.LastWriteTimeUtc; if (lastWriteTimeUtc < minLimitDateTime) { continue; } if (lastWriteTimeUtc >= minRefreshDateTime) { fileInfo.Refresh(); if (fileInfo.Exists == false) { continue; } } if (m_LogContextMap.TryGetValue(fileInfo.Name, out LogContext logContext) == true) { deletedNameSet.Remove(fileInfo.Name); } else { logContext = new LogContext(); m_LogContextMap.Add(fileInfo.Name, logContext); } if (logContext.Length == fileInfo.Length) { continue; } logContext.Length = fileInfo.Length; ParseLog(fileInfo, logContext); } } foreach (var name in deletedNameSet) { m_LogContextMap.Remove(name); } } private void ParseLog(FileInfo fileInfo, LogContext logContext) { try { using (var stream = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 65536, FileOptions.SequentialScan)) { stream.Position = logContext.Position; using (var streamReader = new StreamReader(stream, Encoding.UTF8)) { while (true) { var line = streamReader.ReadLine(); if (line == null) { logContext.Position = stream.Position; break; } // 2020.10.31 23:36:28 Log - [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2021.02.03 10:18:58 Log - [DŽDŽDžDžDžDŽDŽDžDžDŽDžDžDžDžDŽDŽDŽDžDžDŽDŽDžDžDžDžDŽDžDžDžDžDŽDŽDŽDŽDŽDžDŽDžDŽDŽDŽDžDžDŽDžDžDž] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd if (line.Length <= 36 || line[31] != '-') { continue; } if (line[34] == '[') { var offset = line.IndexOf("] ", 35, StringComparison.Ordinal); if (offset >= 35) { offset += 2; if (ParseLogOnPlayerJoinedOrLeft(fileInfo, logContext, line, offset) == true || ParseLogLocation(fileInfo, logContext, line, offset) == true) { continue; } } continue; } if (ParseLogNotification(fileInfo, logContext, line, 34) == true) { continue; } } } } } catch { } } private void AppendLog(string[] item) { m_LogListLock.EnterWriteLock(); try { m_LogList.Add(item); } finally { m_LogListLock.ExitWriteLock(); } } private string ConvertLogTimeToISO8601(string line) { // 2020.10.31 23:36:22 if (DateTime.TryParseExact( line.Substring(0, 19), "yyyy.MM.dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal, out DateTime dt ) == false) { dt = DateTime.UtcNow; } return $"{dt:yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'}"; } private bool ParseLogLocation(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2020.10.31 23:36:28 Log - [VRCFlowManagerVRC] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2020.10.31 23:36:28 Log - [VRCFlowManagerVRC] Destination set: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd // 2020.10.31 23:36:31 Log - [RoomManager] Entering Room: VRChat Home // 2020.10.31 23:36:31 Log - [RoomManager] Joining wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd:67646~private(usr_4f76a584-9d4b-46f6-8209-8305eb683661)~nonce(D9298A536FEEEDDBB61633661A4BDAA09717C5178DEF865C4C09372FE12E09A6) // 2020.10.31 23:36:31 Log - [RoomManager] Joining or Creating Room: VRChat Home // 2020.10.31 23:36:31 Log - [RoomManager] Successfully joined room // 2021.02.03 10:18:58 Log - [DŽDŽDžDžDžDŽDŽDžDžDŽDžDžDžDžDŽDŽDŽDžDžDŽDŽDžDžDžDžDŽDžDžDžDžDŽDŽDŽDŽDŽDžDŽDžDŽDŽDŽDžDžDŽDžDžDž] Destination fetching: wrld_4432ea9b-729c-46e3-8eaf-846aa0a37fdd if (string.Compare(line, offset, "Entering Room: ", 0, 15, StringComparison.Ordinal) == 0) { var worldName = line.Substring(offset + 15); logContext.RecentWorldName = worldName; return true; } if (string.Compare(line, offset, "Joining wrld_", 0, 13, StringComparison.Ordinal) == 0) { var location = line.Substring(offset + 8); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "location", location, logContext.RecentWorldName }); return true; } return false; } private bool ParseLogOnPlayerJoinedOrLeft(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2020.10.31 23:36:58 Log - [NetworkManager] OnPlayerJoined pypy // 2020.10.31 23:36:58 Log - [Player] Initialized PlayerAPI "pypy" is local // 2020.10.31 23:36:58 Log - [NetworkManager] OnPlayerJoined Rize♡ // 2020.10.31 23:36:58 Log - [Player] Initialized PlayerAPI "Rize♡" is remote // 2020.11.01 00:07:01 Log - [NetworkManager] OnPlayerLeft Rize♡ // 2020.11.01 00:07:01 Log - [PlayerManager] Removed player 2 / Rize♡ // 2020.11.01 00:07:02 Log - [Player] Unregistering Rize♡ if (string.Compare(line, offset, "Initialized PlayerAPI \"", 0, 23, StringComparison.Ordinal) == 0) { var pos = line.LastIndexOf("\" is "); if (pos < 0) { return false; } var userDisplayName = line.Substring(offset + 23, pos - (offset + 23)); var userType = line.Substring(pos + 5); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "player-joined", userDisplayName, userType, }); return true; } // fallback method /*if (string.Compare(line, offset, "OnPlayerJoined ", 0, 15, StringComparison.Ordinal) == 0) { var userDisplayName = line.Substring(offset + 15); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "player-joined", userDisplayName }); return true; }*/ if (string.Compare(line, offset, "OnPlayerLeft ", 0, 13, StringComparison.Ordinal) == 0) { var userDisplayName = line.Substring(offset + 13); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "player-left", userDisplayName }); return true; } return false; } private bool ParseLogNotification(FileInfo fileInfo, LogContext logContext, string line, int offset) { // 2021.01.03 05:48:58 Log - Received Notification: < Notification from username:pypy, sender user id:usr_4f76a584-9d4b-46f6-8209-8305eb683661 to of type: friendRequest, id: not_3a8f66eb-613c-4351-bee3-9980e6b5652c, created at: 01/14/2021 15:38:40 UTC, details: {{}}, type:friendRequest, m seen:False, message: ""> received at 01/02/2021 16:48:58 UTC if (string.Compare(line, offset, "Received Notification: <", 0, 24, StringComparison.Ordinal) != 0) { return false; } var pos = line.LastIndexOf("> received at "); if (pos < 0) { return false; } var data = line.Substring(offset + 24, pos - (offset + 24)); AppendLog(new[] { fileInfo.Name, ConvertLogTimeToISO8601(line), "notification", data }); return true; } public void Reset() { m_ResetLog = true; m_Thread?.Interrupt(); } public string[][] Get() { if (m_ResetLog == false && m_LogList.Count > 0) { m_LogListLock.EnterWriteLock(); try { string[][] items; if (m_LogList.Count > 1000) { items = new string[1000][]; m_LogList.CopyTo(0, items, 0, 1000); m_LogList.RemoveRange(0, 1000); } else { items = m_LogList.ToArray(); m_LogList.Clear(); } return items; } finally { m_LogListLock.ExitWriteLock(); } } return new string[][] { }; } } }