Skip to content

Commit 653db64

Browse files
feat: Enhance email command handling with device ID and update process improvements
1 parent e57c484 commit 653db64

12 files changed

Lines changed: 327 additions & 29 deletions

File tree

Program.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ private static void EnsureWatchdogInstalled()
175175
}
176176

177177
Logger.Instance.Info("Watchdog path changed, updating service...");
178+
if (SuppressWatchdogElevation())
179+
{
180+
Logger.Instance.Warn("Skipping elevated watchdog service path update because elevation is disabled for this run.");
181+
return;
182+
}
183+
178184
var psi = new ProcessStartInfo
179185
{
180186
FileName = watchdogPath,
@@ -205,6 +211,12 @@ private static void EnsureWatchdogInstalled()
205211
if (exePath == null) return;
206212

207213
Logger.Instance.Info("Installing watchdog service...");
214+
if (SuppressWatchdogElevation())
215+
{
216+
Logger.Instance.Warn("Skipping elevated watchdog service installation because elevation is disabled for this run.");
217+
return;
218+
}
219+
208220
var psi = new ProcessStartInfo
209221
{
210222
FileName = watchdogPath,
@@ -223,6 +235,11 @@ private static void EnsureWatchdogInstalled()
223235
}
224236
}
225237

238+
private static bool SuppressWatchdogElevation()
239+
{
240+
return string.Equals(Environment.GetEnvironmentVariable("DEVICEMON_SUPPRESS_WATCHDOG_UAC"), "1", StringComparison.Ordinal);
241+
}
242+
226243
private static void StartWatchdogServiceIfNeeded(System.ServiceProcess.ServiceController sc, string watchdogPath, string monitorPath)
227244
{
228245
try
@@ -251,6 +268,12 @@ private static void TryElevatedWatchdogUpdate(string watchdogPath, string monito
251268
if (string.IsNullOrWhiteSpace(watchdogPath) || !File.Exists(watchdogPath))
252269
return;
253270

271+
if (SuppressWatchdogElevation())
272+
{
273+
Logger.Instance.Warn("Skipping elevated watchdog service repair/start because elevation is disabled for this run.");
274+
return;
275+
}
276+
254277
Logger.Instance.Warn("Requesting elevated watchdog service repair/start...");
255278
var psi = new ProcessStartInfo
256279
{

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,11 @@ When you run `DeviceMon.exe`, it automatically looks for `GameHost.exe` in the s
9797
# Output goes to: bin\Release\net8.0-windows\win-x64\publish\
9898
```
9999

100-
The script publishes three projects:
100+
The script publishes four projects:
101101
1. `MonitorAndControl.csproj``DeviceMon.exe`
102102
2. `Watchdog\SystemHelperWatchdog.csproj``GameHost.exe`
103-
3. `PopupHost\PopupHost.csproj``PopupHost.exe`
103+
3. `PopupHost\PopupHost.csproj` -> `PopupHost.exe`
104+
4. `UpdateAgent\UpdateAgent.csproj` -> `UpdateAgent.exe`
104105

105106
It also copies `appsettings.json`, the `wwwroot/` folder, and PowerShell helper scripts.
106107

@@ -143,6 +144,7 @@ MonitorAndControl/
143144
│ └── wwwroot/ # SPA dashboard (HTML, JS, CSS, i18n JSON)
144145
├── Resources/ # .resx localization files
145146
├── PopupHost/ # Warning popup child process
147+
|-- UpdateAgent/ # Self-update helper launched from dashboard
146148
├── Watchdog/ # Windows service watchdog
147149
└── Tests/ # Unit tests
148150
```
@@ -257,11 +259,11 @@ Once installed, the `GameHost` service runs under the SYSTEM account and automat
257259
| **Discover** | Scan system for installed games/apps, add them with default limit |
258260
| **Schedule** | Time‑based allowed hours rules per app |
259261
| **Logs** | Real‑time event log with filtering |
260-
| **Settings** | Kill delay, language, autostart, webhook, email, admin password, backup/restore, health |
262+
| **Settings** | Kill delay, language, dashboard hotkey, auto-start, webhook, email, app update, admin password, backup/restore, health |
261263

262264
**Admin password:** Set one in **Settings** from the child PC or a remote dashboard to protect dashboard changes (pause, resume, reset, kill, settings edits) and shutdown.
263265

264-
**App update:** Put a freshly published package on a local folder, UNC share, or HTTP/HTTPS ZIP URL, then use **Settings -> App Update**. The package must contain `DeviceMon.exe`. The dashboard starts `UpdateAgent.exe`, closes DeviceMon, copies the package over the install folder, restarts DeviceMon, and repairs/restarts the watchdog when permissions allow. Check `update.log` in the install folder if an update fails.
266+
**App update:** Put a freshly published package on a local folder, UNC share, or HTTP/HTTPS ZIP URL, then use **Settings -> App Update**. The package must contain `DeviceMon.exe`. For Windows 11 SMB shares, enter the optional SMB username/password. The dashboard starts `UpdateAgent.exe`, writes an update marker, closes DeviceMon, copies the package over the install folder, restarts DeviceMon, and repairs/restarts the watchdog when permissions allow. The updated watchdog pauses auto-restart while the update marker is fresh. Check `update.log` in the install folder if an update fails.
265267

266268
**Remote access:** Settings show the computer name and IP addresses. From another PC, open `http://CHILD_PC_NAME:5000` or `http://192.168.x.x:5000`.
267269

@@ -276,6 +278,7 @@ Once installed, the `GameHost` service runs under the SYSTEM account and automat
276278
|---------|---------|-------------|
277279
| `help` | `mc: help` | Shows all available commands |
278280
| `status` | `mc: status` | Current limits, schedule, and today's usage |
281+
| `@device command` | `mc: @bedroom status` | Run a command only on the PC with that Device ID |
279282
| `set [app] [min] min` | `mc: set Fortnite 60 min` | Set daily limit for an app |
280283
| `bonus [app] [min] min` | `mc: bonus Fortnite 15 min` | Add bonus time today |
281284
| `extend [app] until bedtime` | `mc: extend Fortnite until bedtime` | Extend until end of allowed window |

Services/EmailService.cs

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class EmailService : IDisposable
2626
private string _email = "";
2727
private string _password = "";
2828
private string _allowedSender = "";
29+
private string _deviceId = Environment.MachineName;
2930
private string _startAlertDate = DateTime.Now.ToString("yyyy-MM-dd");
3031
private const string SmtpHost = "smtp.gmail.com";
3132
private const int SmtpPort = 587;
@@ -50,6 +51,9 @@ public async Task LoadSettingsAsync()
5051
if (!string.IsNullOrEmpty(storedPassword) && !storedPassword.StartsWith("dpapi:", StringComparison.Ordinal))
5152
await _db.SetSettingAsync("EmailPassword", SecretProtector.Protect(_password));
5253
_allowedSender = await _db.GetSettingAsync("EmailAllowedSender", _email);
54+
_deviceId = NormalizeDeviceId(await _db.GetSettingAsync("EmailDeviceId", Environment.MachineName));
55+
if (string.IsNullOrWhiteSpace(_deviceId))
56+
_deviceId = NormalizeDeviceId(Environment.MachineName);
5357
_notifyEnabled = !string.IsNullOrEmpty(_email) && !string.IsNullOrEmpty(_password)
5458
&& (await _db.GetSettingAsync("EmailNotifyEnabled", "false")) == "true";
5559
_startNotifyEnabled = !string.IsNullOrEmpty(_email) && !string.IsNullOrEmpty(_password)
@@ -105,8 +109,8 @@ private async void PollRunningTrackedApps(object? state)
105109
var msg = new MimeMessage();
106110
msg.From.Add(new MailboxAddress("Monitor", _email));
107111
msg.To.Add(new MailboxAddress("Parent", to));
108-
msg.Subject = subject;
109-
msg.Body = new TextPart("plain") { Text = body };
112+
msg.Subject = $"[{_deviceId}] {subject}";
113+
msg.Body = new TextPart("plain") { Text = WithDeviceHeader(body) };
110114

111115
using var client = new SmtpClient();
112116
await client.ConnectAsync(SmtpHost, SmtpPort, SecureSocketOptions.StartTls);
@@ -146,7 +150,7 @@ private async void PollRunningTrackedApps(object? state)
146150
}
147151

148152
var subject = $"App Started: {appName}";
149-
var body = $"{appName} was opened on {Environment.MachineName}.\nProcess: {processName}\nTime: {DateTime.Now:g}";
153+
var body = $"{appName} was opened.\nProcess: {processName}\nTime: {DateTime.Now:g}";
150154
return await SendMailAsync(subject, body, _email);
151155
}
152156

@@ -157,7 +161,7 @@ private async void PollRunningTrackedApps(object? state)
157161

158162
return await SendMailAsync(
159163
"App Started: Test",
160-
$"This is a test app-start email from {Environment.MachineName} at {DateTime.Now:g}.",
164+
$"This is a test app-start email at {DateTime.Now:g}.",
161165
_email);
162166
}
163167

@@ -197,9 +201,14 @@ private async void PollInbox(object? state)
197201
var commandText = ExtractCommandText(subject, body);
198202
if (commandText == null)
199203
continue;
204+
if (!IsCommandForThisDevice(commandText, out var effectiveCommand))
205+
{
206+
Logger.Instance.Info($"Email command ignored on {_deviceId}; target does not match: \"{commandText}\"");
207+
continue;
208+
}
200209

201-
Logger.Instance.Info($"Email command from {from}: \"{commandText}\"");
202-
var reply = await ProcessCommandAsync(commandText);
210+
Logger.Instance.Info($"Email command from {from}: \"{effectiveCommand}\"");
211+
var reply = await ProcessCommandAsync(effectiveCommand);
203212
if (reply != null)
204213
{
205214
Logger.Instance.Info($"Email reply to {from}: \"{reply}\"");
@@ -243,6 +252,24 @@ private string[] GetAllowedSenders()
243252
return null;
244253
}
245254

255+
private bool IsCommandForThisDevice(string commandText, out string effectiveCommand)
256+
{
257+
effectiveCommand = commandText.Trim();
258+
var match = Regex.Match(effectiveCommand, @"^@(?<id>[A-Za-z0-9_.-]+)\s+(?<cmd>.+)$", RegexOptions.IgnoreCase);
259+
if (!match.Success)
260+
match = Regex.Match(effectiveCommand, @"^to\s+(?<id>[A-Za-z0-9_.-]+)\s+(?<cmd>.+)$", RegexOptions.IgnoreCase);
261+
262+
if (!match.Success)
263+
return true;
264+
265+
var target = NormalizeDeviceId(match.Groups["id"].Value);
266+
if (!target.Equals(_deviceId, StringComparison.OrdinalIgnoreCase))
267+
return false;
268+
269+
effectiveCommand = match.Groups["cmd"].Value.Trim();
270+
return effectiveCommand.Length > 0;
271+
}
272+
246273
private async Task<string?> ProcessCommandAsync(string text)
247274
{
248275
var lines = text.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
@@ -253,6 +280,7 @@ private string[] GetAllowedSenders()
253280
// help
254281
if (lower.StartsWith("help"))
255282
return @"Commands:
283+
@device status - run command only on one PC
256284
status - current limits, schedule, today's usage
257285
set [app] [minutes] min - set daily limit (e.g. set aces 60 min)
258286
bonus [app] [minutes] min - add bonus time today (e.g. bonus aces 15 min)
@@ -261,7 +289,7 @@ private string[] GetAllowedSenders()
261289
set kill-delay [seconds] - set kill delay
262290
add [process.exe] [appname] - register a known app (e.g. add aces.exe Aces)
263291
264-
Prefix commands with mc:, for example: mc: status";
292+
Prefix commands with mc:, for example: mc: status or mc: @" + _deviceId + " status";
265293

266294
// status
267295
if (lower.StartsWith("status"))
@@ -270,6 +298,9 @@ set kill-delay [seconds] - set kill delay
270298
var usage = await _db.GetTodayUsageAsync();
271299
var schedule = await _db.GetScheduleRulesAsync();
272300
var sb = new System.Text.StringBuilder();
301+
sb.AppendLine($"Device: {_deviceId}");
302+
sb.AppendLine($"Computer: {Environment.MachineName}");
303+
sb.AppendLine();
273304
sb.AppendLine("=== Limits ===");
274305
foreach (var l in limits)
275306
{
@@ -402,8 +433,8 @@ private async Task SendReplyAsync(string to, string subject, string body)
402433
var msg = new MimeMessage();
403434
msg.From.Add(new MailboxAddress("Monitor", _email));
404435
msg.To.Add(new MailboxAddress("", to));
405-
msg.Subject = "Re: " + subject;
406-
msg.Body = new TextPart("plain") { Text = body };
436+
msg.Subject = $"Re: [{_deviceId}] " + subject;
437+
msg.Body = new TextPart("plain") { Text = WithDeviceHeader(body) };
407438

408439
using var client = new SmtpClient();
409440
await client.ConnectAsync(SmtpHost, SmtpPort, SecureSocketOptions.StartTls);
@@ -429,8 +460,8 @@ private async Task SendReplyAsync(string to, string subject, string body)
429460
var msg = new MimeMessage();
430461
msg.From.Add(new MailboxAddress("Monitor", email));
431462
msg.To.Add(new MailboxAddress("Parent", email));
432-
msg.Subject = "Test from Monitor";
433-
msg.Body = new TextPart("plain") { Text = "Email notification test - If you receive this, email is configured correctly." };
463+
msg.Subject = $"[{_deviceId}] Test from Monitor";
464+
msg.Body = new TextPart("plain") { Text = WithDeviceHeader("Email notification test - If you receive this, email is configured correctly.") };
434465

435466
using var client = new SmtpClient();
436467
await client.ConnectAsync(SmtpHost, SmtpPort, SecureSocketOptions.StartTls);
@@ -449,4 +480,13 @@ public void Dispose()
449480
_pollTimer?.Dispose();
450481
_startAlertTimer?.Dispose();
451482
}
483+
484+
private string WithDeviceHeader(string body) =>
485+
$"Device: {_deviceId}\nComputer: {Environment.MachineName}\n\n{body}";
486+
487+
public static string NormalizeDeviceId(string value)
488+
{
489+
var normalized = Regex.Replace((value ?? "").Trim(), @"[^A-Za-z0-9_.-]+", "-").Trim('-');
490+
return normalized.Length > 40 ? normalized[..40] : normalized;
491+
}
452492
}

0 commit comments

Comments
 (0)