├── Installer ├── version.txt ├── logging.nsh └── license.txt ├── docs ├── install_version.txt ├── favicon ├── aboutbox.png ├── mainwindow.png ├── commands_test.png ├── Guillen256x256.png ├── assets │ ├── favicon.png │ ├── images │ │ └── document.png │ └── css │ │ └── style.scss ├── commands_enable.png ├── settings_client.png ├── settings_server.png ├── telemetry_optin.png ├── mainwindow_about.png ├── settings_activity.png ├── settings_general.png ├── settings_serialserver.png ├── _config.yml ├── _layouts │ └── default.html ├── telemetry.md ├── _sass │ └── rouge-github.scss └── example_commands.md ├── src ├── .editorconfig ├── Resources │ ├── header.png │ ├── Guillen.png │ ├── welcome.bmp │ ├── welcome.png │ ├── mcecontrol.png │ ├── Guillen.Icon.ico │ ├── Guillen128x128.png │ ├── Guillen16x16.png │ ├── Guillen229x229.png │ ├── Guillen256x256.png │ ├── Guillen32x32.png │ ├── Guillen38x38.png │ ├── Guillen48x48.png │ ├── Guillen96x96.png │ ├── Guillen_96x96.bmp │ ├── mcecontrol_96x96.png │ ├── windows_greenbutton.jpg │ ├── Trafficlight-red-icon.png │ ├── Trafficlight-gray-icon.bmp │ ├── Trafficlight-gray-icon.png │ ├── Trafficlight-green-icon.png │ └── MCEControl.xslt ├── WindowsInput │ ├── readme.txt │ ├── Native │ │ ├── XButton.cs │ │ ├── InputType.cs │ │ ├── HARDWAREINPUT.cs │ │ ├── MOUSEKEYBDHARDWAREINPUT.cs │ │ ├── KeyboardFlag.cs │ │ ├── INPUT.cs │ │ ├── MouseFlag.cs │ │ └── KEYBDINPUT.cs │ ├── IInputMessageDispatcher.cs │ ├── IInputSimulator.cs │ ├── WindowsInputMessageDispatcher.cs │ ├── MouseButton.cs │ ├── IInputDeviceStateAdaptor.cs │ ├── license.txt │ ├── InputSimulator.cs │ └── IKeyboardSimulator.cs ├── app.config ├── Services │ ├── TelemetryService.tt.cs │ └── TelemetryService.tt ├── Win32 │ ├── Sacl.cs │ ├── Aces.cs │ ├── DisposableObject.cs │ ├── Luid.cs │ ├── Readme.md │ ├── TokenGroup.cs │ ├── TokenGroups.cs │ ├── TokenSource.cs │ ├── UnmanagedHeapAlloc.cs │ ├── TokenStatistics.cs │ ├── MemoryMarshaler.cs │ ├── SecurityDescriptorMarshaler.cs │ ├── TokenPrivileges.cs │ ├── SecurityAttributes.cs │ ├── SecurityAttributesMarshaler.cs │ └── Dacl.cs ├── .github │ ├── ISSUE_TEMPLATE │ │ ├── feature_request.md │ │ └── bug_report.md │ ├── pull_request_template.md │ ├── dependabot.yml │ └── workflows │ │ ├── pr-validation.yml │ │ ├── README.md │ │ └── ci.yml ├── AssemblyFileVersion.tt ├── MCEControl.sln.DotSettings ├── Commands │ ├── PauseCommand.cs │ ├── CharsCommand.cs │ ├── SetForegroundWindowCmd.cs │ ├── McecCommand.cs │ └── SendMessageCommand.cs ├── Dialogs │ ├── UpdateDialog.cs │ └── About.cs ├── Gma.UserActivityMonitor │ ├── MouseEventExtArgs.cs │ └── HookManager.Structures.cs ├── README.md ├── MCEControl.sln ├── Program.cs ├── AssemblyInfo.cs ├── app.manifest ├── Helpers │ ├── TextBoxExt.cs │ └── CommandFileWatcher.cs ├── GITHUB_ACTIONS_SETUP.md └── MCEControl.csproj ├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ └── bug_report.md ├── license.md ├── README.md ├── tests └── MCEControl.xUnit │ ├── MCEControl.xUnit.csproj │ ├── Helpers │ ├── LoggerTests.cs │ └── CommandFileWatcherTests.cs │ ├── Properties │ └── AssemblyInfo.cs │ ├── Commands │ ├── MouseCommandTests.cs │ ├── McecCommandTests.cs │ ├── CharsCommandTests.cs │ ├── SetForegroundWindowCommandTests.cs │ ├── PauseCommandTests.cs │ ├── ShutdownCommandTests.cs │ ├── SendMessageCommandTests.cs │ ├── SerializedCommandsTests.cs │ ├── SendInputCommandTests.cs │ └── StartProcessCommandTests.cs │ ├── AppSettingsTests.cs │ ├── Services │ └── ServiceBaseTests.cs │ └── Integration │ └── CommandExecutionPipelineTests.cs └── .gitignore /Installer/version.txt: -------------------------------------------------------------------------------- 1 | 3.0.0.6 -------------------------------------------------------------------------------- /docs/install_version.txt: -------------------------------------------------------------------------------- 1 | 2.2.0.1 -------------------------------------------------------------------------------- /docs/favicon: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/favicon -------------------------------------------------------------------------------- /docs/aboutbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/aboutbox.png -------------------------------------------------------------------------------- /src/.editorconfig: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/.editorconfig -------------------------------------------------------------------------------- /docs/mainwindow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/mainwindow.png -------------------------------------------------------------------------------- /docs/commands_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/commands_test.png -------------------------------------------------------------------------------- /docs/Guillen256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/Guillen256x256.png -------------------------------------------------------------------------------- /docs/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/assets/favicon.png -------------------------------------------------------------------------------- /docs/commands_enable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/commands_enable.png -------------------------------------------------------------------------------- /docs/settings_client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/settings_client.png -------------------------------------------------------------------------------- /docs/settings_server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/settings_server.png -------------------------------------------------------------------------------- /docs/telemetry_optin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/telemetry_optin.png -------------------------------------------------------------------------------- /src/Resources/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/header.png -------------------------------------------------------------------------------- /docs/mainwindow_about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/mainwindow_about.png -------------------------------------------------------------------------------- /docs/settings_activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/settings_activity.png -------------------------------------------------------------------------------- /docs/settings_general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/settings_general.png -------------------------------------------------------------------------------- /src/Resources/Guillen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen.png -------------------------------------------------------------------------------- /src/Resources/welcome.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/welcome.bmp -------------------------------------------------------------------------------- /src/Resources/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/welcome.png -------------------------------------------------------------------------------- /src/Resources/mcecontrol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/mcecontrol.png -------------------------------------------------------------------------------- /src/WindowsInput/readme.txt: -------------------------------------------------------------------------------- 1 | Windows Input Simluator 2 | 3 | From http://inputsimulator.codeplex.com/ -------------------------------------------------------------------------------- /docs/assets/images/document.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/assets/images/document.png -------------------------------------------------------------------------------- /docs/settings_serialserver.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/docs/settings_serialserver.png -------------------------------------------------------------------------------- /src/Resources/Guillen.Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen.Icon.ico -------------------------------------------------------------------------------- /src/Resources/Guillen128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen128x128.png -------------------------------------------------------------------------------- /src/Resources/Guillen16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen16x16.png -------------------------------------------------------------------------------- /src/Resources/Guillen229x229.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen229x229.png -------------------------------------------------------------------------------- /src/Resources/Guillen256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen256x256.png -------------------------------------------------------------------------------- /src/Resources/Guillen32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen32x32.png -------------------------------------------------------------------------------- /src/Resources/Guillen38x38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen38x38.png -------------------------------------------------------------------------------- /src/Resources/Guillen48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen48x48.png -------------------------------------------------------------------------------- /src/Resources/Guillen96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen96x96.png -------------------------------------------------------------------------------- /src/Resources/Guillen_96x96.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Guillen_96x96.bmp -------------------------------------------------------------------------------- /src/Resources/mcecontrol_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/mcecontrol_96x96.png -------------------------------------------------------------------------------- /src/Resources/windows_greenbutton.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/windows_greenbutton.jpg -------------------------------------------------------------------------------- /src/Resources/Trafficlight-red-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Trafficlight-red-icon.png -------------------------------------------------------------------------------- /src/Resources/Trafficlight-gray-icon.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Trafficlight-gray-icon.bmp -------------------------------------------------------------------------------- /src/Resources/Trafficlight-gray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Trafficlight-gray-icon.png -------------------------------------------------------------------------------- /src/Resources/Trafficlight-green-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tig/mcec/HEAD/src/Resources/Trafficlight-green-icon.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: MCE Controller 2 | logo: https://tig.github.io/mcec/Guillen256x256.png 3 | show_downloads: true 4 | theme: jekyll-theme-dinky 5 | google_analytics: UA-4945529-6 6 | github: 7 | install_url: https://github.com/tig/mcec/releases 8 | -------------------------------------------------------------------------------- /src/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Services/TelemetryService.tt.cs: -------------------------------------------------------------------------------- 1 | // 2 | // This code was generated by a tool. Any changes made manually will be lost 3 | // the next time this code is regenerated. 4 | // 5 | namespace MCEControl { 6 | public partial class TelemetryService { 7 | public const string Key = ""; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Installer/logging.nsh: -------------------------------------------------------------------------------- 1 | !define LogSet "!insertmacro LogSetMacro" 2 | !macro LogSetMacro SETTING 3 | !ifdef ENABLE_LOGGING 4 | LogSet ${SETTING} 5 | !endif 6 | !macroend 7 | 8 | !define LogText "!insertmacro LogTextMacro" 9 | !macro LogTextMacro INPUT_TEXT 10 | !ifdef ENABLE_LOGGING 11 | LogText ${INPUT_TEXT} 12 | !endif 13 | !macroend -------------------------------------------------------------------------------- /src/Win32/Sacl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Win32.Security { 4 | #pragma warning disable CA1710, CA1010 5 | /// 6 | /// Summary description for Sacl. 7 | /// 8 | public class Sacl : Acl { 9 | internal Sacl(IntPtr pacl) : base(pacl) { 10 | } 11 | public Sacl() : base() { 12 | } 13 | protected override void PrepareAcesForACL() { 14 | // We don't need to sort them for SACL 15 | return; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Services/TelemetryService.tt: -------------------------------------------------------------------------------- 1 | <#@ template debug="false" hostspecific="false" language="C#" #> 2 | <#@ assembly name="System.Core" #> 3 | <#@ output extension=".tt.cs" #> 4 | <# 5 | string id = System.Environment.GetEnvironmentVariable("mcec_telemetryId"); 6 | if (id == null) id = ""; 7 | #> 8 | // 9 | // This code was generated by a tool. Any changes made manually will be lost 10 | // the next time this code is regenerated. 11 | // 12 | namespace MCEControl { 13 | public partial class TelemetryService { 14 | public const string Key = "<#= id #>"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Win32/Aces.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Microsoft.Win32.Security { 4 | /// 5 | /// Summary description for Aces. 6 | /// 7 | internal class Aces : CollectionBase { 8 | public Aces() { 9 | } 10 | public Ace this[int index] { 11 | get { 12 | return (Ace)base.InnerList[index]; 13 | } 14 | } 15 | public void SetAce(int i, Ace ace) { 16 | base.InnerList[i] = ace; 17 | } 18 | public void Add(Ace ace) { 19 | base.InnerList.Add(ace); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/XButton.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsInput.Native { 2 | #pragma warning disable 3 | /// 4 | /// XButton definitions for use in the MouseData property of the structure. (See: http://msdn.microsoft.com/en-us/library/ms646273(VS.85).aspx) 5 | /// 6 | public enum XButton : uint { 7 | /// 8 | /// Set if the first X button is pressed or released. 9 | /// 10 | XButton1 = 0x0001, 11 | 12 | /// 13 | /// Set if the second X button is pressed or released. 14 | /// 15 | XButton2 = 0x0002, 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Win32/DisposableObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Win32.Security { 4 | /// 5 | /// Abstract base class for any disposable object. 6 | /// Handle the finalizer and the call the Gc.SuppressFinalize. 7 | /// Derived classes must implement "Dispose(bool disposing)". 8 | /// 9 | public abstract class DisposableObject : IDisposable { 10 | ~DisposableObject() { 11 | Dispose(false); 12 | } 13 | public void Dispose() { 14 | Dispose(true); 15 | GC.SuppressFinalize(this); 16 | } 17 | protected abstract void Dispose(bool disposing); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Win32/Luid.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Win32.Security { 4 | using Win32Structs; 5 | 6 | /// 7 | /// Summary description for Luid. 8 | /// 9 | public class Luid { 10 | private readonly LUID _luid; 11 | public Luid(LUID luid) { 12 | _luid = luid; 13 | } 14 | #pragma warning disable CS3003 // Identifier is not CLS-compliant 15 | public UInt64 Value { 16 | get { 17 | return (((UInt64)(_luid.HighPart)) << 32) | ((UInt64)_luid.LowPart); 18 | } 19 | } 20 | internal LUID GetNativeLUID() { 21 | return _luid; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Win32/Readme.md: -------------------------------------------------------------------------------- 1 | **Win32Security** 2 | 3 | This is a fork of a the Win32Security library from https://github.com/woanware/Win32Security. Originally written by 4 | Renaud Paquay (is/was an Microsoft employee). woanwar modified it to include some service related access masks. https:://github.com/tig further modified it to work with modern .NET tools and versions of Windows, for use in https://github.com/tig/mcec. 5 | 6 | **Description** 7 | 8 | A C# library containing wrapper classes for ACL, ACE, Security descriptors, Security Attributes, Access tokens, etc. 9 | 10 | The original library was located here (no longer valid): 11 | 12 | http://www.gotdotnet.com/Community/UserSamples/Details.aspx?SampleGuid=e6098575-dda0-48b8-9abf-e0705af065d9 -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /src/.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: '[FEATURE] ' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Is your feature request related to a problem? 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | ## Describe the solution you'd like 14 | A clear and concise description of what you want to happen. 15 | 16 | ## Describe alternatives you've considered 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ## Additional context 20 | Add any other context or screenshots about the feature request here. 21 | 22 | ## Would you be willing to contribute this feature? 23 | - [ ] Yes, I'd like to work on this 24 | - [ ] No, but I can help test it 25 | - [ ] No, just suggesting 26 | -------------------------------------------------------------------------------- /src/Win32/TokenGroup.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Win32.Security { 2 | using Win32Structs; 3 | 4 | /// 5 | /// Summary description for TokenGroup. 6 | /// 7 | public class TokenGroup { 8 | private Sid _sid; 9 | private GroupAttributes _attributes; 10 | 11 | internal TokenGroup(MemoryMarshaler m) { 12 | SID_AND_ATTRIBUTES sa = (SID_AND_ATTRIBUTES)m.ParseStruct(typeof(SID_AND_ATTRIBUTES)); 13 | _sid = new Sid(sa.Sid); 14 | _attributes = (GroupAttributes)sa.Attributes; 15 | } 16 | 17 | public Sid Sid { 18 | get { 19 | return _sid; 20 | } 21 | } 22 | public GroupAttributes Attributes { 23 | get { 24 | return _attributes; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Win32/TokenGroups.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace Microsoft.Win32.Security { 4 | #pragma warning disable 5 | using Win32Structs; 6 | 7 | /// 8 | /// Summary description for TokenGroups. 9 | /// 10 | public class TokenGroups : CollectionBase { 11 | internal TokenGroups(UnmanagedHeapAlloc ptr) { 12 | MemoryMarshaler m = new MemoryMarshaler(ptr.Ptr); 13 | TOKEN_GROUPS grps = (TOKEN_GROUPS)m.ParseStruct(typeof(TOKEN_GROUPS)); 14 | for (int i = 0; i < grps.GroupCount; i++) { 15 | TokenGroup grp = new TokenGroup(m); 16 | base.InnerList.Add(grp); 17 | } 18 | } 19 | public TokenGroup this[int index] { 20 | get { 21 | return (TokenGroup)base.InnerList[index]; 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Win32/TokenSource.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Microsoft.Win32.Security { 5 | using Win32Structs; 6 | 7 | /// 8 | /// Summary description for TokenSource. 9 | /// 10 | public class TokenSource { 11 | private readonly string _name; 12 | private readonly Luid _luid; 13 | 14 | internal TokenSource(IntPtr ptr) { 15 | TOKEN_SOURCE ts = (TOKEN_SOURCE)Marshal.PtrToStructure(ptr, typeof(TOKEN_SOURCE)); 16 | _name = new string(ts.Name); 17 | _luid = new Luid(ts.Indentifier); 18 | } 19 | 20 | public string Name { 21 | get { 22 | return _name; 23 | } 24 | } 25 | public Luid Luid { 26 | get { 27 | return _luid; 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WindowsInput/IInputMessageDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using WindowsInput.Native; 3 | 4 | namespace WindowsInput { 5 | /// 6 | /// The contract for a service that dispatches messages to the appropriate destination. 7 | /// 8 | public interface IInputMessageDispatcher { 9 | #pragma warning disable CS3002, CS3003 // Return type is not CLS-compliant 10 | /// 11 | /// Dispatches the specified list of messages in their specified order. 12 | /// 13 | /// The list of messages to be dispatched. 14 | /// The number of messages that were successfully dispatched. 15 | UInt32 DispatchInput(INPUT[] inputs); 16 | #pragma warning restore CS3002 // Return type is not CLS-compliant 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/InputType.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsInput.Native { 2 | #pragma warning disable 3 | /// 4 | /// Specifies the type of the input event. This member can be one of the following values. 5 | /// 6 | public enum InputType : uint // UInt32 7 | { 8 | /// 9 | /// INPUT_MOUSE = 0x00 (The event is a mouse event. Use the mi structure of the union.) 10 | /// 11 | Mouse = 0, 12 | 13 | /// 14 | /// INPUT_KEYBOARD = 0x01 (The event is a keyboard event. Use the ki structure of the union.) 15 | /// 16 | Keyboard = 1, 17 | 18 | /// 19 | /// INPUT_HARDWARE = 0x02 (Windows 95/98/Me: The event is from input hardware other than a keyboard or mouse. Use the hi structure of the union.) 20 | /// 21 | Hardware = 2, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Describe the Bug 11 | A clear and concise description of what the bug is. 12 | 13 | ## To Reproduce 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | ## Expected Behavior 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Actual Behavior 24 | A clear and concise description of what actually happened. 25 | 26 | ## Screenshots 27 | If applicable, add screenshots to help explain your problem. 28 | 29 | ## Environment 30 | - OS: [e.g. Windows 11] 31 | - Version: [e.g. 2.0.0] 32 | - .NET Version: [e.g. .NET 8.0] 33 | 34 | ## Additional Context 35 | Add any other context about the problem here. 36 | 37 | ## Logs 38 | If applicable, attach relevant log files or error messages. 39 | 40 | ``` 41 | Paste logs here 42 | ``` 43 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/HARDWAREINPUT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 649, CA1815, CA1051, CS3003 5 | /// 6 | /// The HARDWAREINPUT structure contains information about a simulated message generated by an input device other than a keyboard or mouse. (see: http://msdn.microsoft.com/en-us/library/ms646269(VS.85).aspx) 7 | /// Declared in Winuser.h, include Windows.h 8 | /// 9 | public struct HARDWAREINPUT { 10 | /// 11 | /// Value specifying the message generated by the input hardware. 12 | /// 13 | public UInt32 Msg; 14 | 15 | /// 16 | /// Specifies the low-order word of the lParam parameter for uMsg. 17 | /// 18 | public UInt16 ParamL; 19 | 20 | /// 21 | /// Specifies the high-order word of the lParam parameter for uMsg. 22 | /// 23 | public UInt16 ParamH; 24 | } 25 | #pragma warning restore 649 26 | } 27 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT 2 | Copyright (c) 2019 Kindel Systems, LLC 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/WindowsInput/Native/MOUSEKEYBDHARDWAREINPUT.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 649, CA1028, CA1714, CA1815, CA1051 5 | /// 6 | /// The combined/overlayed structure that includes Mouse, Keyboard and Hardware Input message data (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) 7 | /// 8 | [StructLayout(LayoutKind.Explicit)] 9 | public struct MOUSEKEYBDHARDWAREINPUT { 10 | /// 11 | /// The definition. 12 | /// 13 | [FieldOffset(0)] 14 | public MOUSEINPUT Mouse; 15 | 16 | /// 17 | /// The definition. 18 | /// 19 | [FieldOffset(0)] 20 | public KEYBDINPUT Keyboard; 21 | 22 | /// 23 | /// The definition. 24 | /// 25 | [FieldOffset(0)] 26 | public HARDWAREINPUT Hardware; 27 | } 28 | #pragma warning restore 649 29 | } 30 | -------------------------------------------------------------------------------- /Installer/license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2019 Kindel Systems, LLC 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /src/WindowsInput/IInputSimulator.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsInput { 2 | /// 3 | /// The contract for a service that simulates Keyboard and Mouse input and Hardware Input Device state detection for the Windows Platform. 4 | /// 5 | public interface IInputSimulator { 6 | /// 7 | /// Gets the instance for simulating Keyboard input. 8 | /// 9 | /// The instance. 10 | IKeyboardSimulator Keyboard { get; } 11 | 12 | /// 13 | /// Gets the instance for simulating Mouse input. 14 | /// 15 | /// The instance. 16 | IMouseSimulator Mouse { get; } 17 | 18 | /// 19 | /// Gets the instance for determining the state of the various input devices. 20 | /// 21 | /// The instance. 22 | IInputDeviceStateAdaptor InputDeviceState { get; } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Win32/UnmanagedHeapAlloc.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | #pragma warning disable 4 | namespace Microsoft.Win32.Security { 5 | /// 6 | /// Summary description for UnmanagedHeapAlloc. 7 | /// 8 | internal class UnmanagedHeapAlloc : DisposableObject { 9 | private IntPtr _memPtr; 10 | private uint _cbLength; 11 | 12 | public UnmanagedHeapAlloc(uint cbLength) { 13 | _memPtr = Win32.AllocGlobal(cbLength); 14 | _cbLength = cbLength; 15 | } 16 | 17 | protected override void Dispose(bool disposing) { 18 | if (_memPtr != IntPtr.Zero) { 19 | Win32.FreeGlobal(_memPtr); 20 | _memPtr = IntPtr.Zero; 21 | } 22 | } 23 | public IntPtr Ptr { 24 | get { 25 | if (_memPtr == IntPtr.Zero) 26 | throw new NullReferenceException("Ptr member is not initialiazed or has already been disposed"); 27 | 28 | return _memPtr; 29 | } 30 | } 31 | public uint Size { 32 | get { 33 | return _cbLength; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | ## Type of Change 5 | 6 | 7 | - [ ] Bug fix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | - [ ] Documentation update 11 | - [ ] Code refactoring 12 | - [ ] Performance improvement 13 | - [ ] Test updates 14 | 15 | ## Testing 16 | 17 | 18 | - [ ] All existing unit tests pass 19 | - [ ] Added new unit tests for changes 20 | - [ ] Manually tested the changes 21 | 22 | ## Checklist 23 | 24 | 25 | - [ ] My code follows the project's code style 26 | - [ ] I have performed a self-review of my code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] Any dependent changes have been merged and published 31 | 32 | ## Related Issues 33 | 34 | 35 | Fixes # 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CodeFactor](https://www.codefactor.io/repository/github/tig/mcec/badge)](https://www.codefactor.io/repository/github/tig/mcec) 2 | 3 | # MCE Controller 4 | 5 | By Tig Kindel ([@ckindel on Twitter](http://www.twitter.com/ckindel)) - Copyright © 2023 [Kindel](http://www.kindel.com), LLC. 6 | 7 | ![mcec](https://tig.github.io/mcec/mainwindow.png "MCE Controller") 8 | 9 | **MCE Controller** (MCEC) provides robust control of Windows PCs for smart home systems. It runs in the background listening on the network (or serial port) for commands. It then translates those commands into actions such as keystrokes, text input, and the starting of programs. Any remote control, home control system, or application that can send text strings via TCP/IP or a serial port can use MCE Controller to control a Windows PC. 10 | 11 | * [Home Page](https://tig.github.io/mcec/) 12 | * [Download & Install](https://github.com/tig/mcec/releases) 13 | * [Documentation](https://tig.github.io/mcec/documentation.html) 14 | * [Tig's Blog Posts on MCE Controller](https://ceklog.kindel.com/?s=mcec) 15 | 16 | # Integrations 17 | * [Control4 User Activity Driver](https://github.com/tig/User_Activity) 18 | * [Control4.MceControllerDriver](https://github.com/garrynewman/Control4.MceControllerDriver) 19 | -------------------------------------------------------------------------------- /src/WindowsInput/WindowsInputMessageDispatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Runtime.InteropServices; 4 | using WindowsInput.Native; 5 | 6 | namespace WindowsInput { 7 | /// 8 | /// Implements the by calling . 9 | /// 10 | internal class WindowsInputMessageDispatcher : IInputMessageDispatcher { 11 | /// 12 | /// Dispatches the specified list of messages in their specified order by issuing a single called to . 13 | /// 14 | /// The list of messages to be dispatched. 15 | /// 16 | /// The number of messages that were successfully dispatched. 17 | /// 18 | public UInt32 DispatchInput(INPUT[] inputs) { 19 | #pragma warning disable CA1829 // Use Length/Count property instead of Count() when available 20 | return NativeMethods.SendInput((UInt32)inputs.Count(), inputs.ToArray(), Marshal.SizeOf(typeof(INPUT))); 21 | #pragma warning restore CA1829 // Use Length/Count property instead of Count() when available 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Version Information** 27 | Include the first line from the log window, like this: 28 | ``` 29 | 2020-03-29 16:42:20,435 INFO - MCE Controller v2.2.3.2 - OS: Microsoft Windows NT 10.0.18363.0 on x64 - .NET: 4.0.30319.42000 30 | ``` 31 | 32 | **Snippets from the log** 33 | Include any other relevant logs (either copy/pasted from the MCE Controller window or from the log files found in `%appdata%\Kindel Systems\MCE Controller` which contain more debug info). 34 | 35 | ``` 36 | Paste Logs here. 37 | ``` 38 | 39 | **Desktop (please complete the following information):** 40 | Details of home control system you are interfacing with. 41 | 42 | **Additional context** 43 | Add any other context about the problem here. 44 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/KeyboardFlag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 5 | /// 6 | /// Specifies various aspects of a keystroke. This member can be certain combinations of the following values. 7 | /// 8 | [Flags] 9 | public enum KeyboardFlag : uint // UInt32 10 | { 11 | /// 12 | /// KEYEVENTF_EXTENDEDKEY = 0x0001 (If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224).) 13 | /// 14 | ExtendedKey = 0x0001, 15 | 16 | /// 17 | /// KEYEVENTF_KEYUP = 0x0002 (If specified, the key is being released. If not specified, the key is being pressed.) 18 | /// 19 | KeyUp = 0x0002, 20 | 21 | /// 22 | /// KEYEVENTF_UNICODE = 0x0004 (If specified, wScan identifies the key and wVk is ignored.) 23 | /// 24 | Unicode = 0x0004, 25 | 26 | /// 27 | /// KEYEVENTF_SCANCODE = 0x0008 (Windows 2000/XP: If specified, the system synthesizes a VK_PACKET keystroke. The wVk parameter must be zero. This flag can only be combined with the KEYEVENTF_KEYUP flag. For more information, see the Remarks section.) 28 | /// 29 | ScanCode = 0x0008, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/WindowsInput/MouseButton.cs: -------------------------------------------------------------------------------- 1 | using WindowsInput.Native; 2 | 3 | namespace WindowsInput { 4 | internal enum MouseButton { 5 | LeftButton, 6 | MiddleButton, 7 | RightButton, 8 | } 9 | 10 | internal static class MouseButtonExtensions { 11 | internal static MouseFlag ToMouseButtonDownFlag(this MouseButton button) { 12 | switch (button) { 13 | case MouseButton.LeftButton: 14 | return MouseFlag.LeftDown; 15 | 16 | case MouseButton.MiddleButton: 17 | return MouseFlag.MiddleDown; 18 | 19 | case MouseButton.RightButton: 20 | return MouseFlag.RightDown; 21 | 22 | default: 23 | return MouseFlag.LeftDown; 24 | } 25 | } 26 | 27 | internal static MouseFlag ToMouseButtonUpFlag(this MouseButton button) { 28 | switch (button) { 29 | case MouseButton.LeftButton: 30 | return MouseFlag.LeftUp; 31 | 32 | case MouseButton.MiddleButton: 33 | return MouseFlag.MiddleUp; 34 | 35 | case MouseButton.RightButton: 36 | return MouseFlag.RightUp; 37 | 38 | default: 39 | return MouseFlag.LeftUp; 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/MCEControl.xUnit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0-windows 4 | true 5 | false 6 | latest 7 | enable 8 | disable 9 | true 10 | false 11 | CA1416 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /docs/assets/css/style.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | @import "{{ site.theme }}"; 5 | 6 | .svg-icon { 7 | width: 16px; 8 | height: 16px; 9 | display: inline-block; 10 | fill: currentColor; 11 | padding: 5px 3px 2px 5px; 12 | vertical-align: middle; 13 | } 14 | 15 | body{ 16 | background-color: #fff7ed; 17 | font-size: 11pt; 18 | } 19 | 20 | pre { 21 | border: 1px solid #ffd1b4; 22 | padding: 4px 12px; 23 | border-radius: 4px; 24 | overflow: auto; 25 | overflow-y: hidden; 26 | margin-bottom: 20px; 27 | line-height: 1.5em; 28 | } 29 | 30 | code { 31 | font-family: "Cascadia Code", "Lucida Console", Terminal, monospace; 32 | font-size: 10pt; 33 | color: #000; 34 | background: #ffe8d1; 35 | border: 1px solid #ffd1b4; 36 | border-radius: 2px; 37 | padding: 0px 2px; 38 | } 39 | 40 | pre code { 41 | border: unset; 42 | } 43 | 44 | .highlight { 45 | background: #ffe8d1; 46 | } 47 | 48 | a{ 49 | color: #da4e00; 50 | } 51 | 52 | a.document { 53 | background: url(../images/document.png) no-repeat 1px; 54 | } 55 | 56 | header { 57 | background-color: #c05a24; 58 | border: 1px solid #943a08; 59 | } 60 | 61 | header li { 62 | border: unset; 63 | background: linear-gradient(to top, #444 0%, #232323 100%); 64 | box-shadow: inset 0px 1px 1px 0 #444; 65 | } 66 | 67 | header img{ 68 | border: none; 69 | } 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/AssemblyFileVersion.tt: -------------------------------------------------------------------------------- 1 | <#@ template language="C#" hostSpecific="True"#> 2 | <#@ output extension="cs" #> 3 | <#@ import namespace="System.IO" #> 4 | <#@ import namespace="System.Diagnostics" #> 5 | <# 6 | string dir = Host.ResolveAssemblyReference("$(SolutionDir)"); 7 | // Host.ResolveAssemblyReference has a bug where when run from msbuild 8 | // it does not honor expansions for $(SolutionDir). Lame workaround. 9 | int n = dir.IndexOf("$(SolutionDir)"); 10 | if (n > -1) 11 | dir = dir.Remove(n, dir.Length - n); 12 | string file = Path.Combine(dir, "..\\Installer\\version.txt"); 13 | Console.WriteLine("Generating " + file + " ..."); 14 | 15 | string contents = File.ReadAllText(file); 16 | 17 | string[] parts = contents.Split('.'); 18 | int build; 19 | if (int.TryParse(parts[3], out build)) 20 | { 21 | // increment the build number 22 | parts[3] = (build + 1).ToString(); 23 | } 24 | string version = string.Join(".", parts); 25 | // write the new version number back to the file 26 | File.WriteAllText(file, version); 27 | Console.WriteLine("Wrote " + version + " to " + file); 28 | #> 29 | // This file was generated by a tool. Any changes made manually will be lost 30 | // the next time this project is built (via AssemblyFileVersion.tt). 31 | using System.Reflection; 32 | [assembly: AssemblyVersion("<#= version #>")] 33 | [assembly: AssemblyFileVersion("<#= version #>")] 34 | [assembly: AssemblyCopyright("Copyright © Kindel, LLC.")] -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Helpers/LoggerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Helpers 6 | { 7 | public class LoggerTests 8 | { 9 | [Fact] 10 | public void Instance_IsSingleton() 11 | { 12 | var instance1 = Logger.Instance; 13 | var instance2 = Logger.Instance; 14 | 15 | Assert.NotNull(instance1); 16 | Assert.Same(instance1, instance2); 17 | } 18 | 19 | [Fact] 20 | public void LogFile_DefaultsToLogFileInConfigPath() 21 | { 22 | var logger = Logger.Instance; 23 | Assert.Contains("MCEControl.log", logger.LogFile); 24 | } 25 | 26 | [Fact] 27 | public void LogFile_CanBeSet() 28 | { 29 | var logger = Logger.Instance; 30 | string originalPath = logger.LogFile; 31 | 32 | try 33 | { 34 | string testPath = @"C:\temp\test.log"; 35 | logger.LogFile = testPath; 36 | Assert.Equal(testPath, logger.LogFile); 37 | } 38 | finally 39 | { 40 | logger.LogFile = originalPath; 41 | } 42 | } 43 | 44 | [Fact] 45 | public void Log4_IsNotNull() 46 | { 47 | var logger = Logger.Instance; 48 | Assert.NotNull(logger.Log4); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("MCEControl.xUnit")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("MCEControl.xUnit")] 13 | [assembly: AssemblyCopyright("Copyright © 2020")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("802c3d76-90a7-4247-8ea9-2fb75811b97a")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Win32/TokenStatistics.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Microsoft.Win32.Security { 5 | #pragma warning disable CA1051 6 | 7 | using Win32Structs; 8 | 9 | /// 10 | /// Summary description for TokenStatistics. 11 | /// 12 | public class TokenStatistics { 13 | #pragma warning disable CS3008, CS3003 // Identifier is not CLS-compliant 14 | public Luid _tokenId; 15 | public Luid _authenticationId; 16 | public DateTime _expirationTime; 17 | public TokenType _tokenType; 18 | public SecurityImpersonationLevel _impersonationLevel; 19 | public UInt32 _dynamicCharged; 20 | public UInt32 _dynamicAvailable; 21 | public UInt32 _groupCount; 22 | public UInt32 _privilegeCount; 23 | public Luid _modifiedId; 24 | 25 | internal TokenStatistics(IntPtr ptr) { 26 | TOKEN_STATISTICS ts = (TOKEN_STATISTICS)Marshal.PtrToStructure(ptr, typeof(TOKEN_STATISTICS)); 27 | _tokenId = new Luid(ts.TokenId); 28 | _authenticationId = new Luid(ts.AuthenticationId); 29 | _expirationTime = new DateTime(ts.ExpirationTime); 30 | _tokenType = ts.TokenType; 31 | _impersonationLevel = ts.ImpersonationLevel; 32 | _dynamicCharged = ts.DynamicCharged; 33 | _dynamicAvailable = ts.DynamicAvailable; 34 | _groupCount = ts.GroupCount; 35 | _privilegeCount = ts.PrivilegeCount; 36 | _modifiedId = new Luid(ts.ModifiedId); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for NuGet packages 4 | - package-ecosystem: "nuget" 5 | directory: "/src" 6 | schedule: 7 | interval: "weekly" 8 | day: "monday" 9 | open-pull-requests-limit: 10 10 | labels: 11 | - "dependencies" 12 | - "nuget" 13 | commit-message: 14 | prefix: "chore" 15 | include: "scope" 16 | groups: 17 | # Group all patch and minor updates together 18 | minor-and-patch: 19 | patterns: 20 | - "*" 21 | update-types: 22 | - "minor" 23 | - "patch" 24 | 25 | # Enable version updates for test project 26 | - package-ecosystem: "nuget" 27 | directory: "/tests/MCEControl.xUnit" 28 | schedule: 29 | interval: "weekly" 30 | day: "monday" 31 | open-pull-requests-limit: 10 32 | labels: 33 | - "dependencies" 34 | - "nuget" 35 | - "tests" 36 | commit-message: 37 | prefix: "chore" 38 | include: "scope" 39 | groups: 40 | test-dependencies: 41 | patterns: 42 | - "xunit*" 43 | - "Microsoft.NET.Test.Sdk" 44 | - "coverlet*" 45 | update-types: 46 | - "minor" 47 | - "patch" 48 | 49 | # Enable version updates for GitHub Actions 50 | - package-ecosystem: "github-actions" 51 | directory: "/" 52 | schedule: 53 | interval: "weekly" 54 | day: "monday" 55 | open-pull-requests-limit: 5 56 | labels: 57 | - "dependencies" 58 | - "github-actions" 59 | commit-message: 60 | prefix: "chore" 61 | include: "scope" 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MCE Controller .gitignore 2 | ################### 3 | # compiled source # 4 | ################### 5 | *.com 6 | *.class 7 | *.dll 8 | *.exe 9 | *.pdb 10 | *.dll.config 11 | *.cache 12 | *.suo 13 | # Include dlls if they�re in the NuGet packages directory 14 | !/packages/*/lib/*.dll 15 | !/packages/*/lib/*/*.dll 16 | # Include dlls if they're in the CommonReferences directory 17 | !*CommonReferences/*.dll 18 | 19 | # Include latest Release exe 20 | !/docs/*.exe 21 | 22 | #################### 23 | # VS Upgrade stuff # 24 | #################### 25 | UpgradeLog.XML 26 | _UpgradeReport_Files/ 27 | ###################### 28 | # VS Stuff 29 | ###################### 30 | *.user 31 | *.suo 32 | .vs/ 33 | ############### 34 | # Directories # 35 | ############### 36 | bin/ 37 | obj/ 38 | TestResults/ 39 | build/ 40 | .vs/ 41 | 42 | ################### 43 | # Web publish log # 44 | ################### 45 | *.Publish.xml 46 | ############# 47 | # Resharper # 48 | ############# 49 | /_ReSharper.* 50 | *.ReSharper.* 51 | ############ 52 | # Packages # 53 | ############ 54 | # it�s better to unpack these files and commit the raw source 55 | # git has its own built in compression methods 56 | *.7z 57 | *.dmg 58 | *.gz 59 | *.iso 60 | *.jar 61 | *.rar 62 | *.tar 63 | *.zip 64 | ###################### 65 | # Logs and databases # 66 | ###################### 67 | App_Data/ 68 | *.log 69 | *.sqlite 70 | # OS generated files # 71 | ###################### 72 | .DS_Store? 73 | ehthumbs.db 74 | Icon? 75 | Thumbs.db 76 | 77 | # Other stuff 78 | .cvsignore 79 | CVS/ 80 | 81 | #Generated for Installer 82 | *.nsi 83 | AssemblyFileVersion.cs 84 | *.settings 85 | 86 | # T4 generated files (for telemety ID) 87 | Installer/*.config 88 | *.tt.cs -------------------------------------------------------------------------------- /src/WindowsInput/Native/INPUT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 649, CA1815, CA1051, CA1724, CS3003 5 | /// 6 | /// The INPUT structure is used by SendInput to store information for synthesizing input events such as keystrokes, mouse movement, and mouse clicks. (see: http://msdn.microsoft.com/en-us/library/ms646270(VS.85).aspx) 7 | /// Declared in Winuser.h, include Windows.h 8 | /// 9 | /// 10 | /// This structure contains information identical to that used in the parameter list of the keybd_event or mouse_event function. 11 | /// Windows 2000/XP: INPUT_KEYBOARD supports nonkeyboard input methods, such as handwriting recognition or voice recognition, as if it were text input by using the KEYEVENTF_UNICODE flag. For more information, see the remarks section of KEYBDINPUT. 12 | /// 13 | public struct INPUT { 14 | /// 15 | /// Specifies the type of the input event. This member can be one of the following values. 16 | /// - The event is a mouse event. Use the mi structure of the union. 17 | /// - The event is a keyboard event. Use the ki structure of the union. 18 | /// - Windows 95/98/Me: The event is from input hardware other than a keyboard or mouse. Use the hi structure of the union. 19 | /// 20 | public UInt32 Type; 21 | 22 | /// 23 | /// The data structure that contains information about the simulated Mouse, Keyboard or Hardware event. 24 | /// 25 | public MOUSEKEYBDHARDWAREINPUT Data; 26 | } 27 | #pragma warning restore 649 28 | } 29 | -------------------------------------------------------------------------------- /src/Win32/MemoryMarshaler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Microsoft.Win32.Security { 5 | #pragma warning disable CA1720 6 | /// 7 | /// Summary description for MemoryMarshaler. 8 | /// 9 | public class MemoryMarshaler { 10 | private IntPtr _ptr; 11 | public MemoryMarshaler(IntPtr ptr) { 12 | _ptr = ptr; 13 | } 14 | 15 | public void Advance(int cbLength) { 16 | long p = _ptr.ToInt64(); 17 | p += cbLength; 18 | _ptr = (IntPtr)p; 19 | } 20 | public object ParseStruct(System.Type type) { 21 | return ParseStruct(type, true); 22 | } 23 | public object ParseStruct(System.Type type, bool moveOffset) { 24 | object o = Marshal.PtrToStructure(_ptr, type); 25 | if (moveOffset) { 26 | Advance(Marshal.SizeOf(type)); 27 | } 28 | 29 | return o; 30 | } 31 | public byte ParseUInt8() { 32 | return (byte)ParseStruct(typeof(byte)); 33 | } 34 | #pragma warning disable CS3002 // Return type is not CLS-compliant 35 | public UInt16 ParseUInt16() { 36 | return (UInt16)ParseStruct(typeof(UInt16)); 37 | } 38 | public UInt32 ParseUInt32() { 39 | return (UInt32)ParseStruct(typeof(UInt32)); 40 | } 41 | public UInt64 ParseUInt64() { 42 | return (UInt64)ParseStruct(typeof(UInt64)); 43 | } 44 | public IntPtr Ptr { 45 | get { 46 | return _ptr; 47 | } 48 | set { 49 | _ptr = value; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MCEControl.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></Policy> 3 | <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb" /></Policy> 4 | <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="_" Suffix="" Style="aaBb_AaBb" /></Policy> -------------------------------------------------------------------------------- /src/Commands/PauseCommand.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2019 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source control on SourceForge 8 | // http://sourceforge.net/projects/mcecontroller/ 9 | //------------------------------------------------------------------- 10 | 11 | using System.Collections.Generic; 12 | 13 | namespace MCEControl { 14 | /// 15 | /// 16 | /// or 17 | /// pause:5000 18 | /// 19 | public class PauseCommand : Command { 20 | public const string CmdPrefix = "pause:"; 21 | 22 | public static new List BuiltInCommands { 23 | get => new List() { 24 | new PauseCommand { Cmd = $"{CmdPrefix}" } // Commands that use form of "cmd:" must define a blank version 25 | }; 26 | } 27 | 28 | public PauseCommand() { } 29 | 30 | public override string ToString() { 31 | return $"Cmd=\"{Cmd}\" Args=\"{Args}\""; 32 | } 33 | 34 | public override ICommand Clone(Reply reply) => base.Clone(reply, new PauseCommand()); 35 | 36 | // ICommand:Execute 37 | public override bool Execute() { 38 | if (!base.Execute()) { 39 | return false; 40 | } 41 | 42 | if (int.TryParse(Args, out int time)) { 43 | Logger.Instance.Log4.Info($"{this.GetType().Name}: Pausing {time}ms"); 44 | // TODO: Is this the smartest way to do this? 45 | System.Threading.Thread.Sleep(time); 46 | } 47 | return true; 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Dialogs/UpdateDialog.cs: -------------------------------------------------------------------------------- 1 | // Copyright © Kindel, LLC - http://www.kindel.com 2 | // Published under the MIT License - Source on GitHub: https://github.com/tig/mcec 3 | 4 | using System; 5 | using System.Diagnostics; 6 | using System.Windows.Forms; 7 | 8 | namespace MCEControl.Dialogs { 9 | public partial class UpdateDialog : Form { 10 | 11 | private static readonly Lazy _lazy = new Lazy(() => new UpdateDialog()); 12 | public static UpdateDialog Instance { get { return _lazy.Value; } } 13 | 14 | public UpdateDialog() { 15 | InitializeComponent(); 16 | 17 | StartPosition = FormStartPosition.CenterParent; 18 | 19 | this.labelNewVersion.Text = $"A newer version of MCE Controller ({UpdateService.Instance.LatestStableVersion}) is available."; 20 | this.linkReleasePage.Links[0].LinkData = UpdateService.Instance.ReleasePageUri.AbsoluteUri; 21 | } 22 | 23 | internal void UpdateService_CheckForUpdates(object sender, EventArgs e) { 24 | UpdateService.Instance.CheckVersion(); 25 | } 26 | 27 | private void downloadButton_Click(object sender, EventArgs args) { 28 | UpdateService.Instance.StartUpgrade(); 29 | } 30 | 31 | private void linkReleasePage_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { 32 | Process.Start((string)linkReleasePage.Links[0].LinkData); 33 | } 34 | 35 | private void UpdateDialog_VisibleChanged(object sender, EventArgs e) { 36 | if (Visible) 37 | UpdateService.Instance.CheckForUpdates -= UpdateService_CheckForUpdates; 38 | else 39 | UpdateService.Instance.CheckForUpdates += UpdateService_CheckForUpdates; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Helpers/CommandFileWatcherTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Xunit; 4 | using MCEControl; 5 | 6 | namespace MCEControl.xUnit.Helpers 7 | { 8 | public class CommandFileWatcherTests : IDisposable 9 | { 10 | private string? _tempFile; 11 | 12 | [Fact] 13 | public void Constructor_ThrowsIfFilePathNull() 14 | { 15 | Assert.Throws(() => new CommandFileWatcher(null!)); 16 | } 17 | 18 | [Fact] 19 | public void Constructor_CreatesWatcher() 20 | { 21 | _tempFile = Path.GetTempFileName(); 22 | var watcher = new CommandFileWatcher(_tempFile); 23 | 24 | Assert.NotNull(watcher); 25 | watcher.Dispose(); 26 | } 27 | 28 | [Fact] 29 | public void ChangedEvent_CanBeSubscribed() 30 | { 31 | _tempFile = Path.GetTempFileName(); 32 | var watcher = new CommandFileWatcher(_tempFile); 33 | 34 | bool eventFired = false; 35 | watcher.ChangedEvent += (sender, args) => { eventFired = true; }; 36 | 37 | // Just test that the event can be subscribed to 38 | // File system watcher events can be flaky in tests 39 | Assert.False(eventFired); // Should not have fired yet 40 | 41 | watcher.Dispose(); 42 | } 43 | 44 | public void Dispose() 45 | { 46 | if (_tempFile != null && File.Exists(_tempFile)) 47 | { 48 | try 49 | { 50 | File.Delete(_tempFile); 51 | } 52 | catch 53 | { 54 | // Ignore cleanup errors 55 | } 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Gma.UserActivityMonitor/MouseEventExtArgs.cs: -------------------------------------------------------------------------------- 1 | using System.Windows.Forms; 2 | 3 | namespace Gma.UserActivityMonitor { 4 | #pragma warning disable CA1710 // Identifiers should have correct suffix 5 | 6 | /// 7 | /// Provides data for the MouseClickExt and MouseMoveExt events. It also provides a property Handled. 8 | /// Set this property to true to prevent further processing of the event in other applications. 9 | /// 10 | public class MouseEventExtArgs : MouseEventArgs { 11 | /// 12 | /// Initializes a new instance of the MouseEventArgs class. 13 | /// 14 | /// One of the MouseButtons values indicating which mouse button was pressed. 15 | /// The number of times a mouse button was pressed. 16 | /// The x-coordinate of a mouse click, in pixels. 17 | /// The y-coordinate of a mouse click, in pixels. 18 | /// A signed count of the number of detents the wheel has rotated. 19 | public MouseEventExtArgs(MouseButtons buttons, int clicks, int x, int y, int delta) 20 | : base(buttons, clicks, x, y, delta) { } 21 | 22 | /// 23 | /// Initializes a new instance of the MouseEventArgs class. 24 | /// 25 | /// An ordinary argument to be extended. 26 | internal MouseEventExtArgs(MouseEventArgs e) : base(e.Button, e.Clicks, e.X, e.Y, e.Delta) { } 27 | 28 | private bool m_Handled; 29 | 30 | /// 31 | /// Set this property to true inside your event handler to prevent further processing of the event in other applications. 32 | /// 33 | public bool Handled { 34 | get { return m_Handled; } 35 | set { m_Handled = value; } 36 | } 37 | } 38 | } 39 | #pragma warning restore CA1710 // Identifiers should have correct suffix 40 | 41 | -------------------------------------------------------------------------------- /src/Win32/SecurityDescriptorMarshaler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.InteropServices; 4 | 5 | namespace Microsoft.Win32.Security { 6 | #pragma warning disable 7 | /// 8 | /// Custom marshaler for a SecurityDescriptor. 9 | /// We only support the Managed -> Native directionality. 10 | /// 11 | public class SecurityDescriptorMarshaler : ICustomMarshaler { 12 | /// 13 | /// Required by the runtime marshaler. 14 | /// 15 | /// 16 | /// Our marshaler for the SecurityDescriptor class 17 | public static ICustomMarshaler GetInstance(string data) { 18 | return new SecurityDescriptorMarshaler(); ; 19 | } 20 | 21 | private SecurityDescriptor _wrapper; 22 | 23 | #region ICustomMarshaler Members 24 | public object MarshalNativeToManaged(System.IntPtr pNativeData) { 25 | throw new NotSupportedException( 26 | "Marshaling a security descriptor from unmanged memory is not supported." + 27 | "Use the static memebers of the SecurityDescriptor class instead."); 28 | } 29 | public System.IntPtr MarshalManagedToNative(object ManagedObj) { 30 | SecurityDescriptor sdIn = (SecurityDescriptor)ManagedObj; 31 | IntPtr sdPtr = IntPtr.Zero; 32 | if (sdIn != null) { 33 | _wrapper = sdIn; // Keep a ref to the managed object 34 | sdPtr = sdIn.Ptr; 35 | } 36 | 37 | Debug.Assert(sdPtr != IntPtr.Zero, 38 | "Warning: It's almost always a bad idea to use a NULL security descriptor"); 39 | return sdPtr; 40 | } 41 | public void CleanUpManagedData(object ManagedObj) { 42 | } 43 | public int GetNativeDataSize() { 44 | // The return value is currently ignored. 45 | return -1; 46 | } 47 | public void CleanUpNativeData(System.IntPtr pNativeData) { 48 | } 49 | 50 | #endregion 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/.github/workflows/pr-validation.yml: -------------------------------------------------------------------------------- 1 | name: PR Validation 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - develop 7 | types: [opened, synchronize, reopened, ready_for_review] 8 | 9 | jobs: 10 | # Skip draft PRs 11 | check-pr-status: 12 | name: Check PR Status 13 | runs-on: ubuntu-latest 14 | outputs: 15 | should-skip: ${{ steps.check.outputs.should-skip }} 16 | steps: 17 | - name: Check if PR is draft 18 | id: check 19 | run: | 20 | if [[ "${{ github.event.pull_request.draft }}" == "true" ]]; then 21 | echo "should-skip=true" >> $GITHUB_OUTPUT 22 | echo "This is a draft PR, skipping validation" 23 | else 24 | echo "should-skip=false" >> $GITHUB_OUTPUT 25 | fi 26 | 27 | validate-pr-title: 28 | name: Validate PR Title 29 | runs-on: ubuntu-latest 30 | needs: check-pr-status 31 | if: needs.check-pr-status.outputs.should-skip != 'true' 32 | steps: 33 | - name: Check PR title format 34 | uses: amannn/action-semantic-pull-request@v5 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | types: | 39 | feat 40 | fix 41 | docs 42 | style 43 | refactor 44 | perf 45 | test 46 | build 47 | ci 48 | chore 49 | revert 50 | requireScope: false 51 | subjectPattern: ^[A-Z].+$ 52 | subjectPatternError: | 53 | The subject "{subject}" found in the PR title "{title}" 54 | doesn't match the configured pattern. Please ensure that the subject 55 | starts with an uppercase character. 56 | 57 | check-conflicts: 58 | name: Check for Merge Conflicts 59 | runs-on: ubuntu-latest 60 | needs: check-pr-status 61 | if: needs.check-pr-status.outputs.should-skip != 'true' 62 | steps: 63 | - name: Check for merge conflicts 64 | uses: eps1lon/actions-label-merge-conflict@v3 65 | with: 66 | dirtyLabel: "merge-conflict" 67 | repoToken: "${{ secrets.GITHUB_TOKEN }}" 68 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Build & Deploy Info 2 | 3 | ## Pre-requisites 4 | 5 | * .NET 8.0 SDK or greater 6 | * Visual Studio 2022 or greater (optional - can build from command line with `dotnet build`) 7 | * NSIS 3.x - `winget install nsis` (for creating the installer) 8 | 9 | ## Building 10 | 11 | From the command line: 12 | ```bash 13 | cd src 14 | dotnet restore 15 | dotnet build 16 | ``` 17 | 18 | Or open `src/MCEControl.sln` in Visual Studio 2022 and build from there. 19 | 20 | ## Versions & Updates 21 | 22 | Upon build 23 | 24 | * the build (major.minor.rev.build) in `Installer/version.txt` is bumped. 25 | * `src/AssemblyFileVersion.tt` is processed by the T4 compiler. This generates `src/AssemblyFileVersion.cs` and updates `Installer/version.txt`. 26 | 27 | Releases are published at https://github.com/tig/mcec/releases 28 | 29 | Debug builds check for pre-releases and there should ALWAYS be a fake, NEWER pre-release like https://github.com/tig/mcec/releases/tag/v2.3.6.0 to force testing of the update system. 30 | 31 | ## Installer 32 | 33 | * `Installer\MCEController Setup.exe` 34 | * Built using NSIS. A post-build event runs `makensis.exe` against `Installer/Installer.nsi` 35 | 36 | ## How to Release a new version 37 | 38 | 1. Rebuild All for Release 39 | 1. `cat Installer/version.txt` 40 | * E.g. `2.2.10.2` 41 | 1. `git tag -a -m "Release v2.2.10" v2.2.10.2` 42 | 1. `git add .` 43 | 1. `git commit -n "Release v2.2.10"` 44 | 1. `git push --tags` 45 | 1. `git push --all` 46 | 1. On [Releases page](https://github.com/tig/mcec/releases) "Draft New Release" 47 | * Title of form "MCE Controller Version 2.2.10" 48 | * Auto generate release notes and edit as needed. 49 | * Add the following to end: 50 | 51 | ```` 52 | To install, copy and paste the following command into a PowerShell command window. 53 | 54 | 55 | ```powershell 56 | $mcecv="v2.2.10.2"; $mcec="MCEController.Setup.exe"; iwr https://github.com/tig/mcec/releases/download/$mcecv/$mcec -outfile "$env:temp\$mcec"; start "$env:temp\$mcec" 57 | ``` 58 | ```` 59 | * Drag & Drop `Installer/MCEController Setup.exe` to "Attach binaries..." 60 | * Click Publish Release 61 | 62 | ## Unit Tests 63 | 64 | * uses xUnit 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/Commands/CharsCommand.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2019 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source control on SourceForge 8 | // http://sourceforge.net/projects/mcecontroller/ 9 | //------------------------------------------------------------------- 10 | 11 | using System.Collections.Generic; 12 | using System.Text.RegularExpressions; 13 | using WindowsInput; 14 | 15 | namespace MCEControl { 16 | /// 17 | /// Supports sending raw text. 18 | /// 19 | public class CharsCommand : Command { 20 | public const string CmdPrefix = "chars:"; 21 | 22 | public static new List BuiltInCommands { 23 | get => new List() { 24 | new CharsCommand { Cmd = $"{CmdPrefix}" }, 25 | }; 26 | } 27 | 28 | public CharsCommand() { } 29 | 30 | public override ICommand Clone(Reply reply) => base.Clone(reply, new CharsCommand()); 31 | 32 | // ICommand:Execute 33 | public override bool Execute() { 34 | if (!base.Execute()) { 35 | return false; 36 | } 37 | 38 | string text; 39 | // if command came in as a literal "chars:foo" command use args 40 | // otherwise, use the Chars property 41 | if (!string.IsNullOrEmpty(Args)) { 42 | text = Regex.Unescape(Args); 43 | } 44 | else { 45 | text = ""; 46 | } 47 | 48 | Logger.Instance.Log4.Info($"{this.GetType().Name}: Typing {text.Length} chars: {text}"); 49 | 50 | // TODO: Change this such that it treats each char of `text` as a keydown/keyup 51 | // pair vs how `TextEntry()` currently works. See Issue #14. 52 | // OR, implement a new command, keyboard: where Keyboard.KeyPress(vk) is used for 53 | // each character. 54 | new InputSimulator().Keyboard.TextEntry(text); 55 | return true; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Win32/TokenPrivileges.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Diagnostics; 4 | using System.Runtime.InteropServices; 5 | 6 | #pragma warning disable 7 | namespace Microsoft.Win32.Security { 8 | using Win32Structs; 9 | 10 | /// 11 | /// Summary description for TokenPrivileges. 12 | /// 13 | public class TokenPrivileges : CollectionBase { 14 | internal TokenPrivileges(UnmanagedHeapAlloc ptr) { 15 | MemoryMarshaler m = new MemoryMarshaler(ptr.Ptr); 16 | TOKEN_PRIVILEGES privs = (TOKEN_PRIVILEGES)m.ParseStruct(typeof(TOKEN_PRIVILEGES)); 17 | for (int i = 0; i < privs.PrivilegeCount; i++) { 18 | TokenPrivilege priv = new TokenPrivilege(m); 19 | base.InnerList.Add(priv); 20 | } 21 | } 22 | public TokenPrivileges() { 23 | } 24 | public TokenPrivilege this[int index] { 25 | get { 26 | return (TokenPrivilege)base.InnerList[index]; 27 | } 28 | } 29 | public void Add(TokenPrivilege privilege) { 30 | base.InnerList.Add(privilege); 31 | } 32 | public unsafe byte[] GetNativeTOKEN_PRIVILEGES() { 33 | Debug.Assert(Marshal.SizeOf(typeof(TOKEN_PRIVILEGES)) == 4); 34 | 35 | TOKEN_PRIVILEGES tp; 36 | tp.PrivilegeCount = (uint)this.Count; 37 | 38 | int cbLength = 39 | Marshal.SizeOf(typeof(TOKEN_PRIVILEGES)) + 40 | Marshal.SizeOf(typeof(LUID_AND_ATTRIBUTES)) * this.Count; 41 | byte[] res = new byte[cbLength]; 42 | fixed (byte* privs = res) { 43 | Marshal.StructureToPtr(tp, (IntPtr)privs, false); 44 | } 45 | 46 | int resOffset = Marshal.SizeOf(typeof(TOKEN_PRIVILEGES)); 47 | for (int i = 0; i < this.Count; i++) { 48 | byte[] luida = this[i].GetNativeLUID_AND_ATTRIBUTES(); 49 | Array.Copy(luida, 0, res, resOffset, luida.Length); 50 | resOffset += luida.Length; 51 | } 52 | return res; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/MouseCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class MouseCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new MouseCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsMouseCommands() 19 | { 20 | var builtIns = MouseCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | Assert.Contains(builtIns, c => c.Cmd == "mouse:"); 23 | } 24 | 25 | [Fact] 26 | public void Clone_CreatesIndependentCopy() 27 | { 28 | var original = new MouseCommand 29 | { 30 | Cmd = "mouse:", 31 | Args = "left", 32 | Enabled = true 33 | }; 34 | 35 | var clone = (MouseCommand)original.Clone(null); 36 | 37 | Assert.Equal(original.Cmd, clone.Cmd); 38 | Assert.Equal(original.Args, clone.Args); 39 | Assert.Equal(original.Enabled, clone.Enabled); 40 | Assert.NotSame(original, clone); 41 | } 42 | 43 | [Fact] 44 | public void Execute_WhenDisabled_ReturnsFalse() 45 | { 46 | var cmd = new MouseCommand 47 | { 48 | Cmd = "mouse:", 49 | Args = "left", 50 | Enabled = false 51 | }; 52 | 53 | bool result = cmd.Execute(); 54 | Assert.False(result); 55 | } 56 | 57 | [Fact] 58 | public void ToString_ReturnsFormattedString() 59 | { 60 | var cmd = new MouseCommand 61 | { 62 | Cmd = "mouse:" 63 | }; 64 | 65 | string result = cmd.ToString(); 66 | Assert.Contains("mouse:", result); 67 | // ToString uses base Command.ToString() which includes Cmd and Args 68 | // Args is empty by default, so just verify Cmd is present 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MCEControl.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio Version 17 3 | VisualStudioVersion = 17.1.32127.271 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCEControl", "MCEControl.csproj", "{9489CAA5-2568-4ED5-A9AA-F5941CC608EB}" 6 | EndProject 7 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5ACFD7B1-E7F8-4138-86F8-2FA75EFEC9A7}" 8 | ProjectSection(SolutionItems) = preProject 9 | ..\Installer\Installer.tt = ..\Installer\Installer.tt 10 | Installer\license.txt = Installer\license.txt 11 | ..\Installer\logging.nsh = ..\Installer\logging.nsh 12 | Win32\Readme.md = Win32\Readme.md 13 | README.md = README.md 14 | ..\Installer\version.txt = ..\Installer\version.txt 15 | EndProjectSection 16 | EndProject 17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MCEControl.xUnit", "..\tests\MCEControl.xUnit\MCEControl.xUnit.csproj", "{802C3D76-90A7-4247-8EA9-2FB75811B97A}" 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {9489CAA5-2568-4ED5-A9AA-F5941CC608EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {9489CAA5-2568-4ED5-A9AA-F5941CC608EB}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {9489CAA5-2568-4ED5-A9AA-F5941CC608EB}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {9489CAA5-2568-4ED5-A9AA-F5941CC608EB}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {802C3D76-90A7-4247-8EA9-2FB75811B97A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {802C3D76-90A7-4247-8EA9-2FB75811B97A}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {802C3D76-90A7-4247-8EA9-2FB75811B97A}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {802C3D76-90A7-4247-8EA9-2FB75811B97A}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {F28C6E42-3CF3-413D-AB21-8B4CBE8E7CE6} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/McecCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class McecCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new McecCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsMcecCommands() 19 | { 20 | var builtIns = McecCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | 23 | // Check for expected built-in MCE Controller commands 24 | Assert.Contains(builtIns, c => c.Cmd == "mcec:"); 25 | Assert.Contains(builtIns, c => c.Cmd == "mcec:ver"); 26 | Assert.Contains(builtIns, c => c.Cmd == "mcec:exit"); 27 | Assert.Contains(builtIns, c => c.Cmd == "mcec:cmds"); 28 | Assert.Contains(builtIns, c => c.Cmd == "mcec:time"); 29 | } 30 | 31 | [Fact] 32 | public void Clone_CreatesIndependentCopy() 33 | { 34 | var original = new McecCommand 35 | { 36 | Cmd = "mcec:reload", 37 | Enabled = true 38 | }; 39 | 40 | var clone = (McecCommand)original.Clone(null); 41 | 42 | Assert.Equal(original.Cmd, clone.Cmd); 43 | Assert.Equal(original.Enabled, clone.Enabled); 44 | Assert.NotSame(original, clone); 45 | } 46 | 47 | [Fact] 48 | public void Execute_WhenDisabled_ReturnsFalse() 49 | { 50 | var cmd = new McecCommand 51 | { 52 | Cmd = "mcec:reload", 53 | Enabled = false 54 | }; 55 | 56 | bool result = cmd.Execute(); 57 | Assert.False(result); 58 | } 59 | 60 | [Fact] 61 | public void ToString_ReturnsFormattedString() 62 | { 63 | var cmd = new McecCommand 64 | { 65 | Cmd = "mcec:exit" 66 | }; 67 | 68 | string result = cmd.ToString(); 69 | Assert.Contains("mcec:exit", result); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Resources/MCEControl.xslt: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 9 | 10 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/CharsCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class CharsCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new CharsCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsCharsCommand() 19 | { 20 | var builtIns = CharsCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | Assert.Contains(builtIns, c => c.Cmd == "chars:"); 23 | } 24 | 25 | [Fact] 26 | public void Clone_CreatesIndependentCopy() 27 | { 28 | var original = new CharsCommand 29 | { 30 | Cmd = "chars:", 31 | Args = "Hello World", 32 | Enabled = true 33 | }; 34 | 35 | var clone = (CharsCommand)original.Clone(null); 36 | 37 | Assert.Equal(original.Cmd, clone.Cmd); 38 | Assert.Equal(original.Args, clone.Args); 39 | Assert.Equal(original.Enabled, clone.Enabled); 40 | Assert.NotSame(original, clone); 41 | } 42 | 43 | [Fact] 44 | public void Execute_WhenDisabled_ReturnsFalse() 45 | { 46 | var cmd = new CharsCommand 47 | { 48 | Cmd = "chars:", 49 | Args = "test", 50 | Enabled = false 51 | }; 52 | 53 | bool result = cmd.Execute(); 54 | Assert.False(result); 55 | } 56 | 57 | [Fact] 58 | public void ToString_ReturnsFormattedString() 59 | { 60 | var cmd = new CharsCommand 61 | { 62 | Cmd = "chars:", 63 | Args = "test text" 64 | }; 65 | 66 | string result = cmd.ToString(); 67 | Assert.Contains("chars:", result); 68 | Assert.Contains("test text", result); 69 | } 70 | 71 | [Fact] 72 | public void Args_CanBeSet() 73 | { 74 | var cmd = new CharsCommand 75 | { 76 | Args = "Test String" 77 | }; 78 | 79 | Assert.Equal("Test String", cmd.Args); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Win32/SecurityAttributes.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.Win32.Security { 4 | #pragma warning disable CA1303, CA1507 5 | using Win32Structs; 6 | 7 | /// 8 | /// Summary description for SecurityAttributes. 9 | /// 10 | public class SecurityAttributes { 11 | private bool _inheritHandles = false; 12 | private SecurityDescriptor _secDesc; 13 | 14 | public SecurityAttributes() { 15 | } 16 | // 17 | // Only called from the custom marshaler 18 | // 19 | internal void Parse(IntPtr pSecAttr) { 20 | if (pSecAttr == IntPtr.Zero) { 21 | throw new ArgumentException("Can't parse a NULL struct", "pSecAttr"); 22 | } 23 | 24 | SECURITY_ATTRIBUTES attrs = (SECURITY_ATTRIBUTES) 25 | new MemoryMarshaler(pSecAttr).ParseStruct(typeof(SECURITY_ATTRIBUTES)); 26 | 27 | _inheritHandles = Win32.ToBool(attrs.bInheritHandle); 28 | 29 | if (attrs.lpSecurityDescriptor != IntPtr.Zero) { 30 | // Create a new SecDesc only if we don't already have one or 31 | // if the one we have hasn't the same ptr to unmanged memory 32 | if (_secDesc == null || _secDesc.Ptr != attrs.lpSecurityDescriptor) { 33 | _secDesc = new SecurityDescriptor(attrs.lpSecurityDescriptor); 34 | } 35 | } 36 | } 37 | // 38 | // Only called from the custom marshaler 39 | // 40 | internal SECURITY_ATTRIBUTES GetSECURITY_ATTRIBUTES() { 41 | SECURITY_ATTRIBUTES attrs = new SECURITY_ATTRIBUTES(); 42 | attrs.nLength = (uint)SECURITY_ATTRIBUTES.SizeOf; ; 43 | attrs.bInheritHandle = (_inheritHandles ? Win32.TRUE : Win32.FALSE); 44 | attrs.lpSecurityDescriptor = (_secDesc == null ? IntPtr.Zero : _secDesc.Ptr); 45 | return attrs; 46 | } 47 | public bool InheridHandles { 48 | get { 49 | return _inheritHandles; 50 | } 51 | set { 52 | _inheritHandles = value; 53 | } 54 | } 55 | public SecurityDescriptor SecurityDescriptor { 56 | get { 57 | return _secDesc; 58 | } 59 | set { 60 | _secDesc = value; 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/WindowsInput/IInputDeviceStateAdaptor.cs: -------------------------------------------------------------------------------- 1 | using WindowsInput.Native; 2 | 3 | namespace WindowsInput { 4 | /// 5 | /// The contract for a service that interprets the state of input devices. 6 | /// 7 | public interface IInputDeviceStateAdaptor { 8 | /// 9 | /// Determines whether the specified key is up or down. 10 | /// 11 | /// The for the key. 12 | /// 13 | /// true if the key is down; otherwise, false. 14 | /// 15 | bool IsKeyDown(VirtualKeyCode keyCode); 16 | 17 | /// 18 | /// Determines whether the specified key is up or down. 19 | /// 20 | /// The for the key. 21 | /// 22 | /// true if the key is up; otherwise, false. 23 | /// 24 | bool IsKeyUp(VirtualKeyCode keyCode); 25 | 26 | /// 27 | /// Determines whether the physical key is up or down at the time the function is called regardless of whether the application thread has read the keyboard event from the message pump. 28 | /// 29 | /// The for the key. 30 | /// 31 | /// true if the key is down; otherwise, false. 32 | /// 33 | bool IsHardwareKeyDown(VirtualKeyCode keyCode); 34 | 35 | /// 36 | /// Determines whether the physical key is up or down at the time the function is called regardless of whether the application thread has read the keyboard event from the message pump. 37 | /// 38 | /// The for the key. 39 | /// 40 | /// true if the key is up; otherwise, false. 41 | /// 42 | bool IsHardwareKeyUp(VirtualKeyCode keyCode); 43 | 44 | /// 45 | /// Determines whether the toggling key is toggled on (in-effect) or not. 46 | /// 47 | /// The for the key. 48 | /// 49 | /// true if the toggling key is toggled on (in-effect); otherwise, false. 50 | /// 51 | bool IsTogglingKeyInEffect(VirtualKeyCode keyCode); 52 | } 53 | } -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/SetForegroundWindowCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class SetForegroundWindowCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new SetForegroundWindowCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsSetForegroundWindowCommands() 19 | { 20 | var builtIns = SetForegroundWindowCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | // Actual built-in commands use specific app names, not generic "setforegroundwindow:" 23 | Assert.Contains(builtIns, c => c.Cmd == "activatecode"); 24 | Assert.Contains(builtIns, c => c.Cmd == "activatenotepad"); 25 | Assert.Contains(builtIns, c => c.Cmd == "activatemcec"); 26 | } 27 | 28 | [Fact] 29 | public void Clone_CreatesIndependentCopy() 30 | { 31 | var original = new SetForegroundWindowCommand 32 | { 33 | Cmd = "setforegroundwindow:", 34 | Args = "notepad", 35 | Enabled = true 36 | }; 37 | 38 | var clone = (SetForegroundWindowCommand)original.Clone(null); 39 | 40 | Assert.Equal(original.Cmd, clone.Cmd); 41 | Assert.Equal(original.Args, clone.Args); 42 | Assert.Equal(original.Enabled, clone.Enabled); 43 | Assert.NotSame(original, clone); 44 | } 45 | 46 | [Fact] 47 | public void Execute_WhenDisabled_ReturnsFalse() 48 | { 49 | var cmd = new SetForegroundWindowCommand 50 | { 51 | Cmd = "setforegroundwindow:", 52 | Args = "notepad", 53 | Enabled = false 54 | }; 55 | 56 | bool result = cmd.Execute(); 57 | Assert.False(result); 58 | } 59 | 60 | [Fact] 61 | public void ToString_ReturnsFormattedString() 62 | { 63 | var cmd = new SetForegroundWindowCommand 64 | { 65 | Cmd = "activate", 66 | AppName = "Calculator" 67 | }; 68 | 69 | string result = cmd.ToString(); 70 | Assert.Contains("activate", result); 71 | Assert.Contains("Calculator", result); // ToString uses AppName not Args 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Dialogs/About.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright � 2018 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source control on SourceForge 8 | // http://sourceforge.net/projects/mcecontroller/ 9 | //------------------------------------------------------------------- 10 | 11 | using System; 12 | using System.Diagnostics; 13 | using System.Windows.Forms; 14 | using MCEControl.Properties; 15 | 16 | namespace MCEControl { 17 | /// 18 | /// About box 19 | /// 20 | 21 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1501", Justification = "WinForms generated", Scope = "namespace")] 22 | partial class About : Form { 23 | private Label _labelTitle; 24 | private Button _buttonOk; 25 | private LinkLabel _linkLabelMceController; 26 | private LinkLabel _linkLabelKindelSystems; 27 | private Label _labelSummary; 28 | private PictureBox _iconMcec; 29 | private Label _labelVersion; 30 | private Label _label1; 31 | 32 | /// 33 | /// Required designer variable. 34 | /// 35 | public About() { 36 | // 37 | // Required for Windows Form Designer support 38 | // 39 | InitializeComponent(); 40 | // https://www.sgrottel.de/?p=1581&lang=en 41 | //Font = System.Drawing.SystemFonts.DialogFont; 42 | //_label1.Font = 43 | //_labelSummary.Font = _labelTitle.Font = Font; 44 | //_linkLabelKindelSystems.Font = Font; 45 | //_linkLabelMceController.Font = System.Drawing.SystemFonts.; 46 | 47 | _labelVersion.Text = $"{Resources.MCE_Controller_Version_label} {Application.ProductVersion}"; 48 | 49 | UpdateService.Instance.CheckVersion(); 50 | } 51 | 52 | private void ButtonOkClick(object sender, EventArgs e) => Close(); 53 | 54 | private void LinkLabelMceControllerLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { 55 | TelemetryService.Instance.TrackEvent("About Box License Link Clicked"); 56 | Process.Start(_linkLabelMceController.Tag.ToString()); 57 | } 58 | 59 | private void LinkLabelCharlieLinkClicked(object sender, LinkLabelLinkClickedEventArgs e) { 60 | TelemetryService.Instance.TrackEvent("About Box Kindel Systems Page Link Clicked"); 61 | Process.Start(_linkLabelKindelSystems.Tag.ToString()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/WindowsInput/license.txt: -------------------------------------------------------------------------------- 1 | Microsoft Public License (Ms-PL) 2 | 3 | This license governs use of the accompanying software. If you use the software, you accept this license. If you do not accept the license, do not use the software. 4 | 5 | 1. Definitions 6 | 7 | The terms "reproduce," "reproduction," "derivative works," and "distribution" have the same meaning here as under U.S. copyright law. 8 | 9 | A "contribution" is the original software, or any additions or changes to the software. 10 | 11 | A "contributor" is any person that distributes its contribution under this license. 12 | 13 | "Licensed patents" are a contributor's patent claims that read directly on its contribution. 14 | 15 | 2. Grant of Rights 16 | 17 | (A) Copyright Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free copyright license to reproduce its contribution, prepare derivative works of its contribution, and distribute its contribution or any derivative works that you create. 18 | 19 | (B) Patent Grant- Subject to the terms of this license, including the license conditions and limitations in section 3, each contributor grants you a non-exclusive, worldwide, royalty-free license under its licensed patents to make, have made, use, sell, offer for sale, import, and/or otherwise dispose of its contribution in the software or derivative works of the contribution in the software. 20 | 21 | 3. Conditions and Limitations 22 | 23 | (A) No Trademark License- This license does not grant you rights to use any contributors' name, logo, or trademarks. 24 | 25 | (B) If you bring a patent claim against any contributor over patents that you claim are infringed by the software, your patent license from such contributor to the software ends automatically. 26 | 27 | (C) If you distribute any portion of the software, you must retain all copyright, patent, trademark, and attribution notices that are present in the software. 28 | 29 | (D) If you distribute any portion of the software in source code form, you may do so only under this license by including a complete copy of this license with your distribution. If you distribute any portion of the software in compiled or object code form, you may only do so under a license that complies with this license. 30 | 31 | (E) The software is licensed "as-is." You bear the risk of using it. The contributors give no express warranties, guarantees or conditions. You may have additional consumer rights under your local laws which this license cannot change. To the extent permitted under your local laws, the contributors exclude the implied warranties of merchantability, fitness for a particular purpose and non-infringement. -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/PauseCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class PauseCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new PauseCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); // Commands disabled by default 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsPauseCommand() 19 | { 20 | var builtIns = PauseCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | } 23 | 24 | [Fact] 25 | public void Clone_CreatesIndependentCopy() 26 | { 27 | var original = new PauseCommand 28 | { 29 | Cmd = "pause", 30 | Args = "1000", 31 | Enabled = true 32 | }; 33 | 34 | var clone = (PauseCommand)original.Clone(null); 35 | 36 | Assert.Equal(original.Cmd, clone.Cmd); 37 | Assert.Equal(original.Args, clone.Args); 38 | Assert.Equal(original.Enabled, clone.Enabled); 39 | Assert.NotSame(original, clone); 40 | } 41 | 42 | [Fact] 43 | public void Execute_WhenDisabled_DoesNotThrow() 44 | { 45 | var cmd = new PauseCommand 46 | { 47 | Cmd = "pause", 48 | Args = "100", 49 | Enabled = false 50 | }; 51 | 52 | // Disabled commands return false without executing 53 | // This doesn't throw, just returns false 54 | var exception = Record.Exception(() => cmd.Execute()); 55 | Assert.Null(exception); 56 | } 57 | 58 | [Fact] 59 | public void Args_CanBeSet() 60 | { 61 | var cmd = new PauseCommand 62 | { 63 | Args = "500" 64 | }; 65 | 66 | Assert.Equal("500", cmd.Args); 67 | } 68 | 69 | [Fact] 70 | public void ToString_ReturnsFormattedString() 71 | { 72 | var cmd = new PauseCommand 73 | { 74 | Cmd = "pause", 75 | Args = "100" 76 | }; 77 | 78 | string result = cmd.ToString(); 79 | Assert.Contains("pause", result); 80 | Assert.Contains("100", result); 81 | } 82 | 83 | private class TestReply : Reply 84 | { 85 | public override void Write(string text) 86 | { 87 | // No-op for testing 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/ShutdownCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class ShutdownCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new ShutdownCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsShutdownCommands() 19 | { 20 | var builtIns = ShutdownCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | 23 | // Check for expected built-in shutdown commands 24 | Assert.Contains(builtIns, c => c.Cmd == "shutdown"); 25 | Assert.Contains(builtIns, c => c.Cmd == "shutdown-hybrid"); 26 | Assert.Contains(builtIns, c => c.Cmd == "restart"); 27 | Assert.Contains(builtIns, c => c.Cmd == "restart-g"); 28 | Assert.Contains(builtIns, c => c.Cmd == "standby"); 29 | Assert.Contains(builtIns, c => c.Cmd == "hibernate"); 30 | // logoff is also a built-in command 31 | Assert.Contains(builtIns, c => c.Cmd == "logoff"); 32 | } 33 | 34 | [Fact] 35 | public void Clone_CreatesIndependentCopy() 36 | { 37 | var original = new ShutdownCommand 38 | { 39 | Cmd = "shutdown", 40 | Args = "force", 41 | Enabled = true 42 | }; 43 | 44 | var clone = (ShutdownCommand)original.Clone(null); 45 | 46 | Assert.Equal(original.Cmd, clone.Cmd); 47 | Assert.Equal(original.Args, clone.Args); 48 | Assert.Equal(original.Enabled, clone.Enabled); 49 | Assert.NotSame(original, clone); 50 | } 51 | 52 | [Fact] 53 | public void Execute_WhenDisabled_ReturnsFalse() 54 | { 55 | var cmd = new ShutdownCommand 56 | { 57 | Cmd = "shutdown", 58 | Enabled = false 59 | }; 60 | 61 | bool result = cmd.Execute(); 62 | Assert.False(result); 63 | } 64 | 65 | [Fact] 66 | public void ToString_ReturnsFormattedString() 67 | { 68 | var cmd = new ShutdownCommand 69 | { 70 | Cmd = "shutdown", 71 | Type = "shutdown", 72 | TimeOut = 30 73 | }; 74 | 75 | string result = cmd.ToString(); 76 | Assert.Contains("shutdown", result); 77 | Assert.Contains("30", result); // TimeOut should be in the string 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {% seo %} 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 |
19 | 20 |

{{ site.title | default: site.github.repository_name }}

21 |

{{ site.description | default: site.github.project_tagline }}

22 | 23 | 30 |

By Tigger Kindel
31 | @ckindel on Twitter

32 |
33 | 34 |
35 | {{ content }} 36 |
37 | 38 | 42 |
43 | 44 | {% if site.google_analytics %} 45 | 53 | {% endif %} 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/MouseFlag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 5 | /// 6 | /// The set of MouseFlags for use in the Flags property of the structure. (See: http://msdn.microsoft.com/en-us/library/ms646273(VS.85).aspx) 7 | /// 8 | [Flags] 9 | public enum MouseFlag : uint // UInt32 10 | { 11 | /// 12 | /// Specifies that movement occurred. 13 | /// 14 | Move = 0x0001, 15 | 16 | /// 17 | /// Specifies that the left button was pressed. 18 | /// 19 | LeftDown = 0x0002, 20 | 21 | /// 22 | /// Specifies that the left button was released. 23 | /// 24 | LeftUp = 0x0004, 25 | 26 | /// 27 | /// Specifies that the right button was pressed. 28 | /// 29 | RightDown = 0x0008, 30 | 31 | /// 32 | /// Specifies that the right button was released. 33 | /// 34 | RightUp = 0x0010, 35 | 36 | /// 37 | /// Specifies that the middle button was pressed. 38 | /// 39 | MiddleDown = 0x0020, 40 | 41 | /// 42 | /// Specifies that the middle button was released. 43 | /// 44 | MiddleUp = 0x0040, 45 | 46 | /// 47 | /// Windows 2000/XP: Specifies that an X button was pressed. 48 | /// 49 | XDown = 0x0080, 50 | 51 | /// 52 | /// Windows 2000/XP: Specifies that an X button was released. 53 | /// 54 | XUp = 0x0100, 55 | 56 | /// 57 | /// Windows NT/2000/XP: Specifies that the wheel was moved, if the mouse has a wheel. The amount of movement is specified in mouseData. 58 | /// 59 | VerticalWheel = 0x0800, 60 | 61 | /// 62 | /// Specifies that the wheel was moved horizontally, if the mouse has a wheel. The amount of movement is specified in mouseData. Windows 2000/XP: Not supported. 63 | /// 64 | HorizontalWheel = 0x1000, 65 | 66 | /// 67 | /// Windows 2000/XP: Maps coordinates to the entire desktop. Must be used with MOUSEEVENTF_ABSOLUTE. 68 | /// 69 | VirtualDesk = 0x4000, 70 | 71 | /// 72 | /// Specifies that the dx and dy members contain normalized absolute coordinates. If the flag is not set, dxand dy contain relative data (the change in position since the last reported position). This flag can be set, or not set, regardless of what kind of mouse or other pointing device, if any, is connected to the system. For further information about relative mouse motion, see the following Remarks section. 73 | /// 74 | Absolute = 0x8000, 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /docs/telemetry.md: -------------------------------------------------------------------------------- 1 | # How MCE Controller Uses Telemetry 2 | 3 | MCE Controller includes functionality to collect Telemetry Data, which is a term to denote data about how the software is used or performing. Telemetry Data is collected through a “phone home” mechanism built into the software itself. 4 | 5 | The MCE Controller installer provides users an option to opt-in to share statistical data with the developers of the software. 6 | 7 | ![Installer Screenshot](telemetry_optin.png) 8 | 9 | ## Frequently Asked Questions 10 | 11 | ### What data is collected? 12 | 13 | First, here's what is explicitly NOT collected: 14 | 15 | * User names and IDs 16 | * Machine names and IDs 17 | * File and directory names 18 | * Network host names 19 | * User defined commands and other attributes 20 | 21 | [This search shows](https://github.com/tig/mcec/search?q=TrackEvent&unscoped_q=TrackEvent) all instances of `TrackEvent` in the source-code with comments describing the telemetry data being collected and the rationale. 22 | 23 | In addition, the source code has been annotated with comments that include `// TELEMETRY:` with a clear description of the information collected. For example: 24 | 25 | ```csharp 26 | // TELEMETRY: 27 | // what: application runtime 28 | // why: to understand how long the app stays running 29 | // how is PII protected: the time the app runs is not PII 30 | TrackEvent("Application Stopped", metrics: new Dictionary 31 | {{"runTime", _runTime.Elapsed.TotalMilliseconds}}); 32 | ``` 33 | 34 | ### What measures have been implemented to ensure no personally identifiable information is collected via telemetry? 35 | 36 | First, all telemetry data is regularly reviewed to identify potentially personally identifiable information. If there's any risk, a quick revision of the software is made to stop collecting such information. For example, an early build with telemetry enabled was incorrectly collecting the end user's machine name. This was fixed immediately with commit `046c5c3`. 37 | 38 | For any data structures which are collected 'en-mass`, a property attribute has been implemented such that only properties that are explicitly tagged for collection will be collected. This [search illustrates](https://github.com/tig/mcec/search?q=SafeForTelemetryAttribute&unscoped_q=SafeForTelemetryAttribute) this mechanism. 39 | 40 | For example, users can change the wakeup command used. While it's unlikely a user would ever set the wakeup command to be something with PII in it, they may. To protect against collecting such PII the `WakeupCommand` setting is NOT tagged with `SafeForTelemetryAttribute`: 41 | 42 | ```csharp 43 | // [SafeForTelemetryAttribute] 44 | // TELEMETRY: WakeupCommand can be set by user and thus may contain PII, so it is not collected 45 | public string WakeupCommand { get => wakeupCommand; set => wakeupCommand = value; } 46 | ``` 47 | 48 | ### What system is used to collect and analyzed telemetry? 49 | 50 | Microsoft Azure Application Insights. 51 | 52 | ### Can end-users review the telemetry information collected? 53 | 54 | Yes, anyone who would like to review that information should create a GitHub issue requesting access. -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright © Kindel, LLC - http://www.kindel.com 2 | // Published under the MIT License - Source on GitHub: https://github.com/tig/mcec 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using System.Windows.Forms; 7 | 8 | namespace MCEControl; 9 | 10 | internal static class Program { 11 | internal static string ConfigPath { 12 | get { 13 | // Get dir of mcecontrol.exe 14 | string path = AppDomain.CurrentDomain.BaseDirectory; 15 | string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86); 16 | string appdata = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); 17 | 18 | // is this in Program Files? 19 | if (path.Contains(programFiles)) { 20 | // We're running from the default install location. Use %appdata%. 21 | // strip % programfiles % 22 | path = $@"{appdata}\{path.Substring(programFiles.Length + 1)}"; 23 | } 24 | 25 | return path; 26 | } 27 | } 28 | 29 | /// 30 | /// The main entry point for the application. 31 | /// 32 | [STAThread] 33 | private static void Main() { 34 | Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); 35 | Application.EnableVisualStyles(); 36 | Application.SetCompatibleTextRenderingDefault(false); 37 | 38 | AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; 39 | TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; 40 | 41 | // Start logging 42 | Logger.Instance.LogFile = $@"{ConfigPath}MCEControl.log"; 43 | Logger.Instance.Log4.Debug( 44 | $"------ START: v{Application.ProductVersion} - OS: {Environment.OSVersion} on {(Environment.Is64BitProcess ? "x64" : "x86")} - .NET: {Environment.Version.ToString()} ------"); 45 | 46 | Application.Run(MainWindow.Instance); 47 | 48 | Logger.Instance.Log4.Debug($"------ END runtime: {TelemetryService.Instance.RunTime.Elapsed:g} ------"); 49 | } 50 | 51 | private static void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { 52 | Logger.DumpException(e.Exception); 53 | TelemetryService.Instance.TrackException(e.Exception); 54 | MessageBox.Show(@$"Unhandled Exception: {e.Exception.Message}\n\n" + 55 | @$"See log file for details: {Logger.Instance.LogFile}\n\nFor help, open an issue at github.com/tig/mcec", 56 | Application.ProductName); 57 | } 58 | 59 | private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { 60 | Exception ex = e.ExceptionObject as Exception; 61 | Logger.DumpException(ex); 62 | TelemetryService.Instance.TrackException(ex); 63 | MessageBox.Show(@$"Unhandled Exception: {ex.Message}\n\n" + 64 | @$"See log file for details: {Logger.Instance.LogFile}\n\nFor help, open an issue at github.com/tig/mcec", 65 | Application.ProductName); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Commands/SetForegroundWindowCmd.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2019 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source on GitHub: https://github.com/tig/mcec 8 | //------------------------------------------------------------------- 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Diagnostics; 12 | using System.Linq; 13 | using System.Text; 14 | using System.Xml.Serialization; 15 | using Microsoft.Win32.Security; 16 | 17 | namespace MCEControl { 18 | /// 19 | /// Summary description for SetForegroundWindowCommand. 20 | /// 21 | public class SetForegroundWindowCommand : Command { 22 | [XmlAttribute("classname")] 23 | public string ClassName { get => AppName; set => AppName = value; } 24 | [XmlAttribute("appname")] 25 | public string AppName { get; set; } 26 | 27 | public static new List BuiltInCommands { 28 | get => new List() { 29 | new SetForegroundWindowCommand() { Cmd = "activatecode", AppName ="code" }, 30 | new SetForegroundWindowCommand() { Cmd = "activatenotepad", AppName="Notepad" }, 31 | new SetForegroundWindowCommand() { Cmd = "activatemcec", AppName="MCEControl" }, 32 | }; 33 | } 34 | public SetForegroundWindowCommand() { 35 | } 36 | 37 | public SetForegroundWindowCommand(String className, String appName) { 38 | ClassName = className; 39 | AppName = appName; 40 | } 41 | 42 | public override string ToString() { 43 | return $"Cmd=\"{Cmd}\" AppName=\"{AppName}\""; 44 | } 45 | 46 | public override ICommand Clone(Reply reply) => base.Clone(reply, new SetForegroundWindowCommand(ClassName, AppName)); 47 | 48 | // ICommand:Execute 49 | public override bool Execute() { 50 | if (!base.Execute()) { 51 | return false; 52 | } 53 | 54 | try { 55 | if (!string.IsNullOrEmpty(AppName)) { 56 | Process[] procs = Process.GetProcessesByName(AppName); 57 | if (procs.Length > 0) { 58 | Process process = procs.Where(p => p.MainWindowHandle != IntPtr.Zero).FirstOrDefault(); 59 | 60 | Logger.Instance.Log4.Info($"{this.GetType().Name}: SetForegroundWindow({ClassName})"); 61 | Win32.SetForegroundWindow(process.MainWindowHandle); 62 | } 63 | else { 64 | Logger.Instance.Log4.Info($"{this.GetType().Name}: GetProcessByName for {ClassName} failed"); 65 | return false; 66 | } 67 | return true; 68 | } 69 | } 70 | catch (Exception e) { 71 | Logger.Instance.Log4.Error($"{this.GetType().Name}: Failed for {ClassName} with error: {e.Message}"); 72 | return false; 73 | } 74 | return true; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/WindowsInput/Native/KEYBDINPUT.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace WindowsInput.Native { 4 | #pragma warning disable 5 | /// 6 | /// The KEYBDINPUT structure contains information about a simulated keyboard event. (see: http://msdn.microsoft.com/en-us/library/ms646271(VS.85).aspx) 7 | /// Declared in Winuser.h, include Windows.h 8 | /// 9 | /// 10 | /// Windows 2000/XP: INPUT_KEYBOARD supports nonkeyboard-input methods—such as handwriting recognition or voice recognition—as if it were text input by using the KEYEVENTF_UNICODE flag. If KEYEVENTF_UNICODE is specified, SendInput sends a WM_KEYDOWN or WM_KEYUP message to the foreground thread's message queue with wParam equal to VK_PACKET. Once GetMessage or PeekMessage obtains this message, passing the message to TranslateMessage posts a WM_CHAR message with the Unicode character originally specified by wScan. This Unicode character will automatically be converted to the appropriate ANSI value if it is posted to an ANSI window. 11 | /// Windows 2000/XP: Set the KEYEVENTF_SCANCODE flag to define keyboard input in terms of the scan code. This is useful to simulate a physical keystroke regardless of which keyboard is currently being used. The virtual key value of a key may alter depending on the current keyboard layout or what other keys were pressed, but the scan code will always be the same. 12 | /// 13 | public struct KEYBDINPUT { 14 | /// 15 | /// Specifies a virtual-key code. The code must be a value in the range 1 to 254. The Winuser.h header file provides macro definitions (VK_*) for each value. If the dwFlags member specifies KEYEVENTF_UNICODE, wVk must be 0. 16 | /// 17 | public UInt16 KeyCode; 18 | 19 | /// 20 | /// Specifies a hardware scan code for the key. If dwFlags specifies KEYEVENTF_UNICODE, wScan specifies a Unicode character which is to be sent to the foreground application. 21 | /// 22 | public UInt16 Scan; 23 | 24 | /// 25 | /// Specifies various aspects of a keystroke. This member can be certain combinations of the following values. 26 | /// KEYEVENTF_EXTENDEDKEY - If specified, the scan code was preceded by a prefix byte that has the value 0xE0 (224). 27 | /// KEYEVENTF_KEYUP - If specified, the key is being released. If not specified, the key is being pressed. 28 | /// KEYEVENTF_SCANCODE - If specified, wScan identifies the key and wVk is ignored. 29 | /// KEYEVENTF_UNICODE - Windows 2000/XP: If specified, the system synthesizes a VK_PACKET keystroke. The wVk parameter must be zero. This flag can only be combined with the KEYEVENTF_KEYUP flag. For more information, see the Remarks section. 30 | /// 31 | public UInt32 Flags; 32 | 33 | /// 34 | /// Time stamp for the event, in milliseconds. If this parameter is zero, the system will provide its own time stamp. 35 | /// 36 | public UInt32 Time; 37 | 38 | /// 39 | /// Specifies an additional value associated with the keystroke. Use the GetMessageExtraInfo function to obtain this information. 40 | /// 41 | public IntPtr ExtraInfo; 42 | } 43 | #pragma warning restore 649 44 | } 45 | -------------------------------------------------------------------------------- /src/Win32/SecurityAttributesMarshaler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace Microsoft.Win32.Security { 5 | #pragma warning disable 6 | using Win32Structs; 7 | 8 | /// 9 | /// Custom marshaler for SecurityAttributes class. 10 | /// Marshals from/to the Win32 SECURITY_ATTRIBUTES struct. 11 | /// 12 | public class SecurityAttributesMarshaler : ICustomMarshaler { 13 | /// 14 | /// Required by the runtime marshaler. We return a new instance 15 | /// at each call, because our marshaler instance has some internal state. 16 | /// 17 | /// 18 | /// Our marshaler for the SecurityAttributes class 19 | // 20 | public static ICustomMarshaler GetInstance(string data) { 21 | return new SecurityAttributesMarshaler(); 22 | } 23 | 24 | // The original managed object. Needed for GC liveness and in/out support. 25 | private SecurityAttributes _wrapper; 26 | 27 | #region ICustomMarshaler Members 28 | 29 | public object MarshalNativeToManaged(System.IntPtr pNativeData) { 30 | // If native ptr is NULL, return null 31 | if (pNativeData == IntPtr.Zero) 32 | return null; 33 | 34 | // If we have an existing wrapper, re-use it. 35 | // This allows us to properly support the In/Out semantics. 36 | if (_wrapper != null) { 37 | _wrapper.Parse(pNativeData); 38 | return _wrapper; 39 | } 40 | 41 | // Create a new wrapper and parse the native struct 42 | _wrapper = new SecurityAttributes(); 43 | _wrapper.Parse(pNativeData); 44 | return _wrapper; 45 | } 46 | 47 | public System.IntPtr MarshalManagedToNative(object ManagedObj) { 48 | SecurityAttributes saIn = (SecurityAttributes)ManagedObj; 49 | if (saIn == null) 50 | return IntPtr.Zero; 51 | 52 | // Keep a reference to the wrapper in case "MarshalNativeToManaged" 53 | // is called later (In/Out marshaling) 54 | this._wrapper = saIn; 55 | 56 | // Allocate unmanaged memeory to store the SECURITY_ATTRIBUTES struct 57 | SECURITY_ATTRIBUTES saOut = saIn.GetSECURITY_ATTRIBUTES(); 58 | IntPtr pSaIn = Win32.AllocGlobal(SECURITY_ATTRIBUTES.SizeOf); 59 | Marshal.StructureToPtr(saOut, pSaIn, false); 60 | return pSaIn; 61 | } 62 | public void CleanUpManagedData(object ManagedObj) { 63 | } 64 | public int GetNativeDataSize() { 65 | // The return value is currently ignored. 66 | return -1; 67 | } 68 | public void CleanUpNativeData(System.IntPtr pNativeData) { 69 | // We need to preserve/restore the last error here, because 70 | // we're called before the P/Invoke call returns to the caller. 71 | // If the caller of the P/Invoke call needs to access GetLastError, 72 | // it would always be "0" because Win32.FreeGlobal resets it to "0". 73 | UInt32 lastErr = Win32.GetLastError(); 74 | Win32.FreeGlobal(pNativeData); 75 | Win32.SetLastError(lastErr); 76 | } 77 | 78 | #endregion 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2019 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source on GitHub: https://github.com/tig/mcec 8 | //------------------------------------------------------------------- 9 | using System.Reflection; 10 | using System.Resources; 11 | using System.Runtime.InteropServices; 12 | 13 | // 14 | // General Information about an assembly is controlled through the following 15 | // set of attributes. Change these attribute values to modify the information 16 | // associated with an assembly. 17 | // 18 | [assembly: AssemblyTitle("MCE Controller")] 19 | [assembly: AssemblyDescription("Control a Windows PC over the network")] 20 | [assembly: AssemblyConfiguration("")] 21 | [assembly: AssemblyCompany("Kindel Systems")] // DO NOT CHANGE THIS - used for settings file paths 22 | [assembly: AssemblyProduct("MCE Controller")] 23 | // AssemblyCopyright is generated by AssemblyFileVersion.tt 24 | //[assembly: AssemblyCopyright("Copyright © 2017 Kindel Systems, LLC")] 25 | [assembly: AssemblyTrademark("MCE Controller")] 26 | [assembly: AssemblyCulture("")] 27 | [assembly: NeutralResourcesLanguage("en")] 28 | 29 | // 30 | // Version information for an assembly consists of the following four values: 31 | // 32 | // Major Version 33 | // Minor Version 34 | // Build Number 35 | // Revision 36 | // 37 | // You can specify all the values or you can default the Revision and Build Numbers 38 | // by using the '*' as shown below: 39 | 40 | // Now generated via AssemblyFileVersion.tt 41 | // [assembly: AssemblyVersion("1.9.0.*")] 42 | 43 | // 44 | // In order to sign your assembly you must specify a key to use. Refer to the 45 | // Microsoft .NET Framework documentation for more information on assembly signing. 46 | // 47 | // Use the attributes below to control which key is used for signing. 48 | // 49 | // Notes: 50 | // (*) If no key is specified, the assembly is not signed. 51 | // (*) KeyName refers to a key that has been installed in the Crypto Service 52 | // Provider (CSP) on your machine. KeyFile refers to a file which contains 53 | // a key. 54 | // (*) If the KeyFile and the KeyName values are both specified, the 55 | // following processing occurs: 56 | // (1) If the KeyName can be found in the CSP, that key is used. 57 | // (2) If the KeyName does not exist and the KeyFile does exist, the key 58 | // in the KeyFile is installed into the CSP and used. 59 | // (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility. 60 | // When specifying the KeyFile, the location of the KeyFile should be 61 | // relative to the project output directory which is 62 | // %Project Directory%\obj\. For example, if your KeyFile is 63 | // located in the project directory, you would specify the AssemblyKeyFile 64 | // attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] 65 | // (*) Delay Signing is an advanced option - see the Microsoft .NET Framework 66 | // documentation for more information on this. 67 | // 68 | [assembly: AssemblyDelaySign(false)] 69 | [assembly: AssemblyKeyFile("")] 70 | [assembly: AssemblyKeyName("")] 71 | [assembly: ComVisibleAttribute(false)] 72 | //[assembly: log4net.Config.XmlConfigurator(Watch = true)] 73 | -------------------------------------------------------------------------------- /src/app.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 52 | 53 | 54 | 55 | true 56 | PerMonitorV2 57 | 58 | 59 | 60 | 61 | 75 | 76 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/AppSettingsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using System.Collections.Generic; 4 | using System.Text; 5 | using Xunit; 6 | using System.IO; 7 | 8 | namespace MCEControl.xUnit 9 | { 10 | public class AppSettingsTests 11 | { 12 | [Fact] 13 | public void GetSettingsPath_inProgramFiles_Test() 14 | { 15 | // If we're running within Program Files, use %AppData% 16 | string startupPath = $@"{Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles)}\Kindel Systems\MCE Controller"; 17 | // should be "%appdata%\Kindel Systems\MCE Controller" 18 | string settingsPath = $@"{Application.UserAppDataPath.Substring(0, Application.UserAppDataPath.Length - (Application.ProductVersion.Length + 1))}"; 19 | Assert.True(AppSettings.GetSettingsPath(startupPath).CompareTo(settingsPath) == 0); 20 | } 21 | 22 | [Fact] 23 | public void GetSettingsPath_standalone_Test() 24 | { 25 | // If we're running elsewhere (not in Program Files), use current dir 26 | string startupPath = $@"."; 27 | // should be ame as Application.Startpath 28 | string settingsPath = $@"."; 29 | Assert.True(AppSettings.GetSettingsPath(startupPath).CompareTo(settingsPath) == 0); 30 | } 31 | 32 | /// 33 | /// Tests that SafeForTelemetryAttribute is working 34 | /// 35 | [Fact] 36 | public void GetTelemetryDictionary_Test() 37 | { 38 | AppSettings appSettings = new AppSettings(); 39 | IDictionary? dict = appSettings.GetTelemetryDictionary(); 40 | 41 | Assert.True(dict.ContainsKey("AutoStart")); 42 | 43 | Assert.False(dict.ContainsKey("WakeupCommand")); 44 | } 45 | 46 | [Fact] 47 | public void DeserializeExists_Test() 48 | { 49 | string tempPath = Path.GetTempPath(); 50 | string? settingsPath = AppSettings.GetSettingsPath(tempPath); 51 | 52 | AppSettings settings = new AppSettings(); 53 | settings.AutoStart = true; // default is false, so this is a good test 54 | 55 | string settingsFullPath = $@"{tempPath}test.settings.xml"; 56 | settings.Serialize(settingsFullPath); 57 | 58 | string text = System.IO.File.ReadAllText(settingsFullPath); 59 | 60 | Assert.False(string.IsNullOrEmpty(text)); 61 | 62 | AppSettings? readSettings = AppSettings.Deserialize(settingsFullPath); 63 | 64 | Assert.True(readSettings.AutoStart == true); 65 | } 66 | 67 | [Fact] 68 | public void DeserializeNotExists_Test() 69 | { 70 | // create a temp file and then delete it 71 | string settingsFullPath = Path.GetTempFileName(); 72 | File.Delete(settingsFullPath); 73 | 74 | // Verify it was deleted 75 | Assert.False(File.Exists(settingsFullPath)); 76 | 77 | AppSettings? readSettings = AppSettings.Deserialize(settingsFullPath); 78 | Assert.True(File.Exists(settingsFullPath)); 79 | 80 | // This proves default settings were saved 81 | Assert.Equal("localhost", readSettings.ClientHost); 82 | 83 | File.Delete(settingsFullPath); 84 | Assert.False(File.Exists(settingsFullPath)); 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/WindowsInput/InputSimulator.cs: -------------------------------------------------------------------------------- 1 | namespace WindowsInput { 2 | /// 3 | /// Implements the interface to simulate Keyboard and Mouse input and provide the state of those input devices. 4 | /// 5 | public class InputSimulator : IInputSimulator { 6 | /// 7 | /// The instance to use for simulating keyboard input. 8 | /// 9 | private readonly IKeyboardSimulator _keyboardSimulator; 10 | 11 | /// 12 | /// The instance to use for simulating mouse input. 13 | /// 14 | private readonly IMouseSimulator _mouseSimulator; 15 | 16 | /// 17 | /// The instance to use for interpreting the state of the input devices. 18 | /// 19 | private readonly IInputDeviceStateAdaptor _inputDeviceState; 20 | 21 | /// 22 | /// Initializes a new instance of the class using the specified , and instances. 23 | /// 24 | /// The instance to use for simulating keyboard input. 25 | /// The instance to use for simulating mouse input. 26 | /// The instance to use for interpreting the state of input devices. 27 | public InputSimulator(IKeyboardSimulator keyboardSimulator, IMouseSimulator mouseSimulator, IInputDeviceStateAdaptor inputDeviceStateAdaptor) { 28 | _keyboardSimulator = keyboardSimulator; 29 | _mouseSimulator = mouseSimulator; 30 | _inputDeviceState = inputDeviceStateAdaptor; 31 | } 32 | 33 | /// 34 | /// Initializes a new instance of the class using the default , and instances. 35 | /// 36 | public InputSimulator() { 37 | _keyboardSimulator = new KeyboardSimulator(); 38 | _mouseSimulator = new MouseSimulator(); 39 | _inputDeviceState = new WindowsInputDeviceStateAdaptor(); 40 | } 41 | 42 | /// 43 | /// Gets the instance for simulating Keyboard input. 44 | /// 45 | /// The instance. 46 | public IKeyboardSimulator Keyboard { 47 | get { return _keyboardSimulator; } 48 | } 49 | 50 | /// 51 | /// Gets the instance for simulating Mouse input. 52 | /// 53 | /// The instance. 54 | public IMouseSimulator Mouse { 55 | get { return _mouseSimulator; } 56 | } 57 | 58 | /// 59 | /// Gets the instance for determining the state of the various input devices. 60 | /// 61 | /// The instance. 62 | public IInputDeviceStateAdaptor InputDeviceState { 63 | get { return _inputDeviceState; } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/Helpers/TextBoxExt.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | namespace MCEControl { 5 | /// 6 | /// Enforce MaxLength property even when setting text programmatically 7 | /// https://stackoverflow.com/questions/10011508/textbox-maximum-amount-of-characters-its-not-maxlength 8 | /// 9 | public class TextBoxExt : TextBox { 10 | new public void AppendText(string text) { 11 | if (this.Text.Length == this.MaxLength) { 12 | return; 13 | } 14 | else if (text != null && (this.Text.Length + text.Length > this.MaxLength)) { 15 | this.Clear(); 16 | base.AppendText(text); 17 | //base.AppendText(text.Substring(0, (this.MaxLength - this.Text.Length))); 18 | } 19 | else { 20 | base.AppendText(text); 21 | } 22 | } 23 | 24 | public override string Text { 25 | get { 26 | return base.Text; 27 | } 28 | set { 29 | if (!string.IsNullOrEmpty(value) && value.Length > this.MaxLength) { 30 | base.Text = value.Substring(0, this.MaxLength); 31 | } 32 | else { 33 | base.Text = value; 34 | } 35 | } 36 | } 37 | 38 | // Also: Clearing top X lines with high performance 39 | public void ClearTopLines(int count) { 40 | if (count <= 0) { 41 | return; 42 | } 43 | else if (!this.Multiline) { 44 | this.Clear(); 45 | return; 46 | } 47 | 48 | string txt = this.Text; 49 | int cursor = 0, ixOf = 0, brkLength = 0, brkCount = 0; 50 | 51 | while (brkCount < count) { 52 | ixOf = txt.IndexOfBreak(cursor, out brkLength); 53 | if (ixOf < 0) { 54 | this.Clear(); 55 | return; 56 | } 57 | cursor = ixOf + brkLength; 58 | brkCount++; 59 | } 60 | this.Text = txt.Substring(cursor); 61 | } 62 | } 63 | 64 | public static class StringExt { 65 | public static int IndexOfBreak(this string str, out int length) { 66 | return IndexOfBreak(str, 0, out length); 67 | } 68 | 69 | public static int IndexOfBreak(this string str, int startIndex, out int length) { 70 | if (string.IsNullOrEmpty(str)) { 71 | length = 0; 72 | return -1; 73 | } 74 | int ub = str.Length - 1; 75 | int intchr; 76 | if (startIndex > ub) { 77 | throw new ArgumentOutOfRangeException(); 78 | } 79 | for (int i = startIndex; i <= ub; i++) { 80 | intchr = str[i]; 81 | if (intchr == 0x0D) { 82 | if (i < ub && str[i + 1] == 0x0A) { 83 | length = 2; 84 | } 85 | else { 86 | length = 1; 87 | } 88 | return i; 89 | } 90 | else if (intchr == 0x0A) { 91 | length = 1; 92 | return i; 93 | } 94 | } 95 | length = 0; 96 | return -1; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/SendMessageCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class SendMessageCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_WithParameters_SetsProperties() 11 | { 12 | var cmd = new SendMessageCommand("WindowClass", "WindowTitle", 123, 456, 789); 13 | 14 | Assert.Equal("WindowClass", cmd.ClassName); 15 | Assert.Equal("WindowTitle", cmd.WindowName); 16 | Assert.Equal(123, cmd.Msg); 17 | Assert.Equal(456, cmd.WParam); 18 | Assert.Equal(789, cmd.LParam); 19 | } 20 | 21 | [Fact] 22 | public void Constructor_Default_InitializesProperties() 23 | { 24 | var cmd = new SendMessageCommand(); 25 | Assert.NotNull(cmd); 26 | Assert.False(cmd.Enabled); 27 | } 28 | 29 | [Fact] 30 | public void BuiltInCommands_ContainsSendMessageCommands() 31 | { 32 | var builtIns = SendMessageCommand.BuiltInCommands; 33 | Assert.NotEmpty(builtIns); 34 | } 35 | 36 | [Fact] 37 | public void Clone_CreatesIndependentCopy() 38 | { 39 | var original = new SendMessageCommand("Class1", "Window1", 100, 200, 300) 40 | { 41 | Cmd = "sendmsg", 42 | Enabled = true 43 | }; 44 | 45 | var clone = (SendMessageCommand)original.Clone(null); 46 | 47 | Assert.Equal(original.ClassName, clone.ClassName); 48 | Assert.Equal(original.WindowName, clone.WindowName); 49 | Assert.Equal(original.Msg, clone.Msg); 50 | Assert.Equal(original.WParam, clone.WParam); 51 | Assert.Equal(original.LParam, clone.LParam); 52 | Assert.Equal(original.Enabled, clone.Enabled); 53 | Assert.NotSame(original, clone); 54 | } 55 | 56 | [Fact] 57 | public void Execute_WhenDisabled_ReturnsFalse() 58 | { 59 | var cmd = new SendMessageCommand("Class", "Window", 0, 0, 0) 60 | { 61 | Enabled = false 62 | }; 63 | 64 | bool result = cmd.Execute(); 65 | Assert.False(result); 66 | } 67 | 68 | [Fact] 69 | public void ToString_ReturnsFormattedString() 70 | { 71 | var cmd = new SendMessageCommand("TestClass", "TestWindow", 100, 200, 300) 72 | { 73 | Cmd = "test" 74 | }; 75 | 76 | string result = cmd.ToString(); 77 | Assert.Contains("test", result); 78 | Assert.Contains("TestClass", result); 79 | Assert.Contains("TestWindow", result); 80 | Assert.Contains("100", result); 81 | Assert.Contains("200", result); 82 | Assert.Contains("300", result); 83 | } 84 | 85 | [Fact] 86 | public void Properties_CanBeSet() 87 | { 88 | var cmd = new SendMessageCommand 89 | { 90 | ClassName = "NewClass", 91 | WindowName = "NewWindow", 92 | Msg = 555, 93 | WParam = 666, 94 | LParam = 777 95 | }; 96 | 97 | Assert.Equal("NewClass", cmd.ClassName); 98 | Assert.Equal("NewWindow", cmd.WindowName); 99 | Assert.Equal(555, cmd.Msg); 100 | Assert.Equal(666, cmd.WParam); 101 | Assert.Equal(777, cmd.LParam); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/WindowsInput/IKeyboardSimulator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using WindowsInput.Native; 3 | 4 | namespace WindowsInput { 5 | /// 6 | /// The service contract for a keyboard simulator for the Windows platform. 7 | /// 8 | public interface IKeyboardSimulator { 9 | /// 10 | /// Simulates the key down gesture for the specified key. 11 | /// 12 | /// The for the key. 13 | void KeyDown(VirtualKeyCode keyCode); 14 | 15 | /// 16 | /// Simulates the key press gesture for the specified key. 17 | /// 18 | /// The for the key. 19 | void KeyPress(VirtualKeyCode keyCode); 20 | 21 | /// 22 | /// Simulates the key up gesture for the specified key. 23 | /// 24 | /// The for the key. 25 | void KeyUp(VirtualKeyCode keyCode); 26 | 27 | /// 28 | /// Simulates a modified keystroke where there are multiple modifiers and multiple keys like CTRL-ALT-K-C where CTRL and ALT are the modifierKeys and K and C are the keys. 29 | /// The flow is Modifiers KeyDown in order, Keys Press in order, Modifiers KeyUp in reverse order. 30 | /// 31 | /// The list of s for the modifier keys. 32 | /// The list of s for the keys to simulate. 33 | void ModifiedKeyStroke(IEnumerable modifierKeyCodes, IEnumerable keyCodes); 34 | 35 | /// 36 | /// Simulates a modified keystroke where there are multiple modifiers and one key like CTRL-ALT-C where CTRL and ALT are the modifierKeys and C is the key. 37 | /// The flow is Modifiers KeyDown in order, Key Press, Modifiers KeyUp in reverse order. 38 | /// 39 | /// The list of s for the modifier keys. 40 | /// The for the key. 41 | void ModifiedKeyStroke(IEnumerable modifierKeyCodes, VirtualKeyCode keyCode); 42 | 43 | /// 44 | /// Simulates a modified keystroke where there is one modifier and multiple keys like CTRL-K-C where CTRL is the modifierKey and K and C are the keys. 45 | /// The flow is Modifier KeyDown, Keys Press in order, Modifier KeyUp. 46 | /// 47 | /// The for the modifier key. 48 | /// The list of s for the keys to simulate. 49 | void ModifiedKeyStroke(VirtualKeyCode modifierKey, IEnumerable keyCodes); 50 | 51 | /// 52 | /// Simulates a simple modified keystroke like CTRL-C where CTRL is the modifierKey and C is the key. 53 | /// The flow is Modifier KeyDown, Key Press, Modifier KeyUp. 54 | /// 55 | /// The for the modifier key. 56 | /// The for the key. 57 | void ModifiedKeyStroke(VirtualKeyCode modifierKeyCode, VirtualKeyCode keyCode); 58 | 59 | /// 60 | /// Simulates uninterrupted text entry via the keyboard. 61 | /// 62 | /// The text to be simulated. 63 | void TextEntry(string text); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/.github/workflows/README.md: -------------------------------------------------------------------------------- 1 | # MCEControl CI/CD 2 | 3 | This directory contains GitHub Actions workflows for continuous integration and deployment. 4 | 5 | ## Workflows 6 | 7 | ### CI Workflow (`ci.yml`) 8 | 9 | **Triggers:** 10 | - Push to `develop` branch 11 | - Pull requests targeting `develop` branch 12 | 13 | **Jobs:** 14 | - **Build and Test**: Builds the solution and runs all xUnit tests 15 | - Runs on `windows-latest` (required for Windows Forms and Windows-specific APIs) 16 | - Uses .NET 8.0 17 | - Generates code coverage reports using coverlet 18 | - Publishes test results and coverage reports as artifacts 19 | - Adds coverage summary to PR comments 20 | 21 | **Artifacts:** 22 | - Test results (retained for 30 days) 23 | - Code coverage reports (retained for 30 days) 24 | 25 | ## Status Badge 26 | 27 | Add this badge to your README.md to show the CI status: 28 | 29 | ```markdown 30 | [![CI](https://github.com/kindel/mcec/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/kindel/mcec/actions/workflows/ci.yml) 31 | ``` 32 | 33 | ## Configuration 34 | 35 | ### Environment Variables 36 | 37 | The workflow uses these environment variables (configured in `ci.yml`): 38 | - `DOTNET_VERSION`: .NET SDK version (currently 8.0.x) 39 | - `SOLUTION_PATH`: Path to the solution file (src/MCEControl.sln) 40 | - `CONFIGURATION`: Build configuration (Release) 41 | 42 | ### Code Coverage 43 | 44 | The workflow generates code coverage using: 45 | - **coverlet.collector**: Collects coverage during test execution 46 | - **ReportGenerator**: Generates HTML and Cobertura reports 47 | 48 | Coverage reports are: 49 | - Uploaded as workflow artifacts 50 | - Added to PR summaries (when run on PRs) 51 | - Checked against a configurable threshold 52 | 53 | To adjust the coverage threshold, edit the `$threshold` variable in the "Check code coverage threshold" step. 54 | 55 | ## Local Testing 56 | 57 | To run the same tests locally: 58 | 59 | ```bash 60 | # Restore dependencies 61 | dotnet restore src/MCEControl.sln 62 | 63 | # Build 64 | dotnet build src/MCEControl.sln --configuration Release 65 | 66 | # Run tests with coverage 67 | dotnet test src/MCEControl.sln --configuration Release --collect:"XPlat Code Coverage" 68 | ``` 69 | 70 | ## Extending the Workflow 71 | 72 | ### Adding More Branches 73 | 74 | To trigger CI on additional branches, edit the `on` section in `ci.yml`: 75 | 76 | ```yaml 77 | on: 78 | push: 79 | branches: 80 | - develop 81 | - main 82 | - 'feature/**' 83 | pull_request: 84 | branches: 85 | - develop 86 | - main 87 | ``` 88 | 89 | ### Adding Release Workflow 90 | 91 | Consider creating a separate `release.yml` workflow for: 92 | - Building release artifacts 93 | - Creating installers/packages 94 | - Publishing to GitHub Releases 95 | - Deploying documentation 96 | 97 | ### Adding Linting/Code Analysis 98 | 99 | You can add additional jobs for: 100 | - Code style checking (dotnet format) 101 | - Static analysis (SonarCloud, CodeQL) 102 | - Security scanning 103 | - Dependency vulnerability checking 104 | 105 | ## Troubleshooting 106 | 107 | ### Windows-Only Tests 108 | 109 | Some tests may require Windows-specific features. The workflow uses `windows-latest` runners to ensure compatibility. 110 | 111 | ### Test Failures 112 | 113 | If tests fail: 114 | 1. Check the test results artifact for detailed logs 115 | 2. Review the test output in the workflow logs 116 | 3. Run tests locally with the same configuration 117 | 118 | ### Coverage Report Issues 119 | 120 | If coverage reports aren't generated: 121 | 1. Ensure `coverlet.collector` is installed in the test project 122 | 2. Check that tests are actually running 123 | 3. Verify the coverage file paths in the workflow 124 | -------------------------------------------------------------------------------- /src/.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | pull_request: 8 | branches: 9 | - develop 10 | 11 | env: 12 | DOTNET_VERSION: '8.0.x' 13 | SOLUTION_PATH: 'src/MCEControl.sln' 14 | CONFIGURATION: 'Release' 15 | 16 | jobs: 17 | build-and-test: 18 | name: Build and Test 19 | runs-on: windows-latest 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 # Fetch all history for better blame information 26 | 27 | - name: Setup .NET 28 | uses: actions/setup-dotnet@v4 29 | with: 30 | dotnet-version: ${{ env.DOTNET_VERSION }} 31 | 32 | - name: Display .NET version 33 | run: dotnet --version 34 | 35 | - name: Restore dependencies 36 | run: dotnet restore ${{ env.SOLUTION_PATH }} 37 | 38 | - name: Build solution 39 | run: dotnet build ${{ env.SOLUTION_PATH }} --configuration ${{ env.CONFIGURATION }} --no-restore 40 | 41 | - name: Run tests 42 | run: dotnet test ${{ env.SOLUTION_PATH }} --configuration ${{ env.CONFIGURATION }} --no-build --verbosity normal --logger "trx;LogFileName=test-results.trx" --collect:"XPlat Code Coverage" --results-directory ./TestResults 43 | 44 | - name: Publish test results 45 | uses: EnricoMi/publish-unit-test-result-action/windows@v2 46 | if: always() 47 | with: 48 | files: | 49 | TestResults/**/*.trx 50 | check_name: Test Results 51 | comment_mode: off 52 | 53 | - name: Generate code coverage report 54 | if: always() 55 | uses: danielpalme/ReportGenerator-GitHub-Action@5.3.11 56 | with: 57 | reports: 'TestResults/**/coverage.cobertura.xml' 58 | targetdir: 'coveragereport' 59 | reporttypes: 'HtmlInline;Cobertura;MarkdownSummaryGithub' 60 | verbosity: 'Info' 61 | 62 | - name: Add coverage summary to PR 63 | if: github.event_name == 'pull_request' && always() 64 | run: | 65 | if (Test-Path "coveragereport/SummaryGithub.md") { 66 | Get-Content "coveragereport/SummaryGithub.md" >> $env:GITHUB_STEP_SUMMARY 67 | } 68 | shell: pwsh 69 | 70 | - name: Upload coverage report 71 | if: always() 72 | uses: actions/upload-artifact@v4 73 | with: 74 | name: coverage-report 75 | path: coveragereport/ 76 | retention-days: 30 77 | 78 | - name: Upload test results 79 | if: always() 80 | uses: actions/upload-artifact@v4 81 | with: 82 | name: test-results 83 | path: TestResults/ 84 | retention-days: 30 85 | 86 | - name: Check code coverage threshold 87 | if: always() 88 | run: | 89 | if (Test-Path "coveragereport/Cobertura.xml") { 90 | [xml]$coverage = Get-Content "coveragereport/Cobertura.xml" 91 | $lineRate = [double]$coverage.coverage.'line-rate' 92 | $branchRate = [double]$coverage.coverage.'branch-rate' 93 | $lineCoverage = [math]::Round($lineRate * 100, 2) 94 | $branchCoverage = [math]::Round($branchRate * 100, 2) 95 | 96 | Write-Host "Line Coverage: $lineCoverage%" 97 | Write-Host "Branch Coverage: $branchCoverage%" 98 | 99 | # Set a reasonable threshold (adjust as needed) 100 | $threshold = 0 101 | if ($lineCoverage -lt $threshold) { 102 | Write-Host "::warning::Line coverage ($lineCoverage%) is below threshold ($threshold%)" 103 | } 104 | } 105 | shell: pwsh 106 | -------------------------------------------------------------------------------- /docs/_sass/rouge-github.scss: -------------------------------------------------------------------------------- 1 | .highlight table td { padding: 5px; } 2 | .highlight table pre { margin: 0; } 3 | .highlight .cm { 4 | color: #999988; 5 | font-style: italic; 6 | } 7 | .highlight .cp { 8 | color: #999999; 9 | font-weight: bold; 10 | } 11 | .highlight .c1 { 12 | color: #999988; 13 | font-style: italic; 14 | } 15 | .highlight .cs { 16 | color: #999999; 17 | font-weight: bold; 18 | font-style: italic; 19 | } 20 | .highlight .c, .highlight .cd { 21 | color: #999988; 22 | font-style: italic; 23 | } 24 | .highlight .err { 25 | color: #a61717; 26 | background-color: #e3d2d2; 27 | } 28 | .highlight .gd { 29 | color: #000000; 30 | background-color: #ffdddd; 31 | } 32 | .highlight .ge { 33 | color: #000000; 34 | font-style: italic; 35 | } 36 | .highlight .gr { 37 | color: #aa0000; 38 | } 39 | .highlight .gh { 40 | color: #999999; 41 | } 42 | .highlight .gi { 43 | color: #000000; 44 | background-color: #ddffdd; 45 | } 46 | .highlight .go { 47 | color: #888888; 48 | } 49 | .highlight .gp { 50 | color: #555555; 51 | } 52 | .highlight .gs { 53 | font-weight: bold; 54 | } 55 | .highlight .gu { 56 | color: #aaaaaa; 57 | } 58 | .highlight .gt { 59 | color: #aa0000; 60 | } 61 | .highlight .kc { 62 | color: #000000; 63 | font-weight: bold; 64 | } 65 | .highlight .kd { 66 | color: #000000; 67 | font-weight: bold; 68 | } 69 | .highlight .kn { 70 | color: #000000; 71 | font-weight: bold; 72 | } 73 | .highlight .kp { 74 | color: #000000; 75 | font-weight: bold; 76 | } 77 | .highlight .kr { 78 | color: #000000; 79 | font-weight: bold; 80 | } 81 | .highlight .kt { 82 | color: #445588; 83 | font-weight: bold; 84 | } 85 | .highlight .k, .highlight .kv { 86 | color: #000000; 87 | font-weight: bold; 88 | } 89 | .highlight .mf { 90 | color: #009999; 91 | } 92 | .highlight .mh { 93 | color: #009999; 94 | } 95 | .highlight .il { 96 | color: #009999; 97 | } 98 | .highlight .mi { 99 | color: #009999; 100 | } 101 | .highlight .mo { 102 | color: #009999; 103 | } 104 | .highlight .m, .highlight .mb, .highlight .mx { 105 | color: #009999; 106 | } 107 | .highlight .sb { 108 | color: #d14; 109 | } 110 | .highlight .sc { 111 | color: #d14; 112 | } 113 | .highlight .sd { 114 | color: #d14; 115 | } 116 | .highlight .s2 { 117 | color: #d14; 118 | } 119 | .highlight .se { 120 | color: #d14; 121 | } 122 | .highlight .sh { 123 | color: #d14; 124 | } 125 | .highlight .si { 126 | color: #d14; 127 | } 128 | .highlight .sx { 129 | color: #d14; 130 | } 131 | .highlight .sr { 132 | color: #009926; 133 | } 134 | .highlight .s1 { 135 | color: #d14; 136 | } 137 | .highlight .ss { 138 | color: #990073; 139 | } 140 | .highlight .s { 141 | color: #d14; 142 | } 143 | .highlight .na { 144 | color: #008080; 145 | } 146 | .highlight .bp { 147 | color: #999999; 148 | } 149 | .highlight .nb { 150 | color: #0086B3; 151 | } 152 | .highlight .nc { 153 | color: #445588; 154 | font-weight: bold; 155 | } 156 | .highlight .no { 157 | color: #008080; 158 | } 159 | .highlight .nd { 160 | color: #3c5d5d; 161 | font-weight: bold; 162 | } 163 | .highlight .ni { 164 | color: #800080; 165 | } 166 | .highlight .ne { 167 | color: #990000; 168 | font-weight: bold; 169 | } 170 | .highlight .nf { 171 | color: #990000; 172 | font-weight: bold; 173 | } 174 | .highlight .nl { 175 | color: #990000; 176 | font-weight: bold; 177 | } 178 | .highlight .nn { 179 | color: #555555; 180 | } 181 | .highlight .nt { 182 | color: #000080; 183 | } 184 | .highlight .vc { 185 | color: #008080; 186 | } 187 | .highlight .vg { 188 | color: #008080; 189 | } 190 | .highlight .vi { 191 | color: #008080; 192 | } 193 | .highlight .nv { 194 | color: #008080; 195 | } 196 | .highlight .ow { 197 | color: #000000; 198 | font-weight: bold; 199 | } 200 | .highlight .o { 201 | color: #000000; 202 | font-weight: bold; 203 | } 204 | .highlight .w { 205 | color: #bbbbbb; 206 | } 207 | .highlight { 208 | background-color: #f8f8f8; 209 | } 210 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/SerializedCommandsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Xunit; 4 | using MCEControl; 5 | 6 | namespace MCEControl.xUnit.Commands 7 | { 8 | public class SerializedCommandsTests 9 | { 10 | [Fact] 11 | public void SaveCommands_CreatesFile() 12 | { 13 | string tempFile = Path.GetTempFileName(); 14 | File.Delete(tempFile); 15 | 16 | var commands = new SerializedCommands 17 | { 18 | commandArray = new Command[] 19 | { 20 | new PauseCommand() { Cmd = "pause100", Args = "100", Enabled = true } 21 | } 22 | }; 23 | 24 | SerializedCommands.SaveCommands(tempFile, commands, "1.0.0.0"); 25 | 26 | Assert.True(File.Exists(tempFile)); 27 | File.Delete(tempFile); 28 | } 29 | 30 | [Fact] 31 | public void LoadCommands_ReadsFile() 32 | { 33 | string tempFile = Path.GetTempFileName(); 34 | File.Delete(tempFile); 35 | 36 | var commands = new SerializedCommands 37 | { 38 | commandArray = new Command[] 39 | { 40 | new PauseCommand() { Cmd = "pause100", Args = "100", Enabled = true }, 41 | new CharsCommand() { Cmd = "hello", Args = "Hello World", Enabled = false } 42 | } 43 | }; 44 | 45 | SerializedCommands.SaveCommands(tempFile, commands, "1.0.0.0"); 46 | 47 | var loaded = SerializedCommands.LoadCommands(tempFile, "1.0.0.0"); 48 | 49 | Assert.NotNull(loaded); 50 | Assert.Equal(2, loaded.commandArray.Length); 51 | Assert.Equal("pause100", loaded.commandArray[0].Cmd); 52 | Assert.Equal("hello", loaded.commandArray[1].Cmd); 53 | 54 | File.Delete(tempFile); 55 | } 56 | 57 | [Fact] 58 | public void LoadCommands_NonExistentFile_ReturnsNull() 59 | { 60 | string tempFile = Path.GetTempFileName(); 61 | File.Delete(tempFile); 62 | 63 | var loaded = SerializedCommands.LoadCommands(tempFile, "1.0.0.0"); 64 | 65 | Assert.Null(loaded); 66 | } 67 | 68 | [Fact] 69 | public void SaveLoad_RoundTrip_PreservesData() 70 | { 71 | string tempFile = Path.GetTempFileName(); 72 | File.Delete(tempFile); 73 | 74 | var original = new SerializedCommands 75 | { 76 | commandArray = new Command[] 77 | { 78 | new StartProcessCommand() { Cmd = "notepad", File = "notepad.exe", Enabled = true }, 79 | new SendInputCommand() { Cmd = "enter", Vk = "VK_RETURN", Enabled = true }, 80 | new MouseCommand() { Cmd = "click", Args = "left", Enabled = false } 81 | } 82 | }; 83 | 84 | SerializedCommands.SaveCommands(tempFile, original, "1.0.0.0"); 85 | var loaded = SerializedCommands.LoadCommands(tempFile, "1.0.0.0"); 86 | 87 | Assert.NotNull(loaded); 88 | Assert.Equal(3, loaded.commandArray.Length); 89 | 90 | var notepadCmd = loaded.commandArray[0] as StartProcessCommand; 91 | Assert.NotNull(notepadCmd); 92 | Assert.Equal("notepad", notepadCmd.Cmd); 93 | Assert.Equal("notepad.exe", notepadCmd.File); 94 | Assert.True(notepadCmd.Enabled); 95 | 96 | var enterCmd = loaded.commandArray[1] as SendInputCommand; 97 | Assert.NotNull(enterCmd); 98 | Assert.Equal("VK_RETURN", enterCmd.Vk); 99 | 100 | var mouseCmd = loaded.commandArray[2] as MouseCommand; 101 | Assert.NotNull(mouseCmd); 102 | Assert.False(mouseCmd.Enabled); 103 | 104 | File.Delete(tempFile); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Win32/Dacl.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | 4 | namespace Microsoft.Win32.Security { 5 | class OrderAceAccess : IComparer { 6 | public int Compare(object o1, object o2) { 7 | AceAccess lhs = (AceAccess)o1; 8 | AceAccess rhs = (AceAccess)o2; 9 | 10 | // The order is: 11 | // denied direct aces 12 | // denied direct object aces 13 | // allowed direct aces 14 | // allowed direct object aces 15 | // denied inherit aces 16 | // denied inherit object aces 17 | // allowed inherit aces 18 | // allowed inherit object aces 19 | 20 | // inherited aces are always "greater" than non-inherited aces 21 | if (lhs.IsInherited && !rhs.IsInherited) { 22 | return 1; 23 | } 24 | 25 | if (!lhs.IsInherited && rhs.IsInherited) { 26 | return -1; 27 | } 28 | 29 | // if the aces are *both* either inherited or non-inherited, continue... 30 | 31 | // allowed aces are always "greater" than denied aces (subject to above) 32 | if (lhs.IsAllowed && !rhs.IsAllowed) { 33 | return 1; 34 | } 35 | 36 | if (!lhs.IsAllowed && rhs.IsAllowed) { 37 | return -1; 38 | } 39 | 40 | // if the aces are *both* either allowed or denied, continue... 41 | 42 | // object aces are always "greater" than non-object aces (subject to above) 43 | if (lhs.IsObjectAce && !rhs.IsObjectAce) { 44 | return 1; 45 | } 46 | 47 | if (!lhs.IsObjectAce && rhs.IsObjectAce) { 48 | return -1; 49 | } 50 | 51 | // aces are "equal" (e.g., both are access denied inherited object aces) 52 | return 0; 53 | } 54 | } 55 | 56 | /// 57 | /// Summary description for Dacl. 58 | /// 59 | #pragma warning disable CA1010, CA1710, CA1303 60 | public class Dacl : Acl { 61 | internal Dacl(IntPtr pacl) : base(pacl) { 62 | } 63 | public Dacl() : base() { 64 | } 65 | /// 66 | /// This algorithm was copied from ATL source code: CAdcl::PrepareAcesForACL. 67 | /// 68 | /// We can't use QuickSort (or any other n log (n)) generic sort algorithm 69 | /// because we want partial ordering to be preserved. All we want to do is sort 70 | /// the elements according to their "Order" (see OrderAceAccess.Compare method), 71 | /// but we want the elements which compare to "Equal" to remain in their 72 | /// original order in the array. 73 | /// 74 | protected override void PrepareAcesForACL() { 75 | IComparer comparer = new OrderAceAccess(); 76 | 77 | int nCount = this.AceCount; 78 | 79 | // Find first "h" such that 80 | // 1. h * 3 + 1 < nCount 81 | // 2. (h - 1) is exactly divisible by 3 82 | int h = 1; 83 | while (h * 3 + 1 < nCount) { 84 | h = 3 * h + 1; 85 | } 86 | 87 | while (h > 0) { 88 | for (int i = h - 1; i < nCount; i++) { 89 | Ace pivot = this.GetAce(i); 90 | 91 | int j; 92 | for (j = i; 93 | (j >= h) && (comparer.Compare(this.GetAce(j - h), pivot) > 0); 94 | j -= h) { 95 | this.SetAce(j, this.GetAce(j - h)); 96 | } 97 | 98 | this.SetAce(j, pivot); 99 | } 100 | 101 | h /= 3; 102 | } 103 | } 104 | public void AddAce(AceAccess ace) { 105 | base.AddAce(ace); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Services/ServiceBaseTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Services 6 | { 7 | public class ServiceBaseTests 8 | { 9 | // Create a concrete implementation for testing 10 | private class TestService : ServiceBase 11 | { 12 | public void TestSetStatus(ServiceStatus status, string msg = "") 13 | { 14 | SetStatus(status, msg); 15 | } 16 | 17 | public void TestSendNotification(ServiceNotification notification, ServiceStatus status, Reply? replyContext = null, string msg = "") 18 | { 19 | SendNotification(notification, status, replyContext, msg); 20 | } 21 | 22 | public void TestError(string msg) 23 | { 24 | Error(msg); 25 | } 26 | 27 | public override void Send(string text, Reply? replyContext = null) 28 | { 29 | base.Send(text, replyContext); 30 | } 31 | } 32 | 33 | [Fact] 34 | public void Constructor_InitializesCurrentStatus() 35 | { 36 | var service = new TestService(); 37 | Assert.Equal(ServiceStatus.Stopped, service.CurrentStatus); 38 | } 39 | 40 | [Fact] 41 | public void SetStatus_UpdatesCurrentStatus() 42 | { 43 | var service = new TestService(); 44 | service.TestSetStatus(ServiceStatus.Started); 45 | Assert.Equal(ServiceStatus.Started, service.CurrentStatus); 46 | } 47 | 48 | [Fact] 49 | public void SetStatus_RaisesNotificationEvent() 50 | { 51 | var service = new TestService(); 52 | bool eventFired = false; 53 | ServiceStatus receivedStatus = ServiceStatus.Stopped; 54 | 55 | service.Notifications += (notification, status, reply, msg) => 56 | { 57 | if (notification == ServiceNotification.StatusChange) 58 | { 59 | eventFired = true; 60 | receivedStatus = status; 61 | } 62 | }; 63 | 64 | service.TestSetStatus(ServiceStatus.Connected); 65 | 66 | Assert.True(eventFired); 67 | Assert.Equal(ServiceStatus.Connected, receivedStatus); 68 | } 69 | 70 | [Fact] 71 | public void SendNotification_RaisesEvent() 72 | { 73 | var service = new TestService(); 74 | bool eventFired = false; 75 | string receivedMsg = ""; 76 | 77 | service.Notifications += (notification, status, reply, msg) => 78 | { 79 | eventFired = true; 80 | receivedMsg = msg; 81 | }; 82 | 83 | service.TestSendNotification(ServiceNotification.ReceivedData, ServiceStatus.Connected, null, "test data"); 84 | 85 | Assert.True(eventFired); 86 | Assert.Equal("test data", receivedMsg); 87 | } 88 | 89 | [Fact] 90 | public void Error_SendsErrorNotification() 91 | { 92 | var service = new TestService(); 93 | bool errorFired = false; 94 | string errorMsg = ""; 95 | 96 | service.Notifications += (notification, status, reply, msg) => 97 | { 98 | if (notification == ServiceNotification.Error) 99 | { 100 | errorFired = true; 101 | errorMsg = msg; 102 | } 103 | }; 104 | 105 | service.TestError("test error"); 106 | 107 | Assert.True(errorFired); 108 | Assert.Equal("test error", errorMsg); 109 | } 110 | 111 | [Fact] 112 | public void Send_ThrowsOnNullText() 113 | { 114 | var service = new TestService(); 115 | Assert.Throws(() => service.Send(null!)); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Integration/CommandExecutionPipelineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Integration 6 | { 7 | /// 8 | /// Integration tests for the command execution pipeline 9 | /// 10 | public class CommandExecutionPipelineTests 11 | { 12 | private class TestReply : Reply 13 | { 14 | public string? LastWrittenText { get; private set; } 15 | public int WriteCallCount { get; private set; } 16 | 17 | public override void Write(string text) 18 | { 19 | LastWrittenText = text; 20 | WriteCallCount++; 21 | } 22 | } 23 | 24 | [Fact] 25 | public void CommandInvoker_Enqueue_ParsesSimpleCommand() 26 | { 27 | string tempFile = System.IO.Path.GetTempFileName(); 28 | System.IO.File.Delete(tempFile); 29 | 30 | var invoker = CommandInvoker.Create(tempFile, "1.0.0.0", false); 31 | 32 | // Enable the pause command for testing 33 | if (invoker!["pause"] is PauseCommand pauseCmd) 34 | { 35 | pauseCmd.Enabled = true; 36 | } 37 | 38 | var reply = new TestReply(); 39 | 40 | // Just test that enqueue doesn't throw 41 | var exception = Record.Exception(() => invoker.Enqueue(reply, "pause")); 42 | Assert.Null(exception); 43 | 44 | System.IO.File.Delete(tempFile); 45 | } 46 | 47 | [Fact] 48 | public void CommandInvoker_WithColonCommand_ParsesCorrectly() 49 | { 50 | string tempFile = System.IO.Path.GetTempFileName(); 51 | System.IO.File.Delete(tempFile); 52 | 53 | var invoker = CommandInvoker.Create(tempFile, "1.0.0.0", false); 54 | 55 | // Enable chars command 56 | if (invoker!["chars:"] is CharsCommand charsCmd) 57 | { 58 | charsCmd.Enabled = true; 59 | } 60 | 61 | var reply = new TestReply(); 62 | 63 | // This should parse "chars:" as command and "hello" as args 64 | // Just test that enqueue doesn't throw 65 | var exception = Record.Exception(() => invoker.Enqueue(reply, "chars:hello")); 66 | Assert.Null(exception); 67 | 68 | System.IO.File.Delete(tempFile); 69 | } 70 | 71 | [Fact] 72 | public void UnknownCommand_IsHandledGracefully() 73 | { 74 | string tempFile = System.IO.Path.GetTempFileName(); 75 | System.IO.File.Delete(tempFile); 76 | 77 | var invoker = CommandInvoker.Create(tempFile, "1.0.0.0", false); 78 | var reply = new TestReply(); 79 | 80 | // This should not throw 81 | var exception = Record.Exception(() => 82 | { 83 | invoker!.Enqueue(reply, "unknowncommand12345"); 84 | }); 85 | 86 | Assert.Null(exception); 87 | 88 | System.IO.File.Delete(tempFile); 89 | } 90 | 91 | [Fact] 92 | public void SingleCharacter_TreatedAsCharsCommand() 93 | { 94 | string tempFile = System.IO.Path.GetTempFileName(); 95 | System.IO.File.Delete(tempFile); 96 | 97 | var invoker = CommandInvoker.Create(tempFile, "1.0.0.0", false); 98 | 99 | // Enable chars command 100 | if (invoker!["chars:"] is CharsCommand charsCmd) 101 | { 102 | charsCmd.Enabled = true; 103 | } 104 | 105 | var reply = new TestReply(); 106 | 107 | // Single character should be treated as chars: command 108 | var exception = Record.Exception(() => 109 | { 110 | invoker.Enqueue(reply, "a"); 111 | }); 112 | 113 | Assert.Null(exception); 114 | 115 | System.IO.File.Delete(tempFile); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/SendInputCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class SendInputCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new SendInputCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | Assert.False(cmd.Shift); 16 | Assert.False(cmd.Ctrl); 17 | Assert.False(cmd.Alt); 18 | Assert.False(cmd.Win); 19 | } 20 | 21 | [Fact] 22 | public void BuiltInCommands_ContainsSendInputCommands() 23 | { 24 | var builtIns = SendInputCommand.BuiltInCommands; 25 | Assert.NotEmpty(builtIns); 26 | 27 | // Check for some expected built-in commands 28 | // Note: These have colons in the Cmd but no "down" or "up" suffix 29 | Assert.Contains(builtIns, c => c.Cmd == "shiftdown:"); 30 | Assert.Contains(builtIns, c => c.Cmd == "shiftup:"); 31 | Assert.Contains(builtIns, c => c.Cmd == "atlesc"); 32 | Assert.Contains(builtIns, c => c.Cmd == "wintab"); 33 | // Also includes all VK_ codes 34 | Assert.Contains(builtIns, c => c.Cmd == "VK_RETURN"); 35 | } 36 | 37 | [Fact] 38 | public void Clone_CreatesIndependentCopy() 39 | { 40 | var original = new SendInputCommand 41 | { 42 | Cmd = "enter", 43 | Vk = "VK_RETURN", 44 | Shift = true, 45 | Ctrl = false, 46 | Alt = true, 47 | Win = false, 48 | Enabled = true 49 | }; 50 | 51 | var clone = (SendInputCommand)original.Clone(null); 52 | 53 | Assert.Equal(original.Cmd, clone.Cmd); 54 | Assert.Equal(original.Vk, clone.Vk); 55 | Assert.Equal(original.Shift, clone.Shift); 56 | Assert.Equal(original.Ctrl, clone.Ctrl); 57 | Assert.Equal(original.Alt, clone.Alt); 58 | Assert.Equal(original.Win, clone.Win); 59 | Assert.Equal(original.Enabled, clone.Enabled); 60 | Assert.NotSame(original, clone); 61 | } 62 | 63 | [Fact] 64 | public void Execute_WhenDisabled_ReturnsFalse() 65 | { 66 | var cmd = new SendInputCommand 67 | { 68 | Vk = "VK_RETURN", 69 | Enabled = false 70 | }; 71 | 72 | bool result = cmd.Execute(); 73 | Assert.False(result); 74 | } 75 | 76 | [Fact] 77 | public void ToString_ReturnsFormattedString() 78 | { 79 | var cmd = new SendInputCommand 80 | { 81 | Cmd = "ctrl_c", 82 | Vk = "c", 83 | Ctrl = true 84 | }; 85 | 86 | string result = cmd.ToString(); 87 | Assert.Contains("ctrl_c", result); 88 | Assert.Contains("c", result); 89 | } 90 | 91 | [Fact] 92 | public void Properties_CanBeSet() 93 | { 94 | var cmd = new SendInputCommand 95 | { 96 | Vk = "VK_ESCAPE", 97 | Shift = true, 98 | Ctrl = true, 99 | Alt = false, 100 | Win = true 101 | }; 102 | 103 | Assert.Equal("VK_ESCAPE", cmd.Vk); 104 | Assert.True(cmd.Shift); 105 | Assert.True(cmd.Ctrl); 106 | Assert.False(cmd.Alt); 107 | Assert.True(cmd.Win); 108 | } 109 | 110 | [Fact] 111 | public void Clone_WithModifierKeys_PreservesFlags() 112 | { 113 | var original = new SendInputCommand 114 | { 115 | Vk = "a", 116 | Shift = true, 117 | Ctrl = true, 118 | Alt = true, 119 | Win = true, 120 | Enabled = true 121 | }; 122 | 123 | var clone = (SendInputCommand)original.Clone(null); 124 | 125 | Assert.True(clone.Shift); 126 | Assert.True(clone.Ctrl); 127 | Assert.True(clone.Alt); 128 | Assert.True(clone.Win); 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Commands/McecCommand.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2017 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source control on SourceForge 8 | // http://sourceforge.net/projects/mcecontroller/ 9 | //------------------------------------------------------------------- 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Linq; 13 | using System.Text; 14 | using System.Text.RegularExpressions; 15 | using System.Windows.Forms; 16 | 17 | namespace MCEControl { 18 | /// 19 | /// Commands that control MCE Controller, or get information about it 20 | /// 21 | public class McecCommand : Command { 22 | public const string CmdPrefix = "mcec:"; 23 | public static new List BuiltInCommands { 24 | get => new List() { 25 | new McecCommand { Cmd = $"{CmdPrefix}" }, // Commands that use form of "cmd:" must define a blank version 26 | new McecCommand { Cmd = $"{CmdPrefix }ver" }, 27 | new McecCommand { Cmd = $"{CmdPrefix }exit" }, 28 | new McecCommand { Cmd = $"{CmdPrefix }cmds" }, 29 | new McecCommand { Cmd = $"{CmdPrefix }time" } 30 | }; 31 | } 32 | 33 | public McecCommand() { 34 | } 35 | 36 | public override string ToString() { 37 | return $"Cmd=\"{Cmd}\""; 38 | } 39 | 40 | public override ICommand Clone(Reply reply) => base.Clone(reply, new McecCommand()); 41 | 42 | // ICommand:Execute 43 | public override bool Execute() { 44 | if (!base.Execute()) { 45 | return false; 46 | } 47 | 48 | if (this.Reply is null) { 49 | throw new InvalidOperationException("Reply property cannot be null."); 50 | } 51 | 52 | if (this.Args is null) { 53 | throw new InvalidOperationException("Args property cannot be null."); 54 | } 55 | 56 | StringBuilder replyBuilder = new StringBuilder(); 57 | switch (Args.ToLowerInvariant()) { 58 | // MCE Controller version 59 | case "ver": 60 | replyBuilder.Append(Application.ProductVersion); 61 | break; 62 | 63 | // Cause MCE Controller to exit 64 | case "exit": 65 | Reply.WriteLine("exiting"); 66 | MainWindow.Instance.ShutDown(); 67 | return true; 68 | 69 | // Return a list of supported commands (really just for testing) 70 | case "cmds": 71 | Command cmd = this; 72 | Match match = null; 73 | try { 74 | replyBuilder.Append(Environment.NewLine); 75 | IOrderedEnumerable orderedKeys = MainWindow.Instance.Invoker.Keys.Cast().OrderBy(c => c); 76 | foreach (string key in orderedKeys) { 77 | cmd = (Command)MainWindow.Instance.Invoker[key]; 78 | ListViewItem item = new ListViewItem(cmd.Cmd); 79 | match = Regex.Match(cmd.GetType().ToString(), @"MCEControl\.([A-za-z]+)Command"); 80 | replyBuilder.Append($"<{match.Groups[1].Value} {cmd.ToString()} />{Environment.NewLine}"); 81 | } 82 | } 83 | catch (Exception e) { 84 | Logger.Instance.Log4.Error($"{this.GetType().Name}: ({Cmd}:{Args}) <{match.Groups[1].Value} {cmd.ToString()}/> - {e.Message}"); 85 | return false; 86 | } 87 | break; 88 | 89 | // Return the current date/time of the PC 90 | case "time": 91 | DateTime dt = DateTime.Now; 92 | replyBuilder.AppendFormat("{0}", DateTime.Now); 93 | break; 94 | } 95 | 96 | // Reply. 97 | replyBuilder.Insert(0, $"{Args}="); 98 | Logger.Instance.Log4.Info($"{this.GetType().Name}: Sending reply: {replyBuilder.ToString()}"); 99 | Reply.WriteLine(replyBuilder.ToString()); 100 | return true; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /docs/example_commands.md: -------------------------------------------------------------------------------- 1 | # Example MCE Controller Commands 2 | 3 | Here's a bunch of example commands users have defined and posted. They may serve as inspiration or guidance... 4 | 5 | ## Start playing the movie Blade Runner on Netflix 6 | 7 | ```xml 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ``` 25 | 26 | Note, this could also be achieved by having the control system send MCE Controller a set of discrete commands, as shown in this screenshot: 27 | 28 | ![Commands](commands_test.png "Commands") 29 | 30 | ## Start Notepad and do stupid tricks with the window 31 | 32 | ```xml 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ``` 48 | 49 | ## Move the mouse 50 | 51 | ```xml 52 | 53 | 54 | 55 | 56 | 57 | ``` 58 | 59 | ## Controlling HDHomeRun 60 | 61 | ```xml 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ``` 79 | 80 | ## Start Media Center (eHome) 81 | 82 | ```xml 83 | 84 | 87 | 88 | ``` 89 | -------------------------------------------------------------------------------- /src/Gma.UserActivityMonitor/HookManager.Structures.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.InteropServices; 2 | 3 | namespace Gma.UserActivityMonitor { 4 | 5 | public static partial class HookManager { 6 | /// 7 | /// The Point structure defines the X- and Y- coordinates of a point. 8 | /// 9 | /// 10 | /// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/gdi/rectangl_0tiq.asp 11 | /// 12 | [StructLayout(LayoutKind.Sequential)] 13 | private struct Point { 14 | /// 15 | /// Specifies the X-coordinate of the point. 16 | /// 17 | public int X; 18 | /// 19 | /// Specifies the Y-coordinate of the point. 20 | /// 21 | public int Y; 22 | } 23 | 24 | /// 25 | /// The MSLLHOOKSTRUCT structure contains information about a low-level keyboard input event. 26 | /// 27 | [StructLayout(LayoutKind.Sequential)] 28 | private struct MouseLLHookStruct { 29 | /// 30 | /// Specifies a Point structure that contains the X- and Y-coordinates of the cursor, in screen coordinates. 31 | /// 32 | public Point Point; 33 | /// 34 | /// If the message is WM_MOUSEWHEEL, the high-order word of this member is the wheel delta. 35 | /// The low-order word is reserved. A positive value indicates that the wheel was rotated forward, 36 | /// away from the user; a negative value indicates that the wheel was rotated backward, toward the user. 37 | /// One wheel click is defined as WHEEL_DELTA, which is 120. 38 | ///If the message is WM_XBUTTONDOWN, WM_XBUTTONUP, WM_XBUTTONDBLCLK, WM_NCXBUTTONDOWN, WM_NCXBUTTONUP, 39 | /// or WM_NCXBUTTONDBLCLK, the high-order word specifies which X button was pressed or released, 40 | /// and the low-order word is reserved. This value can be one or more of the following values. Otherwise, MouseData is not used. 41 | ///XBUTTON1 42 | ///The first X button was pressed or released. 43 | ///XBUTTON2 44 | ///The second X button was pressed or released. 45 | /// 46 | public int MouseData; 47 | /// 48 | /// Specifies the event-injected flag. An application can use the following value to test the mouse Flags. Value Purpose 49 | ///LLMHF_INJECTED Test the event-injected flag. 50 | ///0 51 | ///Specifies whether the event was injected. The value is 1 if the event was injected; otherwise, it is 0. 52 | ///1-15 53 | ///Reserved. 54 | /// 55 | public int Flags; 56 | /// 57 | /// Specifies the Time stamp for this message. 58 | /// 59 | public int Time; 60 | /// 61 | /// Specifies extra information associated with the message. 62 | /// 63 | public int ExtraInfo; 64 | } 65 | 66 | /// 67 | /// The KBDLLHOOKSTRUCT structure contains information about a low-level keyboard input event. 68 | /// 69 | /// 70 | /// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/hooks/hookreference/hookstructures/cwpstruct.asp 71 | /// 72 | [StructLayout(LayoutKind.Sequential)] 73 | private struct KeyboardHookStruct { 74 | /// 75 | /// Specifies a virtual-key code. The code must be a value in the range 1 to 254. 76 | /// 77 | public int VirtualKeyCode; 78 | /// 79 | /// Specifies a hardware scan code for the key. 80 | /// 81 | public int ScanCode; 82 | /// 83 | /// Specifies the extended-key flag, event-injected flag, context code, and transition-state flag. 84 | /// 85 | public int Flags; 86 | /// 87 | /// Specifies the Time stamp for this message. 88 | /// 89 | public int Time; 90 | /// 91 | /// Specifies extra information associated with the message. 92 | /// 93 | public int ExtraInfo; 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Helpers/CommandFileWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using menelabs.core; 4 | 5 | namespace MCEControl { 6 | public class CommandFileWatcher : IDisposable { 7 | 8 | #pragma warning disable IDE0052 // Remove unread private members 9 | private FileSystemSafeWatcher fileWatcher; 10 | #pragma warning restore IDE0052 // Remove unread private members 11 | 12 | public CommandFileWatcher(string file) { 13 | fileWatcher = CreateFileWatcher(file); 14 | } 15 | 16 | public event EventHandler ChangedEvent; 17 | /// 18 | /// OnChangeEvent is raised whenever the CommandTable is updated due to 19 | /// user commands file changes 20 | /// 21 | protected virtual void OnChangedEvent() { 22 | // Make a temporary copy of the event to avoid possibility of 23 | // a race condition if the last subscriber unsubscribes 24 | // immediately after the null check and before the event is raised. 25 | EventHandler handler = ChangedEvent; 26 | 27 | // Event will be null if there are no subscribers 28 | if (handler != null) { 29 | handler(this, null); 30 | } 31 | } 32 | 33 | private FileSystemSafeWatcher CreateFileWatcher(string path) { 34 | 35 | // Create a new FileSystemSafeWatcher and set its properties. 36 | FileSystemSafeWatcher watcher = new FileSystemSafeWatcher(); 37 | watcher.Path = Path.GetDirectoryName(path); 38 | /* Watch for changes in LastAccess and LastWrite times, and 39 | the renaming of files or directories. */ 40 | watcher.NotifyFilter = NotifyFilters.LastWrite; 41 | watcher.Filter = Path.GetFileName(path); 42 | 43 | // Add event handlers. 44 | watcher.Changed += new FileSystemEventHandler(OnChanged); 45 | //watcher.Created += new FileSystemEventHandler(OnChanged); 46 | //watcher.Deleted += new FileSystemEventHandler(OnChanged); 47 | //watcher.Renamed += new RenamedEventHandler(OnRenamed); 48 | 49 | // Begin watching. 50 | watcher.EnableRaisingEvents = true; 51 | Logger.Instance.Log4.Info($"{this.GetType().Name}: Watching {watcher.Path}\\{watcher.Filter} for changes"); 52 | return watcher; 53 | 54 | } 55 | 56 | private void OnChanged(object source, FileSystemEventArgs e) { 57 | Logger.Instance.Log4.Info($"{this.GetType().Name}: {e.FullPath} changed"); 58 | TelemetryService.Instance.TrackEvent("Commands file change detected"); 59 | OnChangedEvent(); 60 | } 61 | 62 | //private void OnRenamed(object source, RenamedEventArgs e) { 63 | // // Specify what is done when a file is renamed. 64 | // Logger.Instance.Log4.Info($"CommandInvoker:{e.OldFullPath} renamed to {e.FullPath}"); 65 | //} 66 | 67 | #region IDisposable Support 68 | private bool disposedValue = false; // To detect redundant calls 69 | 70 | protected virtual void Dispose(bool disposing) { 71 | if (!disposedValue) { 72 | if (disposing) { 73 | if (fileWatcher != null) { 74 | Stop(); 75 | fileWatcher.Dispose(); 76 | fileWatcher = null; 77 | } 78 | } 79 | disposedValue = true; 80 | } 81 | } 82 | 83 | // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources. 84 | // ~CommandTable() 85 | // { 86 | // // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 87 | // Dispose(false); 88 | // } 89 | 90 | public void Dispose() { 91 | // Do not change this code. Put cleanup code in Dispose(bool disposing) above. 92 | Dispose(true); 93 | GC.SuppressFinalize(this); 94 | } 95 | 96 | internal void Stop() { 97 | fileWatcher.EnableRaisingEvents = false; 98 | fileWatcher.Changed -= OnChanged; 99 | //fileWatcher.Created -= OnChanged; 100 | //fileWatcher.Deleted -= OnChanged; 101 | //fileWatcher.Renamed -= OnRenamed; 102 | Logger.Instance.Log4.Info($"{this.GetType().Name}: Stopped watching {fileWatcher.Path}\\{fileWatcher.Filter} for changes"); 103 | } 104 | #endregion 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/MCEControl.xUnit/Commands/StartProcessCommandTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | using MCEControl; 4 | 5 | namespace MCEControl.xUnit.Commands 6 | { 7 | public class StartProcessCommandTests 8 | { 9 | [Fact] 10 | public void Constructor_SetsDefaultProperties() 11 | { 12 | var cmd = new StartProcessCommand(); 13 | Assert.NotNull(cmd); 14 | Assert.False(cmd.Enabled); 15 | } 16 | 17 | [Fact] 18 | public void BuiltInCommands_ContainsExpectedCommands() 19 | { 20 | var builtIns = StartProcessCommand.BuiltInCommands; 21 | Assert.NotEmpty(builtIns); 22 | 23 | // Check for some expected built-in commands 24 | Assert.Contains(builtIns, c => c.Cmd == "code"); 25 | Assert.Contains(builtIns, c => c.Cmd == "tada"); 26 | Assert.Contains(builtIns, c => c.Cmd == "type_into_notepad"); 27 | Assert.Contains(builtIns, c => c.Cmd == "netflix"); 28 | } 29 | 30 | [Fact] 31 | public void Clone_CreatesIndependentCopy() 32 | { 33 | var original = new StartProcessCommand 34 | { 35 | Cmd = "notepad", 36 | File = "notepad.exe", 37 | Arguments = "test.txt", 38 | Verb = "open", 39 | Enabled = true 40 | }; 41 | 42 | var clone = (StartProcessCommand)original.Clone(null); 43 | 44 | Assert.Equal(original.Cmd, clone.Cmd); 45 | Assert.Equal(original.File, clone.File); 46 | Assert.Equal(original.Arguments, clone.Arguments); 47 | Assert.Equal(original.Verb, clone.Verb); 48 | Assert.Equal(original.Enabled, clone.Enabled); 49 | Assert.NotSame(original, clone); 50 | } 51 | 52 | [Fact] 53 | public void Execute_WhenDisabled_ReturnsFalse() 54 | { 55 | var cmd = new StartProcessCommand 56 | { 57 | Cmd = "test", 58 | File = "notepad.exe", 59 | Enabled = false 60 | }; 61 | 62 | bool result = cmd.Execute(); 63 | Assert.False(result); 64 | } 65 | 66 | [Fact] 67 | public void Execute_WithNullReply_ThrowsNullReferenceException() 68 | { 69 | var cmd = new StartProcessCommand 70 | { 71 | Cmd = "test", 72 | File = "notepad.exe", 73 | Enabled = true, 74 | Reply = null 75 | }; 76 | 77 | Assert.Throws(() => cmd.Execute()); 78 | } 79 | 80 | [Fact] 81 | public void ToString_ReturnsFormattedString() 82 | { 83 | var cmd = new StartProcessCommand 84 | { 85 | Cmd = "notepad", 86 | File = "notepad.exe", 87 | Arguments = "test.txt", 88 | Verb = "open" 89 | }; 90 | 91 | string result = cmd.ToString(); 92 | Assert.Contains("notepad", result); 93 | Assert.Contains("notepad.exe", result); 94 | Assert.Contains("test.txt", result); 95 | Assert.Contains("open", result); 96 | } 97 | 98 | [Fact] 99 | public void Properties_CanBeSet() 100 | { 101 | var cmd = new StartProcessCommand 102 | { 103 | File = "cmd.exe", 104 | Arguments = "/c dir", 105 | Verb = "runas" 106 | }; 107 | 108 | Assert.Equal("cmd.exe", cmd.File); 109 | Assert.Equal("/c dir", cmd.Arguments); 110 | Assert.Equal("runas", cmd.Verb); 111 | } 112 | 113 | [Fact] 114 | public void Clone_WithEmbeddedCommands_ClonesNested() 115 | { 116 | var original = new StartProcessCommand 117 | { 118 | Cmd = "notepad", 119 | File = "notepad.exe", 120 | Enabled = true, 121 | EmbeddedCommands = new System.Collections.Generic.List 122 | { 123 | new PauseCommand { Cmd = "pause", Args = "100", Enabled = true }, 124 | new CharsCommand { Cmd = "chars:", Args = "Hello", Enabled = true } 125 | } 126 | }; 127 | 128 | var clone = (StartProcessCommand)original.Clone(null); 129 | 130 | Assert.NotNull(clone.EmbeddedCommands); 131 | Assert.Equal(2, clone.EmbeddedCommands.Count); 132 | Assert.IsType(clone.EmbeddedCommands[0]); 133 | Assert.IsType(clone.EmbeddedCommands[1]); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/GITHUB_ACTIONS_SETUP.md: -------------------------------------------------------------------------------- 1 | # GitHub Actions CI/CD Setup Complete 2 | 3 | ## ?? What Was Created 4 | 5 | I've successfully set up a modern GitHub Actions CI/CD workflow for your MCEControl project. Here's what was added: 6 | 7 | ### ?? Workflow Files 8 | 9 | #### 1. **`.github/workflows/ci.yml`** - Main CI Workflow 10 | - **Triggers on:** 11 | - Push to `develop` branch 12 | - Pull requests targeting `develop` branch 13 | - **Features:** 14 | - ? Builds the solution using .NET 8.0 15 | - ? Runs all xUnit tests 16 | - ? Generates code coverage reports (using coverlet) 17 | - ? Publishes test results with visual reports 18 | - ? Uploads artifacts (coverage & test results) 19 | - ? Adds coverage summary to PR comments 20 | - ? Validates coverage thresholds 21 | 22 | #### 2. **`.github/workflows/pr-validation.yml`** - PR Quality Gates 23 | - Validates PR title format (conventional commits) 24 | - Checks for merge conflicts 25 | - Skips draft PRs automatically 26 | 27 | ### ?? Templates & Configuration 28 | 29 | #### 3. **`.github/pull_request_template.md`** 30 | - Standardized PR description template 31 | - Checklists for code quality 32 | - Links to related issues 33 | 34 | #### 4. **`.github/ISSUE_TEMPLATE/bug_report.md`** 35 | - Structured bug report template 36 | - Environment details section 37 | - Log attachment guidelines 38 | 39 | #### 5. **`.github/ISSUE_TEMPLATE/feature_request.md`** 40 | - Feature request template 41 | - Alternative solutions section 42 | - Contribution willingness checkbox 43 | 44 | #### 6. **`.github/dependabot.yml`** 45 | - Automated dependency updates 46 | - Groups minor/patch updates together 47 | - Separate configs for main project and tests 48 | - Weekly schedule on Mondays 49 | 50 | #### 7. **`.github/workflows/README.md`** 51 | - Complete documentation of all workflows 52 | - Status badge example 53 | - Local testing instructions 54 | - Troubleshooting guide 55 | 56 | ## ?? Next Steps 57 | 58 | ### 1. Add Status Badge to README 59 | Add this to your main `README.md`: 60 | 61 | ```markdown 62 | [![CI](https://github.com/kindel/mcec/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/kindel/mcec/actions/workflows/ci.yml) 63 | ``` 64 | 65 | ### 2. Test the Workflow 66 | Create a test PR to the `develop` branch to see the workflow in action: 67 | 68 | ```bash 69 | git checkout -b test/github-actions 70 | git add .github/ 71 | git commit -m "ci: Add GitHub Actions CI/CD workflows" 72 | git push origin test/github-actions 73 | ``` 74 | 75 | Then create a PR targeting `develop`. 76 | 77 | ### 3. Adjust Coverage Threshold (Optional) 78 | In `.github/workflows/ci.yml`, line 96, you can adjust the coverage threshold: 79 | 80 | ```yaml 81 | $threshold = 0 # Change to desired percentage (e.g., 70 for 70%) 82 | ``` 83 | 84 | ### 4. Configure Branch Protection Rules 85 | In your GitHub repository settings, consider adding: 86 | - Require PR reviews before merging 87 | - Require status checks to pass (select "Build and Test") 88 | - Require branches to be up to date before merging 89 | 90 | ## ?? What the CI Workflow Does 91 | 92 | 1. **Checkout** - Gets your code 93 | 2. **Setup .NET** - Installs .NET 8.0 SDK 94 | 3. **Restore** - Downloads NuGet packages 95 | 4. **Build** - Compiles the solution in Release mode 96 | 5. **Test** - Runs all xUnit tests with coverage collection 97 | 6. **Report** - Generates HTML coverage reports 98 | 7. **Upload** - Stores test results and coverage as artifacts 99 | 8. **Validate** - Checks coverage thresholds 100 | 101 | ## ?? Key Features 102 | 103 | - **Windows-specific**: Uses `windows-latest` runner (required for Windows Forms) 104 | - **Fast feedback**: Builds and tests on every PR 105 | - **Code coverage**: Tracks test coverage with visual reports 106 | - **Test results**: Beautiful test result summaries in PR checks 107 | - **Artifacts**: 30-day retention of test results and coverage reports 108 | - **Dependabot**: Automatic dependency updates 109 | 110 | ## ??? Testing Locally 111 | 112 | Run the same checks locally: 113 | 114 | ```bash 115 | # From repository root 116 | dotnet restore src/MCEControl.sln 117 | dotnet build src/MCEControl.sln --configuration Release 118 | dotnet test src/MCEControl.sln --configuration Release --collect:"XPlat Code Coverage" 119 | ``` 120 | 121 | ## ?? Additional Resources 122 | 123 | - See `.github/workflows/README.md` for detailed documentation 124 | - GitHub Actions docs: https://docs.github.com/en/actions 125 | - Dependabot docs: https://docs.github.com/en/code-security/dependabot 126 | 127 | ## ?? Workflow Status 128 | 129 | Once you push these changes, you can view workflow runs at: 130 | ``` 131 | https://github.com/kindel/mcec/actions 132 | ``` 133 | 134 | --- 135 | 136 | **All files are ready to commit!** The CI workflow will automatically run when you push to `develop` or create a PR. 137 | -------------------------------------------------------------------------------- /src/MCEControl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | WinExe 4 | net8.0-windows 5 | true 6 | Resources\Guillen.Icon.ico 7 | MCEControl 8 | MCEControl 9 | latest 10 | true 11 | app.manifest 12 | false 13 | disable 14 | disable 15 | AnyCPU 16 | true 17 | true 18 | CA1416 19 | PerMonitorV2 20 | 21 | 22 | 23 | DEBUG;TRACE 24 | full 25 | true 26 | false 27 | 28 | 29 | 30 | TRACE 31 | none 32 | false 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ResXFileCodeGenerator 53 | Resources.Designer.cs 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | True 83 | True 84 | Resources.resx 85 | 86 | 87 | True 88 | True 89 | AssemblyFileVersion.tt 90 | 91 | 92 | True 93 | True 94 | TelemetryService.tt 95 | 96 | 97 | 98 | 99 | 100 | TextTemplatingFileGenerator 101 | AssemblyFileVersion.cs 102 | 103 | 104 | TextTemplatingFileGenerator 105 | TelemetryService.tt.cs 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /src/Commands/SendMessageCommand.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------- 2 | // Copyright © 2017 Kindel Systems, LLC 3 | // http://www.kindel.com 4 | // charlie@kindel.com 5 | // 6 | // Published under the MIT License. 7 | // Source control on SourceForge 8 | // http://sourceforge.net/projects/mcecontroller/ 9 | //------------------------------------------------------------------- 10 | 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Diagnostics; 14 | using System.Xml.Serialization; 15 | using System.Linq; 16 | using Microsoft.Win32.Security; 17 | 18 | namespace MCEControl { 19 | using DWORD = UInt32; 20 | 21 | /// 22 | /// Summary description for SendMessageCommand. 23 | /// 24 | public class SendMessageCommand : Command { 25 | private int msg; 26 | [XmlAttribute("msg")] public int Msg { get => msg; set => msg = value; } 27 | 28 | // This is int so that -1 can be specified in the XML 29 | private int lParam; 30 | [XmlAttribute("lparam")] public int LParam { get => lParam; set => lParam = value; } 31 | 32 | private int wParam; 33 | [XmlAttribute("wparam")] public int WParam { get => wParam; set => wParam = value; } 34 | 35 | private String className; 36 | [XmlAttribute("classname")] public String ClassName { get => className; set => className = value; } 37 | 38 | private String windowName; 39 | [XmlAttribute("windowname")] public String WindowName { get => windowName; set => windowName = value; } 40 | 41 | public static new List BuiltInCommands { 42 | get => new List() { 43 | new SendMessageCommand() { Cmd = "maximize", Msg=274, wParam=61488, lParam=0 }, 44 | new SendMessageCommand() { Cmd = "screensaver", Msg=274, wParam=61760, lParam=0 }, 45 | new SendMessageCommand() { Cmd = "monitoroff", Msg=274, wParam=61808, lParam=2 }, 46 | new SendMessageCommand() { Cmd = "monitoron", Msg=274, wParam=61808, lParam=-1 } 47 | }; 48 | } 49 | 50 | public SendMessageCommand() { 51 | } 52 | 53 | public SendMessageCommand(String className, String windowName, int msg, int wParam, int lParam) { 54 | ClassName = className; 55 | WindowName = windowName; 56 | Msg = (int)msg; 57 | WParam = (int)wParam; 58 | LParam = (int)lParam; 59 | } 60 | 61 | public override string ToString() { 62 | return $"Cmd=\"{Cmd}\" Msg=\"{Msg}\" lParam=\"{LParam}\" wParam=\"{WParam}\" ClassName=\"{ClassName}\" WindowName=\"{WindowName}\""; 63 | } 64 | 65 | public override ICommand Clone(Reply reply) => base.Clone(reply, new SendMessageCommand(ClassName, WindowName, Msg, WParam, LParam)); 66 | 67 | // ICommand:Execute 68 | public override bool Execute() { 69 | if (!base.Execute()) { 70 | return false; 71 | } 72 | 73 | try { 74 | if (!string.IsNullOrWhiteSpace(ClassName)) { 75 | Process[] procs = Process.GetProcessesByName(ClassName); 76 | if (procs.Length > 0) { 77 | Process win = procs[0]; 78 | 79 | if (!string.IsNullOrWhiteSpace(WindowName)) { 80 | // Find MainWindowTitle matching WindowName 81 | win = procs.FirstOrDefault(w => w.MainWindowTitle.Equals(WindowName, StringComparison.Ordinal)); 82 | } 83 | if (win == null) { 84 | Logger.Instance.Log4.Error($"{this.GetType().Name}: Could not find a window of class '{ClassName}' captioned with '{WindowName}'"); 85 | } 86 | else { 87 | Logger.Instance.Log4.Info($"{this.GetType().Name}: SendMessage(\"{win.MainWindowTitle}\", {Msg}, {WParam}, {LParam}) - {ToString()}"); 88 | Win32.SendMessage(win.MainWindowHandle, (DWORD)Msg, (DWORD)WParam, (DWORD)LParam); 89 | } 90 | } 91 | else { 92 | Logger.Instance.Log4.Error($"{this.GetType().Name}: GetProcessByName for class '{ClassName}' failed"); 93 | } 94 | } 95 | else 96 | { 97 | IntPtr h = Win32.GetForegroundWindow(); 98 | Logger.Instance.Log4.Info($"{this.GetType().Name}: SendMessage(, {Msg}, {WParam}, {LParam}) - {ToString()}"); 99 | Win32.SendMessage(h, (DWORD)Msg, (DWORD)WParam, (DWORD)LParam); 100 | } 101 | } 102 | catch (Exception e) { 103 | Logger.Instance.Log4.Error($"{this.GetType().Name}: Failed for '{ClassName}' with error: {e.Message}"); 104 | return true; 105 | } 106 | return false; 107 | } 108 | } 109 | } 110 | --------------------------------------------------------------------------------