/* * WelsonJS.Service * * filename: * ServiceMain.cs * * description: * WelsonJS - Build a Windows app on the Windows built-in JavaScript engine * * website: * - https://github.com/gnh1201/welsonjs * - https://catswords.social/@catswords_oss * - https://teams.live.com/l/community/FEACHncAhq8ldnojAI * * author: * Namhyeon Go * * license: * GPLv3 or MS-RL(Microsoft Reciprocal License) * * references: * - https://learn.microsoft.com/en-us/dotnet/framework/windows-services/how-to-debug-windows-service-applications * - https://stackoverflow.com/questions/6490979/how-to-pass-parameters-to-windows-service * - https://stackoverflow.com/questions/42812333/pass-an-argument-to-a-windows-service-at-automatic-startup * - https://learn.microsoft.com/ko-kr/windows/win32/api/winuser/nf-winuser-getsystemmetrics */ using System; using System.ServiceProcess; using System.Timers; using System.Runtime.InteropServices; using MSScriptControl; using System.IO; using System.Collections.Generic; using WelsonJS.TinyINIController; using System.Collections; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using WelsonJS.Service.Logging; namespace WelsonJS.Service { public partial class ServiceMain : ServiceBase { private readonly static string applicationName = "WelsonJS"; private static List timers; private ILogger logger; private string workingDirectory; private string scriptName; private string scriptFilePath; private string scriptText; private ScriptControl scriptControl; private string[] args; private bool disabledHeartbeat = false; private bool disabledScreenTime = false; private bool disabledFileMonitor = false; private ScreenMatch screenMatcher; private FileEventMonitor fileEventMonitor; private IniFile settingsFileHandler; private UserVariables userVariablesHandler; [DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); private static int SM_REMOTESESSION = 0x1000; public ServiceMain(string[] _args, ILogger _logger) { InitializeComponent(); // set arguments and logger args = _args; logger = _logger; // mapping arguments to each variables var arguments = ParseArguments(this.args); foreach (KeyValuePair entry in arguments) { switch (entry.Key) { case "working-directory": workingDirectory = entry.Value; break; case "script-name": scriptName = entry.Value; break; case "disable-heartbeat": disabledHeartbeat = true; break; case "disable-screen-time": disabledScreenTime = true; break; case "disable-file-monitor": disabledFileMonitor = true; break; } } // load the user variables userVariablesHandler = new UserVariables(this); userVariablesHandler.Load(); // set timers timers = new List(); // set working directory if (string.IsNullOrEmpty(workingDirectory)) { workingDirectory = Path.Combine(Path.GetTempPath(), applicationName); logger.LogInformation("Working directory not provided. Using default value: " + workingDirectory); if (!Directory.Exists(workingDirectory)) { Directory.CreateDirectory(workingDirectory); logger.LogInformation("Directory created: " + workingDirectory); } } Directory.SetCurrentDirectory(workingDirectory); // read settings.ini string settingsFilePath = Path.Combine(workingDirectory, "settings.ini"); if (File.Exists(settingsFilePath)) { try { settingsFileHandler = new IniFile(settingsFilePath); } catch (Exception ex) { logger.LogWarning(ex.Message); } } else { logger.LogInformation($"Configuration file not found: {settingsFilePath}"); } // read configrations from settings.ini if (settingsFileHandler != null) { string[] configNames = new string[] { "DISABLE_HEARTBEAT", "DISABLE_SCREEN_TIME", "DISABLE_FILE_MONITOR" }; foreach (string configName in configNames) { try { if ("true" == ReadSettingsValue(configName)) { switch (configName) { case "DISABLE_HEARTBEAT": disabledHeartbeat = true; break; case "DISABLE_SCREEN_TIME": disabledScreenTime = true; break; case "DISABLE_FILE_MONITOR": disabledFileMonitor = true; break; default: break; } } } catch (Exception ex) { logger.LogInformation($"{configName} is ignored: {ex.Message}"); } } } // set script name if (string.IsNullOrEmpty(scriptName)) { scriptName = "defaultService"; logger.LogInformation($"Script name not provided. Using default value: {scriptName}"); } // set path of the script scriptFilePath = Path.Combine(workingDirectory, "app.js"); // start the heartbeat if (!disabledHeartbeat) { HeartbeatClient heartbeatClient = new HeartbeatClient(this, logger); Task.Run(heartbeatClient.StartHeartbeatAsync); Task.Run(heartbeatClient.StartEventListenerAsync); } // set default timer Timer defaultTimer = new Timer { Interval = 60000 // 1 minute }; defaultTimer.Elapsed += OnElapsedTime; timers.Add(defaultTimer); // check this session is the user interactive mode if (Environment.UserInteractive) { OnUserInteractiveEnvironment(); } else { logger.LogInformation("Disabled the User Interactive Mode. (e.g., OnScreenTime)"); } logger.LogInformation(applicationName + " Service Loaded"); } public string ReadSettingsValue(string key, string defaultValue = null) { if (settingsFileHandler != null) { return settingsFileHandler.Read(key, "Service") ?? defaultValue; } else { logger.LogWarning("Unable to read the value. It seems that settings.ini is not configured correctly."); return defaultValue; } } public UserVariables GetUserVariablesHandler() { return userVariablesHandler; } internal void TestStartupAndStop() { this.OnStart(this.args); Console.ReadLine(); this.OnStop(); } protected override void OnStart(string[] args) { base.OnStart(args); // Check exists the entry script file if (File.Exists(scriptFilePath)) { logger.LogInformation($"Script file found: {scriptFilePath}"); try { // load the script scriptText = File.ReadAllText(scriptFilePath); scriptControl = new ScriptControl { Language = "JScript", AllowUI = false }; scriptControl.Reset(); scriptControl.AddCode(scriptText); // make the start arguments string[] startArguments; string[] _args; if (Environment.UserInteractive) { _args = new string[] { $"--env-file={userVariablesHandler.GetEnvFilePath()}", "--user-interactive" }; } else { _args = new string[] { $"--env-file={userVariablesHandler.GetEnvFilePath()}" }; } startArguments = new string[args.Length + _args.Length]; args.CopyTo(startArguments, 0); for (int i = 0; i < _args.Length; i++) { startArguments[args.Length + i] = _args[i]; } // initialize logger.LogInformation(DispatchServiceEvent("start", startArguments)); } catch (Exception ex) { logger.LogInformation($"Failed to start because of {ex.Message}"); } } else { logger.LogInformation($"Script file not found: {scriptFilePath}"); } // Trace a Sysmon file events (If Sysinternals Sysmon installed) if (!disabledFileMonitor) { fileEventMonitor = new FileEventMonitor(this, workingDirectory, logger); fileEventMonitor.Start(); logger.LogInformation("File Event Monitor Started"); } else { logger.LogInformation("File Event Monitor is Disabled"); } // Start all the registered timers timers.ForEach(timer => timer?.Start()); logger.LogInformation(applicationName + " Service Started"); } protected override void OnStop() { // stop timers timers.ForEach(timer => timer?.Stop()); // stop the File Event Monitor fileEventMonitor?.Stop(); // dispatch stop callback try { logger.LogInformation(DispatchServiceEvent("stop")); scriptControl?.Reset(); } catch (Exception ex) { logger.LogInformation("Exception when stop: " + ex.Message); } scriptControl = null; logger.LogInformation(applicationName + " Service Stopped"); } private void OnUserInteractiveEnvironment() { // check is it a remote desktop session if (GetSystemMetrics(SM_REMOTESESSION) > 0) { disabledScreenTime = true; logger.LogInformation("This application may not work correctly in a remote desktop session"); } // set screen timer if (!disabledScreenTime) { screenMatcher = new ScreenMatch(this, workingDirectory, logger); Timer screenTimer = new Timer { Interval = 1000 // 1 seconds }; screenTimer.Elapsed += OnScreenTime; timers.Add(screenTimer); logger.LogInformation("Screen Time Event Enabled"); } else { disabledScreenTime = true; logger.LogInformation("Screen Time Event Disabled"); } } private void OnElapsedTime(object source, ElapsedEventArgs e) { try { logger.LogInformation(DispatchServiceEvent("elapsedTime")); } catch (Exception ex) { logger.LogInformation("Exception when elapsed time: " + ex.Message); } } private void OnScreenTime(object source, ElapsedEventArgs e) { try { List matchedResults = screenMatcher.CaptureAndMatch(); matchedResults.ForEach(result => { logger.LogInformation(DispatchServiceEvent("screenTemplateMatched", new string[] { result.FileName, result.ScreenNumber.ToString(), result.Position.X.ToString(), result.Position.Y.ToString() })); }); } catch (Exception ex) { logger.LogInformation($"Waiting a next screen time... {ex.Message}"); } } private string InvokeScriptMethod(string methodName, string scriptName, string eventType, string[] args) { if (scriptControl != null) { object[] parameters = new object[] { scriptName, eventType, new ArrayList(args), args.Length }; //scriptControl.AddObject("extern_arguments", new ArrayList(args), true); return scriptControl.Run(methodName, parameters)?.ToString() ?? "void"; } else { logger.LogInformation("InvokeScriptMethod Ignored: " + methodName); } return "void"; } private Dictionary ParseArguments(string[] args) { var arguments = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string arg in args) { if (arg.StartsWith("--")) { var index = arg.IndexOf('='); if (index > 2) { var key = arg.Substring(2, index - 2); var value = arg.Substring(index + 1); arguments[key] = value; } else { var key = arg.Substring(2, index - 2); arguments[key] = ""; } } } return arguments; } public ScreenMatch.TemplateInfo GetNextTemplateInfo() { string templateName = string.Empty; int index = 0; try { templateName = DispatchServiceEvent("screenNextTemplate"); // Check if the received value contains an index string[] parts = templateName.Split(':'); if (parts.Length > 1) { templateName = parts[0]; int.TryParse(parts[1], out index); } } catch (Exception ex) { logger.LogInformation($"Use all templates because of {ex.Message}"); } return new ScreenMatch.TemplateInfo(templateName, index); } public string DispatchServiceEvent(string eventType, string[] args = null) { if (args == null) { return InvokeScriptMethod("dispatchServiceEvent", scriptName, eventType, new string[] { }); } else { return InvokeScriptMethod("dispatchServiceEvent", scriptName, eventType, args); } } } }