懒人记时 代码仓库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

418 lines
13 KiB

  1. 
  2. using AppTime.Properties;
  3. using System;
  4. using System.Collections.Generic;
  5. using System.ComponentModel;
  6. using System.Data.SQLite;
  7. using System.Diagnostics;
  8. using System.Drawing;
  9. using System.Drawing.Imaging;
  10. using System.Globalization;
  11. using System.IO;
  12. using System.Linq;
  13. using System.Runtime.InteropServices;
  14. using System.Text;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using System.Windows.Forms;
  18. namespace AppTime
  19. {
  20. class Recorder
  21. {
  22. public const string ExName = "mkv";
  23. public Recorder()
  24. {
  25. BuildDataPath();
  26. }
  27. /// <summary>
  28. /// 统计周期。
  29. /// </summary>
  30. public int IntervalMs = 1000;
  31. public void Start()
  32. {
  33. new Thread(RecorderThreadProc) { IsBackground = true }.Start();
  34. }
  35. class App
  36. {
  37. public string WinText;
  38. public string AppProcess;
  39. public DateTime TimeStart;
  40. public long WinId;
  41. }
  42. public void BuildDataPath()
  43. {
  44. Directory.CreateDirectory(IconPath);
  45. Directory.CreateDirectory(ScreenPath);
  46. }
  47. Dictionary<int, Process> processes;
  48. public Process GetProcess(int processID)
  49. {
  50. if (processes != null && processes.TryGetValue(processID, out var p))
  51. {
  52. return p;
  53. }
  54. processes = Process.GetProcesses().ToDictionary(p => p.Id);
  55. return processes[processID];
  56. }
  57. class app
  58. {
  59. public long id;
  60. public string process;
  61. public Dictionary<string, win> wins = new Dictionary<string, win>();
  62. }
  63. class win
  64. {
  65. public long id;
  66. public string text;
  67. }
  68. long nextAppId = 0;
  69. Icon GetIcon(string fileName, bool largeIcon)
  70. {
  71. var shfi = new SHFILEINFO();
  72. WinApi.SHGetFileInfo(fileName, 0, ref shfi,
  73. (uint)Marshal.SizeOf(shfi),
  74. (uint)FileInfoFlags.SHGFI_ICON | (uint)FileInfoFlags.SHGFI_USEFILEATTRIBUTES
  75. | (uint)(largeIcon ? FileInfoFlags.SHGFI_LARGEICON : FileInfoFlags.SHGFI_SMALLICON)
  76. );
  77. return Icon.FromHandle(shfi.hIcon);
  78. }
  79. void SaveIcon(Icon icon, string filename)
  80. {
  81. using var img = icon.ToBitmap();
  82. img.Save(filename);
  83. }
  84. public string GetIconPath(long appId, bool large)
  85. {
  86. return Path.Combine(IconPath, $"{appId}{(large ? "l" : "s")}.png");
  87. }
  88. Dictionary<string, app> apps = new Dictionary<string, app>();
  89. app GetApp(Process process)
  90. {
  91. var name = process.ProcessName;
  92. if (apps.TryGetValue(name, out var app))
  93. {
  94. return app;
  95. }
  96. var data = db.ExecuteDynamic(
  97. @"select id from app where process = @process",
  98. new SQLiteParameter("process", name)
  99. ).FirstOrDefault();
  100. if (data == null)
  101. {
  102. if (nextAppId == 0)
  103. {
  104. nextAppId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from app")[0][0];
  105. }
  106. var text = "";
  107. try
  108. {
  109. text = process.MainModule.FileVersionInfo.FileDescription;
  110. using var iconl = GetIcon(process.MainModule.FileName, true);
  111. SaveIcon(iconl, GetIconPath(nextAppId, true));
  112. using var icons = GetIcon(process.MainModule.FileName, false);
  113. SaveIcon(icons, GetIconPath(nextAppId, false));
  114. }
  115. catch (Win32Exception)
  116. {
  117. //ignore
  118. }
  119. catch (FileNotFoundException)
  120. {
  121. //ignore
  122. }
  123. if (string.IsNullOrWhiteSpace(text))
  124. {
  125. text = process.ProcessName;
  126. }
  127. db.Execute(
  128. "insert into app (id, process, text, tagId) values(@id, @process, @text, 0)",
  129. new SQLiteParameter("id", nextAppId),
  130. new SQLiteParameter("process", name),
  131. new SQLiteParameter("text", text)
  132. );
  133. app = new app { id = nextAppId, process = name };
  134. nextAppId++;
  135. }
  136. else
  137. {
  138. app = new app
  139. {
  140. id = data.id,
  141. process = name
  142. };
  143. }
  144. apps.Add(name, app);
  145. //fix icons
  146. var largeIconPath = GetIconPath(app.id, true);
  147. if (!File.Exists(largeIconPath))
  148. {
  149. try
  150. {
  151. using var iconl = GetIcon(process.MainModule.FileName, true);
  152. SaveIcon(iconl, largeIconPath);
  153. using var icons = GetIcon(process.MainModule.FileName, false);
  154. SaveIcon(icons, GetIconPath(app.id, false));
  155. }
  156. catch(Win32Exception)
  157. {
  158. }
  159. }
  160. return app;
  161. }
  162. long nextWinId = 0;
  163. win GetWin(Process process, string winText)
  164. {
  165. var app = GetApp(process);
  166. if (app.wins.TryGetValue(winText, out var win))
  167. {
  168. return win;
  169. }
  170. var data = db.ExecuteDynamic(
  171. "select id from win where appid=@appid and text=@winText",
  172. new SQLiteParameter("appid", app.id),
  173. new SQLiteParameter("winText", winText)
  174. ).FirstOrDefault();
  175. if (data == null)
  176. {
  177. if (nextWinId == 0)
  178. {
  179. nextWinId = (int)(long)db.ExecuteData("select ifnull(max(id),0) + 1 from win")[0][0];
  180. }
  181. db.Execute(
  182. "insert into win (id, appId, text) values(@id, @appId, @text)",
  183. new SQLiteParameter("id", nextWinId),
  184. new SQLiteParameter("appId", app.id),
  185. new SQLiteParameter("text", winText)
  186. );
  187. win = new win { id = nextWinId, text = winText };
  188. nextWinId++;
  189. }
  190. else
  191. {
  192. win = new win
  193. {
  194. id = data.id,
  195. text = winText
  196. };
  197. }
  198. app.wins.Add(winText, win);
  199. return win;
  200. }
  201. DB db = DB.Instance;
  202. public void RecorderThreadProc()
  203. {
  204. App lastApp = null;
  205. while (true)
  206. {
  207. var now = DateTime.Now;
  208. var hwnd = WinApi.GetForegroundWindow();
  209. var text = new StringBuilder(255);
  210. WinApi.GetWindowText(hwnd, text, 255);
  211. var winText = text.ToString();
  212. WinApi.GetWindowThreadProcessId(hwnd, out var processid);
  213. var process = GetProcess(processid);
  214. var appname = process.ProcessName;
  215. if (lastApp != null)
  216. {
  217. db.Execute(
  218. "update period set timeend = @v1 where timestart = @v0",
  219. lastApp.TimeStart,
  220. now.AddMilliseconds(-1)//必须减小,否则可能与下个周期开始时间重叠
  221. );
  222. }
  223. if (lastApp == null || lastApp.AppProcess != appname || lastApp.WinText != winText)
  224. {
  225. var win = GetWin(process, winText);
  226. lastApp = new App { WinId = win.id, AppProcess = appname, TimeStart = now, WinText = winText };
  227. db.Execute(
  228. "insert into [period](winid, timeStart, timeEnd) values(@v0, @v1, @v1)",
  229. win.id, now
  230. );
  231. }
  232. Screenshot(now);
  233. //等到下一个周期
  234. var nextTime = now.AddMilliseconds(IntervalMs);
  235. now = DateTime.Now;
  236. if(nextTime > now)
  237. {
  238. Thread.Sleep(nextTime - now);
  239. }
  240. }
  241. }
  242. string DataPath => string.IsNullOrWhiteSpace(Settings.Default.DataPath) ? Application.StartupPath : Settings.Default.DataPath;
  243. public string ScreenPath => Path.Combine(DataPath, "images");
  244. public string IconPath => Path.Combine(DataPath, "icons");
  245. ImageCodecInfo jpgcodec = ImageCodecInfo.GetImageDecoders().First(codec => codec.MimeType == "image/jpeg");
  246. ///// <summary>
  247. ///// 获取图片文件路径
  248. ///// </summary>
  249. ///// <param name="timeStart"></param>
  250. ///// <param name="timeImage"></param>
  251. ///// <returns></returns>
  252. //public string getImageFile(DateTime timeStart, DateTime timeImage)
  253. //{
  254. // var folder = Path.Combine(ScreenPath, timeImage.ToString("yyyyMMdd"));
  255. // var filename = $"{timeStart:HHmmss}+{Math.Round((timeImage - timeStart).TotalSeconds)}";
  256. // return Path.Combine(folder, $"{filename}.jpg");
  257. //}
  258. public string getFileName(DateTime time)
  259. {
  260. return Path.Combine(ScreenPath, $"{time:yyyyMMdd}", $"{time:HHmmss}." + Recorder.ExName);
  261. }
  262. DateTime lastCheck = DateTime.MinValue.Date;
  263. /// <summary>
  264. /// 截图
  265. /// </summary>
  266. /// <param name="now"></param>
  267. /// <param name="lastApp"></param>
  268. void Screenshot(DateTime now)
  269. {
  270. //检查记录天数限制
  271. if (lastCheck != now.Date)
  272. {
  273. var firstDate = now.Date.AddDays(-Settings.Default.RecordScreenDays);
  274. var dirs = Directory.EnumerateDirectories(ScreenPath, "????????");
  275. foreach (var i in dirs)
  276. {
  277. if (DateTime.TryParseExact(Path.GetFileName(i), "yyyyMMdd", CultureInfo.CurrentCulture, DateTimeStyles.None, out var date))
  278. {
  279. if (date < firstDate)
  280. {
  281. Directory.Delete(i);
  282. }
  283. }
  284. }
  285. lastCheck = now.Date;
  286. }
  287. if (Settings.Default.RecordScreenDays == 0)
  288. {
  289. return;
  290. }
  291. if (buffer == null)
  292. {
  293. buffer = new MemoryBuffer(now);
  294. }
  295. using var img = GetScreen();
  296. using var mem = new MemoryStream();
  297. img.Save(mem, ImageFormat.Jpeg);
  298. buffer.Frames.Add(new Frame(now - buffer.StartTime, mem.ToArray()));
  299. if ((now - buffer.StartTime).TotalSeconds >= 10 * 60 || now.Date != buffer.StartTime.Date) //固定为10mins,防止保存时间长,减少出问题时影响的时长。
  300. {
  301. FlushScreenBuffer();
  302. }
  303. }
  304. public void FlushScreenBuffer()
  305. {
  306. //切换到新buffer
  307. var newBuffer = new MemoryBuffer(DateTime.Now);
  308. var b = buffer;
  309. buffer = newBuffer;
  310. //加入flushing
  311. lock (flushing)
  312. {
  313. flushing.Add(b);
  314. }
  315. var path = getFileName(b.StartTime);
  316. var folder = Path.GetDirectoryName(path);
  317. if (!Directory.Exists(folder))
  318. {
  319. Directory.CreateDirectory(folder);
  320. }
  321. new Thread(() =>
  322. {
  323. Ffmpeg.Save(getFileName(b.StartTime), b.Frames.ToArray());
  324. lock (flushing)
  325. {
  326. flushing.Remove(b);
  327. }
  328. })
  329. {
  330. Priority = ThreadPriority.BelowNormal,
  331. IsBackground = false
  332. }.Start();
  333. }
  334. public class MemoryBuffer
  335. {
  336. public readonly DateTime StartTime;
  337. public readonly List<Frame> Frames = new List<Frame>();
  338. public MemoryBuffer(DateTime startTime)
  339. {
  340. StartTime = startTime;
  341. }
  342. }
  343. public MemoryBuffer buffer;
  344. public List<MemoryBuffer> flushing = new List<MemoryBuffer>();
  345. Bitmap GetScreen()
  346. {
  347. var result = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
  348. using var g = Graphics.FromImage(result);
  349. retry:
  350. try
  351. {
  352. g.CopyFromScreen(0, 0, 0, 0, Screen.PrimaryScreen.Bounds.Size);
  353. }
  354. catch
  355. {
  356. goto retry;
  357. }
  358. return result;
  359. }
  360. }
  361. }