Files
time-mocker-csharp/TimeMocker.UI/Forms/MainForm.cs
T
2026-02-27 10:49:10 +07:00

719 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows.Forms;
using TimeMocker.UI.Core;
namespace TimeMocker.UI.Forms
{
public class PatternRuleDto
{
public string Pattern { get; set; }
public bool UseRegex { get; set; }
public bool Enabled { get; set; } = true;
}
public class AppConfig
{
private static readonly string ConfigPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"TimeMocker",
"timemocker-config.json");
public bool AutoInjectEnabled { get; set; } = true;
public List<PatternRuleDto> Patterns { get; set; } = new List<PatternRuleDto>();
public void Save()
{
var dir = Path.GetDirectoryName(ConfigPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
File.WriteAllText(ConfigPath, JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }));
}
public static AppConfig Load()
{
return File.Exists(ConfigPath)
? JsonSerializer.Deserialize<AppConfig>(File.ReadAllText(ConfigPath)) ?? new AppConfig()
: new AppConfig();
}
}
public partial class MainForm : Form
{
private readonly AppConfig _config;
private InjectionManager _injMgr;
private ProcessWatcher _watcher;
// Controls
private TabControl tabMain;
private TabPage tabProcesses, tabPatterns, tabLog;
// -- Process tab
private DataGridView dgvProcesses;
private Button btnRefresh, btnEject;
private TextBox txtProcSearch;
private Label lblProcSearch;
// -- Time panel (shared)
private GroupBox grpTime;
private DateTimePicker dtpDate;
private DateTimePicker dtpTime;
private Button btnApply;
private Label lblPreview;
// -- Patterns tab
private DataGridView dgvPatterns;
private Button btnAddPattern, btnRemovePattern;
private CheckBox chkWatcherEnabled;
private TextBox txtNewPattern;
private RadioButton rdoGlob, rdoRegex;
// -- Log tab
private RichTextBox rtbLog;
private Button btnClearLog;
public MainForm()
{
_config = AppConfig.Load();
Text = "TimeMocker Process Time Injection";
Size = new Size(900, 680);
MinimumSize = new Size(750, 560);
StartPosition = FormStartPosition.CenterScreen;
Font = new Font("Segoe UI", 9f);
BackColor = Color.FromArgb(45, 52, 64);
ForeColor = Color.FromArgb(200, 200, 200);
_injMgr = new InjectionManager();
_watcher = new ProcessWatcher(_injMgr);
_injMgr.LogMessage += AppendLog;
_watcher.LogMessage += AppendLog;
_watcher.ProcessAutoInjected += entry =>
BeginInvoke((Action)(() => RefreshProcessList()));
BuildUI();
// Load config settings
chkWatcherEnabled.Checked = _config.AutoInjectEnabled;
foreach (var pattern in _config.Patterns)
{
_watcher.AddRule(new PatternRule
{
Pattern = pattern.Pattern,
UseRegex = pattern.UseRegex,
Enabled = pattern.Enabled
});
dgvPatterns.Rows.Add(pattern.Pattern, pattern.UseRegex ? "Regex" : "Glob", pattern.Enabled);
}
RefreshProcessList();
UpdateTimePreview();
ApplyTime(); // Initialize with current time
}
// =====================================================================
// UI Builder
// =====================================================================
private void BuildUI()
{
// ---- Shared time panel (top) ------------------------------------
grpTime = new GroupBox
{
Text = "Mock Time Settings",
Dock = DockStyle.Top,
Height = 110,
ForeColor = Color.FromArgb(100, 160, 220),
Padding = new Padding(8)
};
var timeFlow = new FlowLayoutPanel
{
Dock = DockStyle.Fill,
FlowDirection = FlowDirection.LeftToRight,
WrapContents = false,
AutoSize = false
};
dtpDate = new DateTimePicker
{
Format = DateTimePickerFormat.Short,
Width = 120,
Height = 26,
Value = DateTime.Now,
Margin = new Padding(4, 10, 4, 0)
};
dtpDate.ValueChanged += (s, e) => UpdateTimePreview();
dtpTime = new DateTimePicker
{
Format = DateTimePickerFormat.Time,
ShowUpDown = true,
Width = 100,
Height = 26,
Value = DateTime.Now,
Margin = new Padding(4, 10, 4, 0)
};
dtpTime.ValueChanged += (s, e) => UpdateTimePreview();
var btnSetNow = MakeButton("Now", 70, Color.FromArgb(100, 110, 120));
btnSetNow.Margin = new Padding(4, 10, 4, 0);
btnSetNow.Click += (s, e) =>
{
dtpDate.Value = dtpTime.Value = DateTime.Now;
};
btnApply = MakeButton("Set", 70, Color.FromArgb(70, 140, 200));
btnApply.Margin = new Padding(4, 10, 4, 0);
btnApply.Click += (s, e) => ApplyTime();
lblPreview = new Label
{
AutoSize = false,
Width = 300,
Height = 20,
ForeColor = Color.FromArgb(140, 150, 160),
Font = new Font("Segoe UI", 8.5f, FontStyle.Italic),
Margin = new Padding(4, 14, 0, 0)
};
timeFlow.Controls.AddRange(new Control[]
{
dtpDate, dtpTime, btnSetNow, btnApply, lblPreview
});
grpTime.Controls.Add(timeFlow);
// ---- Tabs -------------------------------------------------------
tabMain = new TabControl
{
Dock = DockStyle.Fill,
DrawMode = TabDrawMode.OwnerDrawFixed,
SizeMode = TabSizeMode.Fixed,
ItemSize = new Size(120, 28)
};
tabMain.DrawItem += DrawTab;
tabProcesses = new TabPage("Processes");
tabPatterns = new TabPage("Auto-Inject Rules");
tabLog = new TabPage("Log");
StyleTab(tabProcesses);
StyleTab(tabPatterns);
StyleTab(tabLog);
BuildProcessTab();
BuildPatternsTab();
BuildLogTab();
tabMain.TabPages.AddRange(new[] { tabProcesses, tabPatterns, tabLog });
Controls.Add(tabMain);
Controls.Add(grpTime);
}
// =====================================================================
// Process Tab
// =====================================================================
private void BuildProcessTab()
{
var panel = new Panel { Dock = DockStyle.Fill };
// Top toolbar
var toolbar = new FlowLayoutPanel
{
Dock = DockStyle.Top,
Height = 40,
Padding = new Padding(4)
};
lblProcSearch = new Label { Text = "Search:", AutoSize = true, Margin = new Padding(4, 8, 2, 0) };
txtProcSearch = new TextBox { Width = 180, Margin = new Padding(0, 6, 8, 0) };
txtProcSearch.TextChanged += (s, e) => FilterProcessList();
btnRefresh = MakeButton("⟳ Refresh", 90, Color.FromArgb(100, 110, 120));
btnRefresh.Margin = new Padding(0, 6, 4, 0);
btnRefresh.Click += (s, e) => RefreshProcessList();
btnEject = MakeButton("✕ Eject Selected", 110, Color.FromArgb(190, 90, 90));
btnEject.Margin = new Padding(0, 6, 4, 0);
btnEject.Click += OnEjectClick;
toolbar.Controls.AddRange(new Control[] { lblProcSearch, txtProcSearch, btnRefresh, btnEject });
toolbar.BackColor = Color.FromArgb(55, 62, 74);
// Single process list with Inject checkbox
var lblSection = MakeSectionLabel("Processes");
dgvProcesses = MakeGrid();
dgvProcesses.ReadOnly = false;
dgvProcesses.MultiSelect = true;
dgvProcesses.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
dgvProcesses.Columns.AddRange(
Col("PID", 50), Col("Name", 160), Col("Path", 380), BoolCol("Injected", 70));
// Make all columns read-only except the checkbox
foreach (DataGridViewColumn col in dgvProcesses.Columns)
{
if (col.Name != "Injected")
col.ReadOnly = true;
}
dgvProcesses.CellValueChanged += dgvProcesses_CellValueChanged;
dgvProcesses.CurrentCellDirtyStateChanged += (s, e) =>
{
if (dgvProcesses.IsCurrentCellDirty)
{
dgvProcesses.CommitEdit(DataGridViewDataErrorContexts.Commit);
}
};
panel.Controls.Add(lblSection);
panel.Controls.Add(dgvProcesses);
panel.Controls.Add(toolbar);
tabProcesses.Controls.Add(panel);
}
private List<ProcessRow> _allRows = new List<ProcessRow>();
private class ProcessRow
{
public int Id;
public string Name, Path;
public bool Injected;
}
private void dgvProcesses_CellValueChanged(object sender, DataGridViewCellEventArgs e)
{
if (e.RowIndex < 0 || e.ColumnIndex < 0) return;
if (dgvProcesses.Columns[e.ColumnIndex].Name != "Injected") return;
var pidCell = dgvProcesses.Rows[e.RowIndex].Cells[0];
if (pidCell.Value == null) return;
var pid = (int)pidCell.Value;
var shouldInject = (bool)dgvProcesses.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
if (shouldInject)
{
// Inject the process
if (!_injMgr.IsInjected(pid))
{
try
{
var p = Process.GetProcessById(pid);
_injMgr.Inject(p);
var dt = GetFakeTime();
_injMgr.SetFakeTime(p.Id, dt.ToUniversalTime());
AppendLog($"Injected into [{pid}] {p.ProcessName}");
}
catch (Exception ex)
{
MessageBox.Show($"Injection failed:\n{ex.Message}", "Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
// Revert checkbox on failure
dgvProcesses.BeginInvoke((Action)(() =>
{
dgvProcesses.Rows[e.RowIndex].Cells[e.ColumnIndex].Value = false;
}));
}
}
}
else
{
// Eject the process
if (_injMgr.IsInjected(pid))
{
_injMgr.Eject(pid);
AppendLog($"Ejected from [{pid}]");
}
}
}
private void RefreshProcessList()
{
_allRows.Clear();
var injectedPids = new HashSet<int>(_injMgr.InjectedProcesses.Select(x => x.ProcessId));
foreach (var p in Process.GetProcesses().OrderBy(x => x.ProcessName))
{
var path = "";
try
{
path = p.MainModule?.FileName ?? "";
}
catch
{
}
_allRows.Add(new ProcessRow
{
Id = p.Id,
Name = p.ProcessName,
Path = path,
Injected = injectedPids.Contains(p.Id)
});
}
FilterProcessList();
}
private void FilterProcessList()
{
var q = txtProcSearch.Text.Trim().ToLower();
dgvProcesses.SuspendLayout();
dgvProcesses.Rows.Clear();
foreach (var r in _allRows)
{
if (q.Length > 0 && !r.Name.ToLower().Contains(q) && !r.Path.ToLower().Contains(q)) continue;
dgvProcesses.Rows.Add(r.Id, r.Name, r.Path, r.Injected);
}
dgvProcesses.ResumeLayout();
}
private void OnInjectClick(object sender, EventArgs e)
{
// No longer needed - injection is handled by checkbox
}
private void OnEjectClick(object sender, EventArgs e)
{
foreach (DataGridViewRow row in dgvProcesses.SelectedRows)
{
if (row.Cells[0].Value == null) continue;
var pid = (int)row.Cells[0].Value;
if (_injMgr.IsInjected(pid))
{
_injMgr.Eject(pid);
// Uncheck the Injected checkbox
row.Cells["Injected"].Value = false;
AppendLog($"Ejected from [{pid}]");
}
}
}
// =====================================================================
// Patterns Tab
// =====================================================================
private void BuildPatternsTab()
{
var panel = new Panel { Dock = DockStyle.Fill };
var toolbar = new FlowLayoutPanel
{
Dock = DockStyle.Top,
Height = 80,
Padding = new Padding(4),
BackColor = Color.FromArgb(55, 62, 74)
};
// Pattern input row
var lblNew = new Label { Text = "Pattern:", AutoSize = true, Margin = new Padding(4, 12, 4, 0) };
txtNewPattern = new TextBox
{ Width = 280, Margin = new Padding(0, 10, 4, 0), Text = "e.g. C:\\Games\\MyGame\\* or ^.*chrome.*$" };
rdoGlob = new RadioButton
{
Text = "Glob", Checked = true, AutoSize = true, Margin = new Padding(4, 12, 4, 0),
ForeColor = Color.FromArgb(200, 200, 200)
};
rdoRegex = new RadioButton
{
Text = "Regex", AutoSize = true, Margin = new Padding(4, 12, 4, 0),
ForeColor = Color.FromArgb(200, 200, 200)
};
btnAddPattern = MakeButton("+ Add Rule", 100, Color.FromArgb(80, 160, 100));
btnAddPattern.Margin = new Padding(8, 8, 4, 0);
btnAddPattern.Click += OnAddPattern;
btnRemovePattern = MakeButton("✕ Remove", 90, Color.FromArgb(190, 90, 90));
btnRemovePattern.Margin = new Padding(4, 8, 4, 0);
btnRemovePattern.Click += OnRemovePattern;
chkWatcherEnabled = new CheckBox
{
Text = "Enable Auto-Inject Watcher",
ForeColor = Color.FromArgb(120, 190, 120),
AutoSize = true,
Margin = new Padding(16, 12, 4, 0),
Checked = true
};
chkWatcherEnabled.CheckedChanged += OnWatcherToggle;
toolbar.Controls.AddRange(new Control[]
{
lblNew, txtNewPattern, rdoGlob, rdoRegex,
btnAddPattern, btnRemovePattern, chkWatcherEnabled
});
var lblSection = MakeSectionLabel("Auto-Inject Rules (process path or name must match)");
dgvPatterns = MakeGrid();
dgvPatterns.Columns.AddRange(
Col("Pattern", 320), Col("Type", 60), BoolCol("Enabled"));
panel.Controls.Add(dgvPatterns);
panel.Controls.Add(lblSection);
panel.Controls.Add(toolbar);
tabPatterns.Controls.Add(panel);
}
private void OnAddPattern(object sender, EventArgs e)
{
var pat = txtNewPattern.Text.Trim();
if (string.IsNullOrEmpty(pat)) return;
var rule = new PatternRule
{
Pattern = pat,
UseRegex = rdoRegex.Checked,
Enabled = true
};
_watcher.AddRule(rule);
dgvPatterns.Rows.Add(pat, rule.UseRegex ? "Regex" : "Glob", true);
txtNewPattern.Clear();
}
private void OnRemovePattern(object sender, EventArgs e)
{
if (dgvPatterns.SelectedRows.Count == 0) return;
var idx = dgvPatterns.SelectedRows[0].Index;
var pat = dgvPatterns.Rows[idx].Cells[0].Value?.ToString();
_watcher.ClearRules();
dgvPatterns.Rows.RemoveAt(idx);
// Re-add remaining
foreach (DataGridViewRow row in dgvPatterns.Rows)
_watcher.AddRule(new PatternRule
{
Pattern = row.Cells[0].Value?.ToString() ?? "",
UseRegex = row.Cells[1].Value?.ToString() == "Regex",
Enabled = (bool)(row.Cells[2].Value ?? true)
});
}
private void OnWatcherToggle(object sender, EventArgs e)
{
if (chkWatcherEnabled.Checked)
{
_watcher.FakeUtc = GetFakeTime().ToUniversalTime();
_watcher.Start();
AppendLog("Process watcher started.");
}
else
{
_watcher.Stop();
AppendLog("Process watcher stopped.");
}
}
// =====================================================================
// Log Tab
// =====================================================================
private void BuildLogTab()
{
rtbLog = new RichTextBox
{
Dock = DockStyle.Fill,
BackColor = Color.FromArgb(30, 35, 42),
ForeColor = Color.FromArgb(170, 210, 150),
Font = new Font("Consolas", 9f),
ReadOnly = true,
ScrollBars = RichTextBoxScrollBars.Vertical
};
btnClearLog = MakeButton("Clear", 70, Color.FromArgb(100, 110, 120));
btnClearLog.Dock = DockStyle.Bottom;
btnClearLog.Click += (s, e) => rtbLog.Clear();
tabLog.Controls.Add(rtbLog);
tabLog.Controls.Add(btnClearLog);
}
// =====================================================================
// Time Logic
// =====================================================================
private DateTime GetFakeTime()
{
return dtpDate.Value.Date + dtpTime.Value.TimeOfDay;
}
private void ApplyTime()
{
var dt = GetFakeTime().ToUniversalTime();
_injMgr.SetFakeTimeAll(dt);
_watcher.FakeUtc = dt;
UpdateTimePreview();
}
private void UpdateTimePreview()
{
var dt = GetFakeTime();
// Calculate what the current offset is
var realNow = DateTime.Now;
var delta = dt - realNow;
var deltaStr = delta.TotalSeconds >= 0
? $"+{delta.TotalSeconds:F0}s"
: $"{delta.TotalSeconds:F0}s";
lblPreview.Text = $"Fake: {dt:yyyy-MM-dd HH:mm:ss} (local, offset {deltaStr})";
}
// =====================================================================
// Helpers
// =====================================================================
private void AppendLog(string msg)
{
if (rtbLog == null) return;
if (rtbLog.InvokeRequired)
{
rtbLog.BeginInvoke((Action)(() => AppendLog(msg)));
return;
}
rtbLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {msg}\n");
rtbLog.ScrollToCaret();
}
private int? GetSelectedProcessId(DataGridView dgv)
{
if (dgv.SelectedRows.Count == 0) return null;
var cell = dgv.SelectedRows[0].Cells[0].Value;
return cell is int i ? i : (int?)null;
}
private static DataGridView MakeGrid()
{
var g = new DataGridView
{
Dock = DockStyle.Fill,
ReadOnly = true,
AllowUserToAddRows = false,
AllowUserToDeleteRows = false,
SelectionMode = DataGridViewSelectionMode.FullRowSelect,
MultiSelect = false,
BackgroundColor = Color.FromArgb(52, 58, 68),
ForeColor = Color.FromArgb(175, 180, 185),
GridColor = Color.FromArgb(85, 92, 102),
BorderStyle = BorderStyle.None,
RowHeadersVisible = false,
AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.DisplayedCells,
ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize,
ColumnHeadersHeight = 28
};
g.EnableHeadersVisualStyles = false;
g.ColumnHeadersDefaultCellStyle.BackColor = Color.FromArgb(72, 78, 88);
g.ColumnHeadersDefaultCellStyle.ForeColor = Color.FromArgb(160, 185, 210);
g.DefaultCellStyle.SelectionBackColor = Color.FromArgb(98, 138, 178);
g.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(58, 64, 74);
return g;
}
private static DataGridViewTextBoxColumn Col(string name, int w)
{
return new DataGridViewTextBoxColumn
{
Name = name,
HeaderText = name,
Width = w,
MinimumWidth = w,
SortMode = DataGridViewColumnSortMode.NotSortable
};
}
private static DataGridViewCheckBoxColumn BoolCol(string name, int width = 65)
{
return new DataGridViewCheckBoxColumn
{
Name = name,
HeaderText = name,
Width = width,
MinimumWidth = width
};
}
private static Button MakeButton(string text, int w, Color bg)
{
return new Button
{
Text = text,
Width = w,
Height = 26,
BackColor = bg,
ForeColor = Color.White,
FlatStyle = FlatStyle.Flat,
Cursor = Cursors.Hand
};
}
private static Label MakeSectionLabel(string text)
{
return new Label
{
Text = text,
Dock = DockStyle.Top,
Height = 22,
ForeColor = Color.FromArgb(150, 180, 220),
Font = new Font("Segoe UI", 8.5f, FontStyle.Bold),
Padding = new Padding(4, 2, 0, 0),
BackColor = Color.FromArgb(50, 57, 69)
};
}
private static void StyleTab(TabPage tab)
{
tab.BackColor = Color.FromArgb(45, 52, 64);
tab.ForeColor = Color.FromArgb(200, 200, 200);
}
private void DrawTab(object sender, DrawItemEventArgs e)
{
var tab = (TabControl)sender;
var page = tab.TabPages[e.Index];
var rect = e.Bounds;
var selected = e.Index == tab.SelectedIndex;
using var bg = new SolidBrush(selected ? Color.FromArgb(80, 130, 180) : Color.FromArgb(60, 68, 80));
e.Graphics.FillRectangle(bg, rect);
var sf = new StringFormat
{ Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
using var fg = new SolidBrush(selected ? Color.White : Color.FromArgb(170, 180, 190));
e.Graphics.DrawString(page.Text, Font, fg, rect, sf);
}
private static void ShowInfo(string msg)
{
MessageBox.Show(msg, "Info", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// Save config
_config.AutoInjectEnabled = chkWatcherEnabled.Checked;
_config.Patterns.Clear();
foreach (DataGridViewRow row in dgvPatterns.Rows)
{
_config.Patterns.Add(new PatternRuleDto
{
Pattern = row.Cells[0].Value?.ToString() ?? "",
UseRegex = row.Cells[1].Value?.ToString() == "Regex",
Enabled = (bool)(row.Cells[2].Value ?? true)
});
}
_config.Save();
_watcher.Stop();
_injMgr.Dispose();
base.OnFormClosing(e);
}
}
}