├── .github └── workflows │ └── build.yml ├── .gitignore ├── CHANGES.md ├── LICENSE ├── README.md ├── checkstyle.xml ├── pom.xml ├── spotbugs-exclude.xml └── src └── main ├── java └── com │ └── fatico │ └── winthing │ ├── Application.java │ ├── ApplicationModule.java │ ├── Settings.java │ ├── common │ └── BaseController.java │ ├── gui │ └── WindowGui.java │ ├── logging │ ├── ConsoleLogger.java │ └── FileLogEnabler.java │ ├── messaging │ ├── Engine.java │ ├── Message.java │ ├── MessagePublisher.java │ ├── MessagingModule.java │ ├── QualityOfService.java │ ├── Registry.java │ ├── RegistryConfigurator.java │ └── SimpleRegistry.java │ ├── systems │ ├── desktop │ │ ├── DesktopController.java │ │ ├── DesktopService.java │ │ └── Module.java │ ├── keyboard │ │ ├── KeyboardController.java │ │ ├── KeyboardService.java │ │ └── Module.java │ ├── radeon │ │ ├── AdlException.java │ │ ├── Module.java │ │ ├── RadeonController.java │ │ ├── RadeonService.java │ │ └── jna │ │ │ └── AtiAdl.java │ └── system │ │ ├── Module.java │ │ ├── SystemCommander.java │ │ ├── SystemController.java │ │ └── SystemService.java │ └── windows │ ├── SystemException.java │ ├── WindowsModule.java │ ├── input │ ├── KeyboardKey.java │ └── MouseButton.java │ └── jna │ ├── Advapi32.java │ ├── Kernel32.java │ └── User32.java └── resources ├── application.properties ├── favicon-green.png ├── favicon-red.png ├── favicon.ico ├── favicon.png └── logback.xml /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - uses: actions/setup-java@v1 9 | with: 10 | java-version: 8 11 | java-package: jdk 12 | - name: Run Checkstyle 13 | run: mvn checkstyle:check 14 | - name: Build 15 | run: mvn package 16 | - name: Run PMD 17 | run: mvn pmd:check 18 | - name: Run SpotBugs 19 | run: mvn spotbugs:check 20 | - name: Create artifacts directory 21 | run: mkdir -p target/artifacts 22 | - name: Prepare artifacts for packaging 23 | run: mv target/winthing-* target/artifacts 24 | - uses: actions/upload-artifact@v1 25 | with: 26 | name: artifacts 27 | path: target/artifacts 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.iml 3 | .idea 4 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Release notes 2 | 3 | ## 1.4.2 4 | 5 | - Updated build process and minor security fixes. 6 | 7 | ## 1.4.1 8 | 9 | - Fix winthing.ini whitelist file and command execution 10 | - Format log in the console and log file 11 | 12 | ## 1.4.0 13 | 14 | - Fix duplicate system/system in winthing/system/online topic 15 | - Fix Windows executable build for Java 11 16 | - Fix logging to file when -debug parameter is passed 17 | - Change how GUI is created 18 | - Fix check whether file logging should be enabled 19 | 20 | ## 1.3.0 21 | 22 | - Add tray icon and GUI console window 23 | - Read settings from file 24 | - Add configurable whitelist of allowed commands to execute 25 | - Create file logs only when enabled 26 | - Remove prefix from config parameters 27 | - Update dependencies 28 | - Update build for Java 11 29 | 30 | ## 1.2.0 31 | 32 | - Original version 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WinThing 2 | 3 | ![Build Status](https://github.com/msiedlarek/winthing/workflows/build/badge.svg) 4 | 5 | A modular background service that makes Windows remotely controllable through MQTT. For home automation and Internet of Things. 6 | 7 | ## :warning: Deprecation Notice :warning: 8 | 9 | **WinThing is no longer actively maintained.** As an alternative, please consider [IOT Link](https://iotlink.gitlab.io/), which is actively maintained and has a lot more features, including most of WinThing's features. 10 | 11 | ## Requirements 12 | 13 | Java 8 or greater. 14 | 15 | ## Running 16 | 17 | Download either JAR or EXE file from [Releases page](https://github.com/msiedlarek/winthing/releases) and execute it: 18 | 19 | target/winthing-1.4.2.exe 20 | java -jar target/winthing-1.4.2.jar 21 | 22 | ## Configuration 23 | 24 | Configuration parameters can be passed from command line or they can be placed in configuration files in the working directory from where you launch WinThing. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
PropertyDescriptionDefault
brokerURL of the MQTT broker to use127.0.0.1:1883
usernameUsername used when connecting to MQTT brokermqtt
passwordPassword used when connecting to MQTT brokermqtt
clientidClient ID to present to the brokerWinThing
prefixPrefix for all MQTT topics used by this WinThing instancewinthing
reconnectTime interval between connection attempts in seconds5
35 | 36 | ### Command line parameters 37 | 38 | Example how to pass parameters from command line: 39 | 40 | java -Dbroker="127.0.0.1:1883" -jar winthing-1.2.0.jar 41 | 42 | ### winthing.conf 43 | 44 | WinThing will look for this file in the current working directory (directory from where you launched WinThing). Create this file and put desired parameters into it. 45 | 46 | Example file: 47 | 48 | broker = "127.0.0.1:1883" 49 | username = "mqtt" 50 | password = "somesecret" 51 | 52 | ### winthing.ini 53 | 54 | By default WinThing executes any command it receives in the system/commands/run topic. Create this file in the current working directory to whitelist only specific commands. The file contains an unique string identifier (used as payload in the MQTT message, see below) and path to executable. 55 | 56 | Example file: 57 | 58 | notepad = "c:/windows/system32/notepad.exe" 59 | adobe = "c:\\program files\\adobe\\reader.exe" 60 | 61 | *Note you can use slash* ' / ' *or double backslash* ' \\\\ ' *as path separator.* 62 | 63 | ## Logging 64 | 65 | You can open application log by clicking on the tray icon. To log into **winthing.log** file in the current working directory run WinThing with the **-debug** parameter. 66 | 67 | winthing.exe -debug 68 | 69 | ## Supported messages 70 | 71 | The payload of all messages is either empty or a valid JSON element (possibly a primitive, like a single integer). This means, specifically, that if an argument is supposed to be a single string, it should be sent in double quotes. 72 | 73 | Example valid message payloads: 74 | 75 | * `123` 76 | * `true` 77 | * `"notepad.exe"` 78 | * `[1024, 768]` 79 | * `["notepad.exe", "C:\\file.txt", "C:\\"]` (note that JSON string requires escaped backslash) 80 | 81 | ### Broadcast status 82 | 83 | #### System 84 | 85 | **Topic:** winthing/system/online
86 | **Payload:** state:boolean
87 | **QoS:** 2
88 | **Persistent:** yes
89 | 90 | True when WinThing is running, false otherwise. WinThing registers a "last will" message with the broker to notify clients when WinThing disconnects. 91 | 92 | ### Commands 93 | 94 | #### System 95 | 96 | **Topic:** winthing/system/commands/shutdown
97 | **Payload:** - 98 | 99 | Trigger immediate system shutdown. 100 | 101 | --- 102 | 103 | **Topic:** winthing/system/commands/reboot
104 | **Payload:** - 105 | 106 | Trigger immediate system reboot. 107 | 108 | --- 109 | 110 | **Topic:** winthing/system/commands/suspend
111 | **Payload:** - 112 | 113 | Trigger immediate system suspend. 114 | 115 | --- 116 | 117 | **Topic:** winthing/system/commands/hibernate
118 | **Payload:** - 119 | 120 | Trigger immediate system hibernate. 121 | 122 | --- 123 | 124 | **Topic:** winthing/system/commands/run
125 | **Payload:** [command:string, arguments:string, workingDirectory:string] 126 | 127 | Run a command. Arguments and working directory are optional (empty string and null by default).
128 | If whitelist is enabled, only the command as unique identifier is required. The identifier is checked against the whitelist file (see **whitelist.ini** above). 129 | 130 | --- 131 | 132 | **Topic:** winthing/system/commands/open
133 | **Payload:** uri:string 134 | 135 | Opens an URI, like a website in a browser or a disk location in a file browser. 136 | 137 | #### Desktop 138 | 139 | **Topic:** winthing/desktop/commands/close_active_window
140 | **Payload:** - 141 | 142 | Closes currently active window. 143 | 144 | --- 145 | 146 | **Topic:** winthing/desktop/commands/set_display_sleep
147 | **Payload:** displaySleep:boolean 148 | 149 | Puts the display to sleep (on true) or wakes it up (on false). 150 | 151 | #### Keyboard 152 | 153 | **Topic:** winthing/keyboard/commands/press_keys
154 | **Payload:** [key:string...] 155 | 156 | Simulates pressing of given set of keyboard keys. Keys are specified by name. List of available key names and aliases can be found [here](src/main/java/com/fatico/winthing/windows/input/KeyboardKey.java). 157 | 158 | #### ATI Radeon display driver 159 | 160 | **Topic:** winthing/radeon/commands/set_best_resolution
161 | **Payload:** - 162 | 163 | Sets the screen to the best available resolution. 164 | 165 | --- 166 | 167 | **Topic:** winthing/radeon/commands/set_resolution
168 | **Payload:** [widthInPixels:integer, heightInPixels:integer] 169 | 170 | Sets the screen to the given resolution. 171 | 172 | ## Building 173 | 174 | Maven is required to build the application. For convenience the Maven build file contains execution to produce a Windows executable. 175 | 176 | mvn clean package 177 | 178 | To run static analysis tools, use these commands: 179 | 180 | mvn checkstyle:check 181 | mvn pmd:check 182 | mvn spotbugs:check 183 | 184 | ## License 185 | 186 | Copyright 2015-2020 Mikołaj Siedlarek <mikolaj@siedlarek.pl> 187 | 188 | Licensed under the Apache License, Version 2.0 (the "License"); 189 | you may not use this software except in compliance with the License. 190 | You may obtain a copy of the License at 191 | 192 | > http://www.apache.org/licenses/LICENSE-2.0 193 | 194 | Unless required by applicable law or agreed to in writing, software 195 | distributed under the License is distributed on an "AS IS" BASIS, 196 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 197 | See the License for the specific language governing permissions and 198 | limitations under the License. 199 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 59 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 76 | 77 | 78 | 80 | 81 | 82 | 88 | 89 | 90 | 91 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 105 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 122 | 124 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 171 | 172 | 173 | 174 | 176 | 177 | 178 | 179 | 181 | 182 | 183 | 184 | 186 | 187 | 188 | 189 | 191 | 192 | 193 | 194 | 196 | 197 | 198 | 199 | 201 | 202 | 203 | 204 | 206 | 207 | 208 | 209 | 211 | 212 | 213 | 214 | 216 | 217 | 218 | 219 | 221 | 223 | 225 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 255 | 256 | 257 | 259 | 260 | 261 | 262 | 267 | 268 | 269 | 270 | 273 | 274 | 275 | 276 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 290 | 291 | 292 | 293 | 294 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 322 | 323 | 324 | 325 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | com.fatico 7 | winthing 8 | 1.4.2 9 | 10 | WinThing 11 | Remotely control Windows through MQTT. 12 | https://github.com/msiedlarek/winthing 13 | 14 | 15 | https://github.com/msiedlarek/winthing/issues 16 | GitHub Issues 17 | 18 | 19 | 20 | 21 | Apache License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | 25 | 26 | 27 | 28 | https://github.com/msiedlarek/winthing 29 | scm:git:git://github.com/msiedlarek/winthing.git 30 | scm:git:git@github.com:msiedlarek/winthing.git 31 | 32 | 33 | 34 | 35 | mikolaj@siedlarek.pl 36 | Mikołaj Siedlarek 37 | msiedlarek 38 | 39 | 40 | 41 | 42 | 1.8 43 | 1.8 44 | UTF-8 45 | UTF-8 46 | ${project.basedir}/checkstyle.xml 47 | 1.7.30 48 | 1.2.3 49 | 28.2-jre 50 | 4.2.3 51 | 2.8.6 52 | 1.2.2 53 | 5.5.0 54 | 1.4.0 55 | 3.1.2 56 | 4.0.1 57 | 58 | 59 | 60 | 61 | org.slf4j 62 | slf4j-api 63 | ${version.slf4j} 64 | 65 | 66 | org.slf4j 67 | jcl-over-slf4j 68 | ${version.slf4j} 69 | runtime 70 | 71 | 72 | ch.qos.logback 73 | logback-classic 74 | ${version.logback} 75 | 76 | 77 | com.google.guava 78 | guava 79 | ${version.guava} 80 | 81 | 82 | com.google.inject 83 | guice 84 | ${version.guice} 85 | 86 | 87 | com.google.code.gson 88 | gson 89 | ${version.gson} 90 | 91 | 92 | org.eclipse.paho 93 | org.eclipse.paho.client.mqttv3 94 | ${version.paho} 95 | 96 | 97 | net.java.dev.jna 98 | jna 99 | ${version.jna} 100 | 101 | 102 | net.java.dev.jna 103 | jna-platform 104 | ${version.jna} 105 | 106 | 107 | com.typesafe 108 | config 109 | ${version.typesafe.config} 110 | 111 | 112 | org.codehaus.janino 113 | janino 114 | ${version.janino} 115 | 116 | 117 | com.github.spotbugs 118 | spotbugs 119 | ${version.spotbugs} 120 | 121 | 122 | 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-checkstyle-plugin 128 | 3.1.1 129 | 130 | ${checkstyle.config} 131 | UTF-8 132 | true 133 | warning 134 | false 135 | true 136 | 137 | 138 | 139 | com.puppycrawl.tools 140 | checkstyle 141 | 8.30 142 | 143 | 144 | 145 | 146 | org.apache.maven.plugins 147 | maven-pmd-plugin 148 | 3.13.0 149 | 150 | true 151 | true 152 | false 153 | 154 | 155 | 156 | com.github.spotbugs 157 | spotbugs-maven-plugin 158 | 3.1.11 159 | 160 | Max 161 | Low 162 | true 163 | spotbugs-exclude.xml 164 | 165 | 166 | com.h3xstream.findsecbugs 167 | findsecbugs-plugin 168 | 1.10.1 169 | 170 | 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-compiler-plugin 176 | 3.8.1 177 | 178 | 179 | -Xlint 180 | -Werror 181 | 182 | 183 | 184 | 185 | org.apache.maven.plugins 186 | maven-shade-plugin 187 | 3.2.2 188 | 189 | 190 | package 191 | 192 | shade 193 | 194 | 195 | ${project.artifactId}-${project.version} 196 | ${project.build.directory}/pom.xml 197 | 198 | 199 | 201 | com.fatico.winthing.Application 202 | 203 | ${project.name} 204 | ${project.version} 205 | 206 | 207 | 208 | 209 | 210 | *:* 211 | 212 | META-INF/*.SF 213 | META-INF/*.DSA 214 | META-INF/*.RSA 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | com.akathist.maven.plugins.launch4j 224 | launch4j-maven-plugin 225 | 1.7.25 226 | 227 | 228 | l4j-clui 229 | package 230 | 231 | launch4j 232 | 233 | 234 | gui 235 | ${project.build.directory}/${project.artifactId}-${project.version}.jar 236 | ${project.build.directory}/winthing-${project.version}.exe 237 | ${project.build.resources[0].directory}/favicon.ico 238 | http://java.com/download 239 | 240 | com.fatico.winthing.Application 241 | 242 | 243 | false 244 | false 245 | ${maven.compiler.target} 246 | preferJdk 247 | 64 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | -------------------------------------------------------------------------------- /spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/Application.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing; 2 | 3 | import com.fatico.winthing.gui.WindowGui; 4 | import com.fatico.winthing.messaging.Engine; 5 | import com.google.inject.Guice; 6 | import com.google.inject.Injector; 7 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | public class Application { 12 | private static final Application app = new Application(); 13 | 14 | private boolean debug = false; 15 | private WindowGui gui; 16 | private Logger logger; 17 | 18 | private void parseArgs(String[] args) { 19 | for (String arg : args) { 20 | if (arg.equals("-debug")) { 21 | debug = true; 22 | } 23 | } 24 | } 25 | 26 | public static boolean debug() { 27 | return app.debug; 28 | } 29 | 30 | public static WindowGui getApp() { 31 | return app.gui; 32 | } 33 | 34 | @SuppressFBWarnings("DM_EXIT") 35 | public static void quit() { 36 | app.logger.info("Application terminated."); 37 | System.exit(0); 38 | } 39 | 40 | public static void main(final String[] args) { 41 | try { 42 | app.parseArgs(args); 43 | 44 | app.logger = LoggerFactory.getLogger(Application.class); 45 | 46 | app.gui = new WindowGui(); 47 | app.gui.initialize(); 48 | 49 | final Injector injector = Guice.createInjector(new ApplicationModule()); 50 | final Engine engine = injector.getInstance(Engine.class); 51 | engine.run(); 52 | 53 | } catch (final Throwable throwable) { 54 | app.logger.error("Critical error.", throwable); 55 | System.exit(1); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/ApplicationModule.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing; 2 | 3 | import com.fatico.winthing.messaging.MessagingModule; 4 | import com.fatico.winthing.windows.WindowsModule; 5 | import com.google.gson.Gson; 6 | import com.google.inject.AbstractModule; 7 | import com.google.inject.Provides; 8 | import com.google.inject.Singleton; 9 | import com.typesafe.config.Config; 10 | import com.typesafe.config.ConfigFactory; 11 | import com.typesafe.config.ConfigParseOptions; 12 | import com.typesafe.config.ConfigSyntax; 13 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 14 | import java.io.File; 15 | import java.util.Locale; 16 | 17 | public class ApplicationModule extends AbstractModule { 18 | public static final String ConfigFile = "winthing.conf"; 19 | 20 | @Override 21 | protected void configure() { 22 | bind(Gson.class).in(Singleton.class); 23 | 24 | install(new MessagingModule()); 25 | if (System.getProperty("os.name").toLowerCase(Locale.getDefault()).contains("win")) { 26 | install(new WindowsModule()); 27 | install(new com.fatico.winthing.systems.system.Module()); 28 | install(new com.fatico.winthing.systems.keyboard.Module()); 29 | install(new com.fatico.winthing.systems.desktop.Module()); 30 | } 31 | } 32 | 33 | @Provides 34 | @Singleton 35 | @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT") 36 | Config config() { 37 | Config cfg = ConfigFactory.load(); 38 | String path = System.getProperty("user.dir") + File.separator + ConfigFile; 39 | 40 | File fp = new File(path); 41 | if (fp.exists()) { 42 | ConfigParseOptions options = ConfigParseOptions.defaults(); 43 | options.setSyntax(ConfigSyntax.CONF); 44 | 45 | cfg = ConfigFactory.parseFile(fp, options).withFallback(cfg); 46 | } 47 | 48 | return cfg; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/Settings.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing; 2 | 3 | public abstract class Settings { 4 | 5 | public static final String BROKER_URL = "broker"; 6 | public static final String BROKER_USERNAME = "username"; 7 | public static final String BROKER_PASSWORD = "password"; 8 | public static final String CLIENT_ID = "clientid"; 9 | public static final String TOPIC_PREFIX = "prefix"; 10 | public static final String RECONNECT_INTERVAL = "reconnect"; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/common/BaseController.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.common; 2 | 3 | import com.fatico.winthing.messaging.Message; 4 | import com.fatico.winthing.messaging.QualityOfService; 5 | import com.google.gson.JsonElement; 6 | import java.util.Objects; 7 | 8 | public class BaseController { 9 | 10 | protected final String prefix; 11 | 12 | public BaseController(String prefix) { 13 | Objects.requireNonNull(prefix); 14 | prefix = prefix.replaceFirst("^/+", ""); 15 | if (!prefix.isEmpty() && !prefix.endsWith("/")) { 16 | prefix += "/"; 17 | } 18 | this.prefix = prefix; 19 | } 20 | 21 | protected Message makeMessage(final String topic) { 22 | return new Message(prefix + topic); 23 | } 24 | 25 | protected Message makeMessage(final String topic, final JsonElement payload) { 26 | return new Message(prefix + topic, payload); 27 | } 28 | 29 | protected Message makeMessage(final String topic, final JsonElement payload, 30 | final QualityOfService qos) { 31 | return new Message(prefix + topic, payload, qos); 32 | } 33 | 34 | protected Message makeMessage(final String topic, final JsonElement payload, 35 | final QualityOfService qos, boolean retained) { 36 | return new Message(prefix + topic, payload, qos, retained); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/gui/WindowGui.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.gui; 2 | 3 | import com.fatico.winthing.Application; 4 | import com.fatico.winthing.logging.ConsoleLogger; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import java.awt.AWTException; 7 | import java.awt.BorderLayout; 8 | import java.awt.Component; 9 | import java.awt.Dimension; 10 | import java.awt.FlowLayout; 11 | import java.awt.Image; 12 | import java.awt.MenuItem; 13 | import java.awt.PopupMenu; 14 | import java.awt.SystemTray; 15 | import java.awt.Toolkit; 16 | import java.awt.TrayIcon; 17 | import java.awt.event.ActionEvent; 18 | import java.awt.event.ActionListener; 19 | import java.awt.event.MouseEvent; 20 | import java.awt.event.MouseListener; 21 | import java.net.URL; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import javax.swing.JButton; 25 | import javax.swing.JFrame; 26 | import javax.swing.JPanel; 27 | import javax.swing.JScrollPane; 28 | import javax.swing.JTextArea; 29 | 30 | @SuppressWarnings("serial") 31 | public class WindowGui extends JFrame { 32 | private final String appName; 33 | private final String appVersion; 34 | private Map components = new HashMap(); 35 | 36 | public enum Gui { 37 | TEXTAREA(0), 38 | SCROLLBAR(1), 39 | TRAYICON(2); 40 | 41 | public final int key; 42 | 43 | Gui(int value) { 44 | key = value; 45 | } 46 | } 47 | 48 | public enum Actions { 49 | EXIT, 50 | EVENTS, 51 | CLOSE, 52 | QUIT 53 | } 54 | 55 | public WindowGui() { 56 | appName = Application.class.getPackage().getImplementationTitle(); 57 | appVersion = Application.class.getPackage().getImplementationVersion(); 58 | } 59 | 60 | public void initialize() throws AWTException { 61 | setTitle(appName + " " + appVersion); 62 | setDefaultCloseOperation(HIDE_ON_CLOSE); 63 | setVisible(false); 64 | setLayout(new BorderLayout()); 65 | setResizable(false); 66 | setPreferredSize(new Dimension(800, 530)); 67 | setSize(getPreferredSize()); 68 | 69 | JTextArea logs = new JTextArea(); 70 | logs.setEditable(false); 71 | logs.setRows(25); 72 | components.put(Gui.TEXTAREA.key, logs); 73 | 74 | JScrollPane scrolls = new JScrollPane(logs); 75 | scrolls.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); 76 | getContentPane().add(scrolls, BorderLayout.NORTH); 77 | components.put(Gui.SCROLLBAR.key, scrolls); 78 | 79 | JPanel panel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 80 | getContentPane().add(panel, BorderLayout.SOUTH); 81 | 82 | JButton btnClose = new JButton("Close"); 83 | btnClose.setToolTipText("Close this window"); 84 | btnClose.addMouseListener(new MouseEventListener(this, Actions.CLOSE)); 85 | panel.add(btnClose); 86 | 87 | JButton btnQuit = new JButton("Quit"); 88 | btnQuit.setToolTipText("Stop and terminate application"); 89 | btnQuit.addMouseListener(new MouseEventListener(this, Actions.QUIT)); 90 | panel.add(btnQuit); 91 | 92 | TrayIcon trayIcon = null; 93 | if (SystemTray.isSupported()) { 94 | PopupMenu popup = new PopupMenu(); 95 | 96 | MenuItem menuTitle = new MenuItem(appName); 97 | menuTitle.setEnabled(false); 98 | popup.add(menuTitle); 99 | 100 | popup.addSeparator(); 101 | 102 | MenuItem menuEvent = new MenuItem("Events"); 103 | menuEvent.addActionListener(new MouseEventListener(this, Actions.EVENTS)); 104 | popup.add(menuEvent); 105 | 106 | MenuItem menuExit = new MenuItem("Exit"); 107 | menuExit.addActionListener(new MouseEventListener(this, Actions.EXIT)); 108 | popup.add(menuExit); 109 | 110 | SystemTray tray = SystemTray.getSystemTray(); 111 | 112 | URL url = getClass().getClassLoader().getResource("favicon-red.png"); 113 | Image image = Toolkit.getDefaultToolkit().getImage(url); 114 | 115 | int trayWidth = tray.getTrayIconSize().width; 116 | int trayheight = tray.getTrayIconSize().height; 117 | Image scaled = image.getScaledInstance(trayWidth, trayheight, Image.SCALE_SMOOTH); 118 | 119 | trayIcon = new TrayIcon(scaled, appName + " " + appVersion, popup); 120 | trayIcon.addMouseListener(new MouseEventListener(this, Actions.EVENTS)); 121 | tray.add(trayIcon); 122 | } 123 | } 124 | 125 | public void setIcon(boolean color) { 126 | SystemTray tray = SystemTray.getSystemTray(); 127 | TrayIcon[] icons = tray.getTrayIcons(); 128 | if (icons.length > 0) { 129 | String name = color ? "favicon-green.png" : "favicon-red.png"; 130 | 131 | URL url = getClass().getClassLoader().getResource(name); 132 | Image image = Toolkit.getDefaultToolkit().getImage(url); 133 | 134 | int trayWidth = tray.getTrayIconSize().width; 135 | int trayHeight = tray.getTrayIconSize().height; 136 | Image scaled = image.getScaledInstance(trayWidth, trayHeight, Image.SCALE_SMOOTH); 137 | icons[0].setImage(scaled); 138 | } 139 | } 140 | 141 | public void reload() { 142 | if (isVisible()) { 143 | JTextArea area = (JTextArea)components.get(Gui.TEXTAREA.key); 144 | area.setText(ConsoleLogger.getEvents()); 145 | 146 | JScrollPane scroll = (JScrollPane)components.get(Gui.SCROLLBAR.key); 147 | scroll.getVerticalScrollBar().setValue(scroll.getVerticalScrollBar().getMaximum()); 148 | } 149 | } 150 | 151 | public void open() { 152 | pack(); 153 | setLocationRelativeTo(null); 154 | setVisible(true); 155 | 156 | reload(); 157 | } 158 | 159 | public void close() { 160 | setVisible(false); 161 | } 162 | 163 | public void quit() { 164 | Application.quit(); 165 | } 166 | 167 | @SuppressFBWarnings("SIC_INNER_SHOULD_BE_STATIC") 168 | public class MouseEventListener implements ActionListener, MouseListener { 169 | private Actions action; 170 | private WindowGui window; 171 | 172 | public MouseEventListener(WindowGui gui, Actions act) { 173 | window = gui; 174 | action = act; 175 | } 176 | 177 | @Override 178 | public void actionPerformed(ActionEvent event) { 179 | if (action == WindowGui.Actions.EXIT) { 180 | window.quit(); 181 | } 182 | 183 | if (action == WindowGui.Actions.EVENTS) { 184 | window.open(); 185 | } 186 | } 187 | 188 | @Override 189 | public void mouseClicked(MouseEvent event) { 190 | if (event.getButton() == MouseEvent.BUTTON1) { 191 | switch (action) { 192 | default: 193 | break; 194 | 195 | case EVENTS: 196 | window.open(); 197 | break; 198 | 199 | case CLOSE: 200 | window.close(); 201 | break; 202 | 203 | case QUIT: 204 | window.quit(); 205 | break; 206 | } 207 | } 208 | } 209 | 210 | @Override 211 | public void mousePressed(MouseEvent event) { 212 | // no code here 213 | } 214 | 215 | @Override 216 | public void mouseReleased(MouseEvent event) { 217 | // no code here 218 | } 219 | 220 | @Override 221 | public void mouseEntered(MouseEvent event) { 222 | // no code here 223 | } 224 | 225 | @Override 226 | public void mouseExited(MouseEvent event) { 227 | // no code here 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/logging/ConsoleLogger.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.logging; 2 | 3 | import ch.qos.logback.classic.spi.ILoggingEvent; 4 | import ch.qos.logback.core.ConsoleAppender; 5 | import com.fatico.winthing.Application; 6 | import java.nio.charset.Charset; 7 | import java.util.Iterator; 8 | import java.util.StringJoiner; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | 11 | public class ConsoleLogger extends ConsoleAppender { 12 | private static final int LOG_SIZE = 50; 13 | private static final ConcurrentLinkedQueue events = new ConcurrentLinkedQueue(); 14 | 15 | public static String getEvents() { 16 | if (events.size() == 0) { 17 | return ""; 18 | } 19 | 20 | StringJoiner joiner = new StringJoiner(""); 21 | Iterator iterator = events.iterator(); 22 | while (iterator.hasNext()) { 23 | joiner.add(iterator.next()); 24 | } 25 | 26 | String result = joiner.toString(); 27 | 28 | return result; 29 | } 30 | 31 | protected void append(ILoggingEvent event) { 32 | super.append(event); 33 | 34 | if (events.size() > LOG_SIZE) { 35 | events.remove(); 36 | } 37 | 38 | byte[] data = encoder.encode(event); 39 | events.add(new String(data, Charset.forName("UTF-8"))); 40 | 41 | Application.getApp().reload(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/logging/FileLogEnabler.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.logging; 2 | 3 | import ch.qos.logback.core.PropertyDefinerBase; 4 | import com.fatico.winthing.Application; 5 | 6 | public class FileLogEnabler extends PropertyDefinerBase { 7 | 8 | @Override 9 | public String getPropertyValue() { 10 | if (Application.debug()) { 11 | return "true"; 12 | } 13 | 14 | return "false"; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/Engine.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import com.fatico.winthing.Application; 4 | import com.fatico.winthing.Settings; 5 | import com.google.common.base.Charsets; 6 | import com.google.gson.Gson; 7 | import com.google.gson.JsonElement; 8 | import com.google.gson.JsonSyntaxException; 9 | import com.google.inject.Inject; 10 | import com.typesafe.config.Config; 11 | import java.nio.charset.Charset; 12 | import java.time.Duration; 13 | import java.util.Collection; 14 | import java.util.Map; 15 | import java.util.Objects; 16 | import java.util.concurrent.locks.Condition; 17 | import java.util.concurrent.locks.Lock; 18 | import java.util.concurrent.locks.ReentrantLock; 19 | import java.util.function.Consumer; 20 | import org.eclipse.paho.client.mqttv3.IMqttAsyncClient; 21 | import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; 22 | import org.eclipse.paho.client.mqttv3.MqttAsyncClient; 23 | import org.eclipse.paho.client.mqttv3.MqttCallback; 24 | import org.eclipse.paho.client.mqttv3.MqttClientPersistence; 25 | import org.eclipse.paho.client.mqttv3.MqttConnectOptions; 26 | import org.eclipse.paho.client.mqttv3.MqttException; 27 | import org.eclipse.paho.client.mqttv3.MqttMessage; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | public class Engine implements MqttCallback, MessagePublisher { 32 | 33 | private static final Charset CHARSET = Charsets.UTF_8; 34 | 35 | private final Logger logger = LoggerFactory.getLogger(getClass()); 36 | 37 | private final Gson gson; 38 | private final Registry registry; 39 | private final String topicPrefix; 40 | private final IMqttAsyncClient client; 41 | private final MqttConnectOptions options = new MqttConnectOptions(); 42 | private final Duration reconnectInterval; 43 | 44 | private final Lock runnningLock = new ReentrantLock(); 45 | private final Condition runningCondition = runnningLock.newCondition(); 46 | 47 | @Inject 48 | public Engine(final Gson gson, final Registry registry, final Config config, 49 | final MqttClientPersistence persistence) throws MqttException { 50 | String topicPrefix = config.getString(Settings.TOPIC_PREFIX).replaceFirst("^/+", ""); 51 | if (!topicPrefix.isEmpty() && !topicPrefix.endsWith("/")) { 52 | topicPrefix += "/"; 53 | } 54 | this.topicPrefix = topicPrefix; 55 | 56 | this.reconnectInterval = Duration.ofSeconds(config.getLong(Settings.RECONNECT_INTERVAL)); 57 | 58 | this.gson = Objects.requireNonNull(gson); 59 | this.registry = Objects.requireNonNull(registry); 60 | 61 | this.client = new MqttAsyncClient( 62 | "tcp://" + config.getString(Settings.BROKER_URL), 63 | config.getString(Settings.CLIENT_ID), 64 | persistence 65 | ); 66 | this.client.setCallback(this); 67 | 68 | { 69 | final String username = config.getString(Settings.BROKER_USERNAME); 70 | if (username != null && !username.isEmpty()) { 71 | this.options.setUserName(username); 72 | } 73 | } 74 | { 75 | final String password = config.getString(Settings.BROKER_PASSWORD); 76 | if (password != null && !password.isEmpty()) { 77 | this.options.setPassword(password.toCharArray()); 78 | } 79 | } 80 | 81 | this.options.setCleanSession(true); 82 | } 83 | 84 | public void run() { 85 | runnningLock.lock(); 86 | try { 87 | while (true) { 88 | boolean connected = false; 89 | try { 90 | connect(); 91 | connected = true; 92 | } catch (final MqttException exception) { 93 | logger.error("Could not connect: {}", exception.getMessage()); 94 | } 95 | if (connected) { 96 | try { 97 | runningCondition.await(); 98 | } catch (final InterruptedException exception) { 99 | try { 100 | disconnect(); 101 | } catch (final MqttException disconnectException) { 102 | logger.error("Could not disconnect.", disconnectException); 103 | } 104 | return; 105 | } 106 | } 107 | logger.info( 108 | "Trying to reconnect in {} seconds...", 109 | reconnectInterval.getSeconds() 110 | ); 111 | try { 112 | Thread.sleep(reconnectInterval.toMillis()); 113 | } catch (final InterruptedException exception) { 114 | return; 115 | } 116 | } 117 | } finally { 118 | runnningLock.unlock(); 119 | } 120 | } 121 | 122 | private void connect() throws MqttException { 123 | if (registry.getWill().isPresent()) { 124 | final Message will = registry.getWill().get(); 125 | final MqttMessage mqttMessage = serialize(will); 126 | this.options.setWill( 127 | topicPrefix + will.getTopic(), 128 | mqttMessage.getPayload(), 129 | mqttMessage.getQos(), 130 | mqttMessage.isRetained() 131 | ); 132 | } 133 | 134 | logger.info("Connecting to {} as {}...", client.getServerURI(), client.getClientId()); 135 | client.connect(options).waitForCompletion(); 136 | logger.info("Connected."); 137 | 138 | logger.info("Subscribing to topics..."); 139 | for (final Map.Entry entry 140 | : registry.getSubscriptions().entrySet()) { 141 | logger.info(" - {}", topicPrefix + entry.getKey()); 142 | client.subscribe(topicPrefix + entry.getKey(), entry.getValue().ordinal()); 143 | } 144 | logger.info("Subscribed."); 145 | 146 | logger.info("Sending initial messages..."); 147 | registry.getInitialMessages().stream().forEach(this::publish); 148 | 149 | logger.info("Engine started."); 150 | 151 | Application.getApp().setIcon(true); 152 | } 153 | 154 | private void disconnect() throws MqttException { 155 | Application.getApp().setIcon(false); 156 | 157 | client.disconnect(); 158 | } 159 | 160 | @Override 161 | public void publish(final Message message) { 162 | final MqttMessage mqttMessage = serialize(message); 163 | try { 164 | client.publish( 165 | topicPrefix + message.getTopic(), 166 | mqttMessage 167 | ); 168 | } catch (final MqttException exception) { 169 | logger.error("Error while publishing message.", exception); 170 | } 171 | } 172 | 173 | @Override 174 | public void connectionLost(final Throwable throwable) { 175 | Application.getApp().setIcon(false); 176 | 177 | logger.error("Connection lost."); 178 | runnningLock.lock(); 179 | try { 180 | runningCondition.signal(); 181 | } finally { 182 | runnningLock.unlock(); 183 | } 184 | } 185 | 186 | @Override 187 | public void messageArrived(final String topic, final MqttMessage mqttMessage) throws Exception { 188 | try { 189 | handleMessage(topic, mqttMessage); 190 | } catch (final Throwable throwable) { 191 | logger.error( 192 | "Error while handling message " + topic 193 | + "(" + new String(mqttMessage.getPayload(), CHARSET) + "): " 194 | + throwable.getMessage(), 195 | throwable 196 | ); 197 | } 198 | } 199 | 200 | @Override 201 | public void deliveryComplete(final IMqttDeliveryToken token) { 202 | // Do nothing. 203 | } 204 | 205 | private void handleMessage(String topic, final MqttMessage mqttMessage) throws Exception { 206 | if (!topic.startsWith(topicPrefix)) { 207 | return; 208 | } 209 | topic = topic.substring(topicPrefix.length()); 210 | 211 | final Collection> consumers = registry.getConsumers(topic); 212 | if (consumers.isEmpty()) { 213 | return; 214 | } 215 | 216 | final byte[] payloadBytes = mqttMessage.getPayload(); 217 | final JsonElement payload; 218 | if (payloadBytes.length == 0) { 219 | payload = null; 220 | } else { 221 | try { 222 | payload = gson.fromJson(new String(payloadBytes, CHARSET), JsonElement.class); 223 | } catch (final JsonSyntaxException exception) { 224 | logger.error("Invalid JSON received for: {}", topic); 225 | return; 226 | } 227 | } 228 | 229 | final Message message = new Message( 230 | topic, 231 | payload, 232 | QualityOfService.values()[mqttMessage.getQos()], 233 | mqttMessage.isRetained() 234 | ); 235 | 236 | logger.debug( 237 | "Received: {}({})", 238 | message.getTopic(), 239 | message.getPayload().isPresent() ? message.getPayload().get().toString() : "" 240 | ); 241 | 242 | for (final Consumer consumer : consumers) { 243 | try { 244 | consumer.accept(message); 245 | } catch (final Exception exception) { 246 | logger.error( 247 | "Error while processing {}({}): {}", 248 | message.getTopic(), 249 | message.getPayload().isPresent() 250 | ? message.getPayload().get().toString() : "", 251 | exception.getMessage() 252 | ); 253 | } 254 | } 255 | } 256 | 257 | private MqttMessage serialize(final Message message) { 258 | final byte[] payload; 259 | if (message.getPayload().isPresent()) { 260 | payload = gson.toJson(message.getPayload().get()).getBytes(CHARSET); 261 | } else { 262 | payload = new byte[0]; 263 | } 264 | final MqttMessage mqttMessage = new MqttMessage(payload); 265 | mqttMessage.setQos(message.getQualityOfService().ordinal()); 266 | mqttMessage.setRetained(message.isRetained()); 267 | return mqttMessage; 268 | } 269 | 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/Message.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import com.google.gson.JsonElement; 4 | import java.util.Objects; 5 | import java.util.Optional; 6 | 7 | public class Message { 8 | 9 | private static final JsonElement DEFAULT_PAYLOAD = null; 10 | private static final QualityOfService DEFAULT_QOS = QualityOfService.AT_MOST_ONCE; 11 | private static final boolean DEFAULT_RETAINMENT = false; 12 | 13 | private final String topic; 14 | private final JsonElement payload; 15 | private final QualityOfService qos; 16 | private final boolean retained; 17 | 18 | public Message(final String topic) { 19 | this.topic = Objects.requireNonNull(topic); 20 | this.payload = DEFAULT_PAYLOAD; 21 | this.qos = DEFAULT_QOS; 22 | this.retained = DEFAULT_RETAINMENT; 23 | } 24 | 25 | public Message(final String topic, final JsonElement payload) { 26 | this.topic = Objects.requireNonNull(topic); 27 | this.payload = payload; 28 | this.qos = DEFAULT_QOS; 29 | this.retained = DEFAULT_RETAINMENT; 30 | } 31 | 32 | public Message(final String topic, final JsonElement payload, final QualityOfService qos) { 33 | this.topic = Objects.requireNonNull(topic); 34 | this.payload = payload; 35 | this.qos = Objects.requireNonNull(qos); 36 | this.retained = DEFAULT_RETAINMENT; 37 | } 38 | 39 | public Message(final String topic, final JsonElement payload, final QualityOfService qos, 40 | boolean retained) { 41 | this.topic = Objects.requireNonNull(topic); 42 | this.payload = payload; 43 | this.qos = Objects.requireNonNull(qos); 44 | this.retained = retained; 45 | } 46 | 47 | public String getTopic() { 48 | return topic; 49 | } 50 | 51 | public Optional getPayload() { 52 | return Optional.ofNullable(payload); 53 | } 54 | 55 | public QualityOfService getQualityOfService() { 56 | return qos; 57 | } 58 | 59 | public boolean isRetained() { 60 | return retained; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/MessagePublisher.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | public interface MessagePublisher { 4 | 5 | void publish(Message message); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/MessagingModule.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import com.google.inject.PrivateModule; 4 | import com.google.inject.Singleton; 5 | import org.eclipse.paho.client.mqttv3.MqttClientPersistence; 6 | import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; 7 | 8 | public class MessagingModule extends PrivateModule { 9 | 10 | @Override 11 | protected void configure() { 12 | bind(MqttClientPersistence.class).to(MemoryPersistence.class).in(Singleton.class); 13 | 14 | bind(Registry.class).to(SimpleRegistry.class).in(Singleton.class); 15 | 16 | bind(Engine.class).in(Singleton.class); 17 | bind(MessagePublisher.class).to(Engine.class); 18 | 19 | expose(Registry.class); 20 | expose(MessagePublisher.class); 21 | expose(Engine.class); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/QualityOfService.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | public enum QualityOfService { 4 | 5 | AT_MOST_ONCE, 6 | AT_LEAST_ONCE, 7 | EXACTLY_ONCE; 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/Registry.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | import java.util.Optional; 6 | import java.util.Queue; 7 | import java.util.function.Consumer; 8 | 9 | public interface Registry extends RegistryConfigurator { 10 | 11 | Map getSubscriptions(); 12 | 13 | Collection> getConsumers(String topic); 14 | 15 | Queue getInitialMessages(); 16 | 17 | Optional getWill(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/RegistryConfigurator.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public interface RegistryConfigurator { 6 | 7 | void subscribe(final String topic, final Consumer consumer); 8 | 9 | void subscribe(final String topic, final Consumer consumer, 10 | final QualityOfService qos); 11 | 12 | void queueInitialMessage(final Message message); 13 | 14 | void setWill(final Message message); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/messaging/SimpleRegistry.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.messaging; 2 | 3 | import com.google.common.collect.HashMultimap; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.google.common.collect.Multimap; 6 | import java.util.Collection; 7 | import java.util.HashMap; 8 | import java.util.LinkedList; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | import java.util.Queue; 12 | import java.util.function.Consumer; 13 | 14 | class SimpleRegistry implements Registry { 15 | 16 | private static final QualityOfService DEFAULT_QOS = QualityOfService.EXACTLY_ONCE; 17 | 18 | private final Map topicQos = new HashMap<>(); 19 | private final Multimap> consumers = HashMultimap.create(); 20 | private final Queue initialMessages = new LinkedList<>(); 21 | private Message will = null; 22 | 23 | @Override 24 | public void subscribe(final String topic, final Consumer consumer) { 25 | subscribe(topic, consumer, DEFAULT_QOS); 26 | } 27 | 28 | @Override 29 | public void subscribe(final String topic, final Consumer consumer, 30 | final QualityOfService qos) { 31 | topicQos.compute(topic, (currentTopic, currentQos) -> { 32 | if (currentQos == null || qos.compareTo(currentQos) > 0) { 33 | return qos; 34 | } else { 35 | return currentQos; 36 | } 37 | }); 38 | consumers.put(topic, consumer); 39 | } 40 | 41 | @Override 42 | public Map getSubscriptions() { 43 | return ImmutableMap.copyOf(topicQos); 44 | } 45 | 46 | @Override 47 | public Collection> getConsumers(final String topic) { 48 | return consumers.get(topic); 49 | } 50 | 51 | @Override 52 | public void queueInitialMessage(final Message message) { 53 | initialMessages.add(message); 54 | } 55 | 56 | @Override 57 | public Queue getInitialMessages() { 58 | return initialMessages; 59 | } 60 | 61 | @Override 62 | public void setWill(final Message message) { 63 | this.will = message; 64 | } 65 | 66 | @Override 67 | public Optional getWill() { 68 | return Optional.ofNullable(will); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/desktop/DesktopController.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.desktop; 2 | 3 | import com.fatico.winthing.common.BaseController; 4 | import com.fatico.winthing.messaging.Message; 5 | import com.fatico.winthing.messaging.Registry; 6 | import com.google.inject.Inject; 7 | import java.util.NoSuchElementException; 8 | import java.util.Objects; 9 | 10 | public class DesktopController extends BaseController { 11 | 12 | private final DesktopService desktopService; 13 | 14 | @Inject 15 | public DesktopController(final Registry registry, final DesktopService desktopService) { 16 | super("desktop"); 17 | this.desktopService = Objects.requireNonNull(desktopService); 18 | registry.subscribe(prefix + "commands/close_active_window", this::closeActiveWindow); 19 | registry.subscribe(prefix + "commands/set_display_sleep", this::setDisplaySleep); 20 | } 21 | 22 | public void closeActiveWindow(final Message message) { 23 | desktopService.getForegroundWindow().ifPresent(desktopService::closeWindow); 24 | } 25 | 26 | public void setDisplaySleep(final Message message) { 27 | final boolean sleep; 28 | try { 29 | sleep = message.getPayload().get().getAsBoolean(); 30 | } catch (final NoSuchElementException | IllegalStateException exception) { 31 | throw new IllegalArgumentException("Invalid arguments."); 32 | } 33 | desktopService.setDisplaySleep(sleep); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/desktop/DesktopService.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.desktop; 2 | 3 | import com.fatico.winthing.windows.jna.User32; 4 | import com.google.inject.Inject; 5 | import com.sun.jna.Pointer; 6 | import com.sun.jna.platform.win32.WinDef; 7 | import com.sun.jna.platform.win32.WinUser; 8 | import com.sun.jna.ptr.IntByReference; 9 | import java.util.Objects; 10 | import java.util.Optional; 11 | 12 | public class DesktopService { 13 | 14 | private static final long SC_MONITORPOWER = 0xF170; 15 | 16 | private final User32 user32; 17 | 18 | @Inject 19 | public DesktopService(final User32 user32) { 20 | this.user32 = Objects.requireNonNull(user32); 21 | } 22 | 23 | public Optional getForegroundWindow() { 24 | return Optional.ofNullable(user32.GetForegroundWindow()); 25 | } 26 | 27 | public void setForegroundWindow(final WinDef.HWND window) { 28 | user32.SetForegroundWindow(window); 29 | } 30 | 31 | public void closeWindow(final WinDef.HWND window) { 32 | user32.SendMessage(window, WinUser.WM_CLOSE, null, null); 33 | } 34 | 35 | public Optional getMainWindow(final int process) { 36 | class Callback implements WinUser.WNDENUMPROC { 37 | public WinDef.HWND foundWindow = null; 38 | 39 | @Override 40 | public boolean callback(final WinDef.HWND hwnd, final Pointer pointer) { 41 | final IntByReference processIdReference = new IntByReference(); 42 | user32.GetWindowThreadProcessId(hwnd, processIdReference); 43 | if (processIdReference.getValue() == process) { 44 | foundWindow = hwnd; 45 | return false; 46 | } 47 | return true; 48 | } 49 | } 50 | 51 | final Callback callback = new Callback(); 52 | user32.EnumWindows(callback, null); 53 | return Optional.ofNullable(callback.foundWindow); 54 | } 55 | 56 | public void setDisplaySleep(final boolean sleep) { 57 | user32.DefWindowProc( 58 | getForegroundWindow().get(), 59 | WinUser.WM_SYSCOMMAND, 60 | new WinDef.WPARAM(SC_MONITORPOWER), 61 | new WinDef.LPARAM(sleep ? 2 : -1) 62 | ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/desktop/Module.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.desktop; 2 | 3 | import com.google.inject.PrivateModule; 4 | import com.google.inject.Singleton; 5 | 6 | public class Module extends PrivateModule { 7 | 8 | @Override 9 | protected void configure() { 10 | bind(DesktopService.class).in(Singleton.class); 11 | bind(DesktopController.class).asEagerSingleton(); 12 | expose(DesktopService.class); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/keyboard/KeyboardController.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.keyboard; 2 | 3 | import com.fatico.winthing.common.BaseController; 4 | import com.fatico.winthing.messaging.Message; 5 | import com.fatico.winthing.messaging.Registry; 6 | import com.fatico.winthing.windows.input.KeyboardKey; 7 | import com.google.gson.JsonArray; 8 | import com.google.gson.JsonElement; 9 | import com.google.inject.Inject; 10 | import java.util.ArrayList; 11 | import java.util.List; 12 | import java.util.NoSuchElementException; 13 | import java.util.Objects; 14 | 15 | public class KeyboardController extends BaseController { 16 | 17 | private final KeyboardService keyboardService; 18 | 19 | @Inject 20 | public KeyboardController(final Registry registry, final KeyboardService keyboardService) { 21 | super("keyboard"); 22 | this.keyboardService = Objects.requireNonNull(keyboardService); 23 | registry.subscribe(prefix + "commands/press_keys", this::pressKeys); 24 | } 25 | 26 | public void pressKeys(final Message message) { 27 | final List keys; 28 | try { 29 | final JsonArray arguments = message.getPayload().get().getAsJsonArray(); 30 | keys = new ArrayList<>(arguments.size()); 31 | for (final JsonElement element : arguments) { 32 | try { 33 | keys.add(KeyboardKey.getByCodename(element.getAsString())); 34 | } catch (final NoSuchElementException exception) { 35 | throw new IllegalArgumentException("Unknown key: " + element.getAsString()); 36 | } 37 | } 38 | } catch (final NoSuchElementException | IllegalStateException exception) { 39 | throw new IllegalArgumentException("Invalid arguments."); 40 | } 41 | keyboardService.pressKeys(keys); 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/keyboard/KeyboardService.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.keyboard; 2 | 3 | import com.fatico.winthing.windows.input.KeyboardKey; 4 | import com.fatico.winthing.windows.jna.User32; 5 | import com.google.inject.Inject; 6 | import com.sun.jna.platform.win32.WinDef; 7 | import com.sun.jna.platform.win32.WinUser; 8 | import java.util.List; 9 | import java.util.ListIterator; 10 | import java.util.Objects; 11 | 12 | public class KeyboardService { 13 | 14 | private final User32 user32; 15 | 16 | @Inject 17 | public KeyboardService(final User32 user32) { 18 | this.user32 = Objects.requireNonNull(user32); 19 | } 20 | 21 | public void pressKeys(final List keys) { 22 | if (keys.isEmpty()) { 23 | return; 24 | } 25 | 26 | final WinUser.INPUT input = new WinUser.INPUT(); 27 | final WinUser.INPUT[] inputs = (WinUser.INPUT[]) input.toArray(keys.size() * 2); 28 | 29 | final ListIterator iterator = keys.listIterator(); 30 | int index = 0; 31 | while (iterator.hasNext()) { 32 | setKeyDown(inputs[index], iterator.next()); 33 | index++; 34 | } 35 | while (iterator.hasPrevious()) { 36 | setKeyUp(inputs[index], iterator.previous()); 37 | index++; 38 | } 39 | 40 | user32.SendInput(new WinDef.DWORD(inputs.length), inputs, inputs[0].size()); 41 | } 42 | 43 | private void setKeyDown(final WinUser.INPUT input, final KeyboardKey key) { 44 | input.type.setValue(WinUser.INPUT.INPUT_KEYBOARD); 45 | input.input.setType(WinUser.KEYBDINPUT.class); 46 | input.input.ki.wVk = key.getVirtualKeyCode(); 47 | } 48 | 49 | private void setKeyUp(final WinUser.INPUT input, final KeyboardKey key) { 50 | input.type.setValue(WinUser.INPUT.INPUT_KEYBOARD); 51 | input.input.setType(WinUser.KEYBDINPUT.class); 52 | input.input.ki.dwFlags.setValue(WinUser.KEYBDINPUT.KEYEVENTF_KEYUP); 53 | input.input.ki.wVk = key.getVirtualKeyCode(); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/keyboard/Module.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.keyboard; 2 | 3 | import com.google.inject.PrivateModule; 4 | import com.google.inject.Singleton; 5 | 6 | public class Module extends PrivateModule { 7 | 8 | @Override 9 | protected void configure() { 10 | bind(KeyboardService.class).in(Singleton.class); 11 | bind(KeyboardController.class).asEagerSingleton(); 12 | expose(KeyboardService.class); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/radeon/AdlException.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.radeon; 2 | 3 | import com.fatico.winthing.windows.SystemException; 4 | 5 | @SuppressWarnings("serial") 6 | public class AdlException extends SystemException { 7 | 8 | public AdlException(final String function, final int code) { 9 | super(function + " returned error code " + code + "."); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/radeon/Module.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.radeon; 2 | 3 | import com.fatico.winthing.systems.radeon.jna.AtiAdl; 4 | import com.google.inject.PrivateModule; 5 | import com.google.inject.Singleton; 6 | 7 | public class Module extends PrivateModule { 8 | 9 | @Override 10 | protected void configure() { 11 | bind(AtiAdl.class).toInstance(AtiAdl.INSTANCE); 12 | bind(RadeonService.class).in(Singleton.class); 13 | bind(RadeonController.class).asEagerSingleton(); 14 | expose(RadeonService.class); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/radeon/RadeonController.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.radeon; 2 | 3 | import com.fatico.winthing.common.BaseController; 4 | import com.fatico.winthing.messaging.Message; 5 | import com.fatico.winthing.messaging.Registry; 6 | import com.google.gson.JsonArray; 7 | import com.google.inject.Inject; 8 | import java.util.NoSuchElementException; 9 | import java.util.Objects; 10 | 11 | public class RadeonController extends BaseController { 12 | 13 | private final RadeonService radeonService; 14 | 15 | @Inject 16 | public RadeonController(final Registry registry, final RadeonService radeonService) { 17 | super("radeon"); 18 | this.radeonService = Objects.requireNonNull(radeonService); 19 | registry.subscribe(prefix + "commands/set_best_resolution", this::setBestResolution); 20 | registry.subscribe(prefix + "commands/set_resolution", this::setResolution); 21 | } 22 | 23 | public void setBestResolution(final Message message) { 24 | radeonService.setBestResolution(radeonService.getPrimaryAdapterIndex()); 25 | } 26 | 27 | public void setResolution(final Message message) { 28 | final int width; 29 | final int height; 30 | try { 31 | final JsonArray arguments = message.getPayload().get().getAsJsonArray(); 32 | width = arguments.get(0).getAsInt(); 33 | height = arguments.get(1).getAsInt(); 34 | } catch (final NoSuchElementException | IllegalStateException exception) { 35 | throw new IllegalArgumentException("Invalid arguments."); 36 | } 37 | radeonService.setResolution(radeonService.getPrimaryAdapterIndex(), width, height); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/radeon/RadeonService.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.radeon; 2 | 3 | import com.fatico.winthing.systems.radeon.jna.AtiAdl; 4 | import com.google.common.collect.ComparisonChain; 5 | import com.google.inject.Inject; 6 | import com.sun.jna.Memory; 7 | import com.sun.jna.Pointer; 8 | import com.sun.jna.ptr.IntByReference; 9 | import com.sun.jna.ptr.PointerByReference; 10 | import java.util.Arrays; 11 | import java.util.Collections; 12 | import java.util.NoSuchElementException; 13 | import java.util.Objects; 14 | 15 | @SuppressWarnings({"checkstyle:nofinalizer"}) 16 | public class RadeonService { 17 | 18 | private final AtiAdl atiAdl; 19 | private final Pointer context; 20 | 21 | @Inject 22 | public RadeonService(final AtiAdl atiAdl) { 23 | this.atiAdl = Objects.requireNonNull(atiAdl); 24 | { 25 | final PointerByReference contextReference = new PointerByReference(); 26 | final int result = atiAdl.ADL2_Main_Control_Create( 27 | new MallocCallback(), 28 | 1, 29 | contextReference 30 | ); 31 | if (result != AtiAdl.ADL_OK) { 32 | throw new AdlException("ADL2_Main_Control_Create", result); 33 | } 34 | this.context = contextReference.getValue(); 35 | } 36 | } 37 | 38 | @SuppressWarnings("deprecation") 39 | @Override 40 | protected void finalize() throws Throwable { 41 | atiAdl.ADL2_Main_Control_Destroy(context); 42 | super.finalize(); 43 | } 44 | 45 | public int getPrimaryAdapterIndex() { 46 | final IntByReference adapterIndexReference = new IntByReference(); 47 | final int result = atiAdl.ADL2_Adapter_Primary_Get( 48 | context, 49 | adapterIndexReference 50 | ); 51 | if (result != AtiAdl.ADL_OK) { 52 | throw new AdlException("ADL2_Display_CustomizedModeListNum_Get", result); 53 | } 54 | return adapterIndexReference.getValue(); 55 | } 56 | 57 | public void setBestResolution(final int adapterIndex) { 58 | final AtiAdl.ADLMode mode = getBestMode(adapterIndex); 59 | setMode(adapterIndex, mode); 60 | } 61 | 62 | public void setResolution(final int adapterIndex, final int width, final int height) { 63 | final AtiAdl.ADLMode mode = getBestMode(adapterIndex); 64 | mode.iXRes = width; 65 | mode.iYRes = height; 66 | setMode(adapterIndex, mode); 67 | } 68 | 69 | private void setMode(final int adapterIndex, final AtiAdl.ADLMode mode) { 70 | final int result = atiAdl.ADL2_Display_Modes_Set( 71 | context, 72 | adapterIndex, 73 | -1, 74 | 1, 75 | (AtiAdl.ADLMode[]) mode.toArray(1) 76 | ); 77 | if (result != AtiAdl.ADL_OK) { 78 | throw new AdlException("ADL2_Display_Modes_Set", result); 79 | } 80 | } 81 | 82 | private AtiAdl.ADLMode getBestMode(final int adapterIndex) { 83 | final AtiAdl.ADLMode[] modes; 84 | { 85 | final IntByReference numberOfModesReference = new IntByReference(); 86 | final PointerByReference pointer = new PointerByReference(); 87 | final int result = atiAdl.ADL2_Display_PossibleMode_Get( 88 | context, 89 | adapterIndex, 90 | numberOfModesReference, 91 | pointer 92 | ); 93 | if (result != AtiAdl.ADL_OK) { 94 | throw new AdlException("ADL2_Display_Modes_Get", result); 95 | } 96 | modes = (AtiAdl.ADLMode[]) new AtiAdl.ADLMode(pointer.getValue()).toArray( 97 | numberOfModesReference.getValue() 98 | ); 99 | } 100 | if (modes.length == 0) { 101 | throw new NoSuchElementException(); 102 | } 103 | return Collections.max(Arrays.asList(modes), (left, right) -> ComparisonChain.start() 104 | .compare(left.iColourDepth, right.iColourDepth) 105 | .compare(left.iXRes, right.iXRes) 106 | .compare(left.iYRes, right.iYRes) 107 | .compare(left.fRefreshRate, right.fRefreshRate) 108 | .result() 109 | ); 110 | } 111 | 112 | private static class MallocCallback extends Memory implements AtiAdl.ADL_MAIN_MALLOC_CALLBACK { 113 | @Override 114 | public Pointer invoke(int size) { 115 | return new Pointer(Memory.malloc(size)); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/radeon/jna/AtiAdl.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.radeon.jna; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.sun.jna.Callback; 5 | import com.sun.jna.Library; 6 | import com.sun.jna.Native; 7 | import com.sun.jna.Platform; 8 | import com.sun.jna.Pointer; 9 | import com.sun.jna.Structure; 10 | import com.sun.jna.ptr.IntByReference; 11 | import com.sun.jna.ptr.PointerByReference; 12 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 13 | import java.util.List; 14 | 15 | @SuppressWarnings({ 16 | "checkstyle:typename", 17 | "checkstyle:abbreviationaswordinname", 18 | "checkstyle:methodname", 19 | "checkstyle:membername", 20 | "checkstyle:parametername"}) 21 | @SuppressFBWarnings({"UUF_UNUSED_PUBLIC_OR_PROTECTED_FIELD", 22 | "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD"}) 23 | public interface AtiAdl extends Library { 24 | 25 | AtiAdl INSTANCE = (AtiAdl) Native.load( 26 | Platform.is64Bit() ? "atiadlxx" : "atiadlxy", 27 | AtiAdl.class 28 | ); 29 | 30 | int ADL_OK = 0; 31 | 32 | interface ADL_MAIN_MALLOC_CALLBACK extends Callback { 33 | Pointer invoke(int size); 34 | } 35 | 36 | class ADLDisplayID extends Structure { 37 | public int iDisplayLogicalIndex; 38 | public int iDisplayPhysicalIndex; 39 | public int iDisplayLogicalAdapterIndex; 40 | public int iDisplayPhysicalAdapterIndex; 41 | 42 | @Override 43 | protected List getFieldOrder() { 44 | return ImmutableList.of( 45 | "iDisplayLogicalIndex", 46 | "iDisplayPhysicalIndex", 47 | "iDisplayLogicalAdapterIndex", 48 | "iDisplayPhysicalAdapterIndex" 49 | ); 50 | } 51 | } 52 | 53 | class ADLMode extends Structure { 54 | public int iAdapterIndex; 55 | public ADLDisplayID displayID; 56 | public int iXPos; 57 | public int iYPos; 58 | public int iXRes; 59 | public int iYRes; 60 | public int iColourDepth; 61 | public float fRefreshRate; 62 | public int iOrientation; 63 | public int iModeFlag; 64 | public int iModeMask; 65 | public int iModeValue; 66 | 67 | public ADLMode(final Pointer pointer) { 68 | super(pointer); 69 | } 70 | 71 | @Override 72 | protected List getFieldOrder() { 73 | return ImmutableList.of( 74 | "iAdapterIndex", 75 | "displayID", 76 | "iXPos", 77 | "iYPos", 78 | "iXRes", 79 | "iYRes", 80 | "iColourDepth", 81 | "fRefreshRate", 82 | "iOrientation", 83 | "iModeFlag", 84 | "iModeMask", 85 | "iModeValue" 86 | ); 87 | } 88 | } 89 | 90 | int ADL2_Main_Control_Create( 91 | ADL_MAIN_MALLOC_CALLBACK callback, 92 | int iEnumConnectedAdapters, 93 | PointerByReference context 94 | ); 95 | 96 | int ADL2_Main_Control_Destroy(Pointer context); 97 | 98 | int ADL2_Adapter_Primary_Get( 99 | Pointer context, 100 | IntByReference lpPrimaryAdapterIndex 101 | ); 102 | 103 | int ADL2_Display_Modes_Set( 104 | Pointer context, 105 | int iAdapterIndex, 106 | int iDisplayIndex, 107 | int iNumModes, 108 | ADLMode[] lpModes 109 | ); 110 | 111 | int ADL2_Display_PossibleMode_Get( 112 | Pointer context, 113 | int iAdapterIndex, 114 | IntByReference lpNumModes, 115 | PointerByReference lppModes 116 | ); 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/system/Module.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.system; 2 | 3 | import com.google.inject.PrivateModule; 4 | import com.google.inject.Singleton; 5 | 6 | public class Module extends PrivateModule { 7 | 8 | @Override 9 | protected void configure() { 10 | bind(SystemService.class).in(Singleton.class); 11 | bind(SystemController.class).asEagerSingleton(); 12 | expose(SystemService.class); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/system/SystemCommander.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.system; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import com.typesafe.config.ConfigParseOptions; 6 | import com.typesafe.config.ConfigSyntax; 7 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 8 | import java.io.File; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.StringJoiner; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | public class SystemCommander { 17 | public static final String ConfigFile = "winthing.ini"; 18 | 19 | private final Logger logger = LoggerFactory.getLogger(getClass()); 20 | private boolean isEnabled = false; 21 | private Map whitelist = new HashMap(); 22 | 23 | public boolean isEnabled() { 24 | return isEnabled; 25 | } 26 | 27 | public String getCommand(String key) { 28 | return whitelist.get(key); 29 | } 30 | 31 | @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT") 32 | public void parseConfig() { 33 | String path = System.getProperty("user.dir") + File.separator + ConfigFile; 34 | File fp = new File(path); 35 | if (!fp.exists()) { 36 | logger.warn("No whitelist found. Every command is allowed to execute on this device!"); 37 | return; 38 | } 39 | 40 | try { 41 | StringJoiner joiner = new StringJoiner(", "); 42 | 43 | ConfigParseOptions options = ConfigParseOptions.defaults(); 44 | options.setSyntax(ConfigSyntax.CONF); 45 | 46 | Config cfg = ConfigFactory.parseFile(fp, options); 47 | Set map = cfg.root().keySet(); 48 | for (String key : map) { 49 | whitelist.put(key, cfg.getString(key)); 50 | joiner.add(key); 51 | } 52 | 53 | logger.info("Found whitelist of allowed commands to execute, using it..."); 54 | logger.info("Allowed commands: [" + joiner.toString() + "]"); 55 | 56 | isEnabled = true; 57 | } catch (Exception e) { 58 | logger.error("Unable to process whitelist file", e); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/system/SystemController.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.system; 2 | 3 | import com.fatico.winthing.common.BaseController; 4 | import com.fatico.winthing.messaging.Message; 5 | import com.fatico.winthing.messaging.QualityOfService; 6 | import com.fatico.winthing.messaging.Registry; 7 | import com.fatico.winthing.windows.SystemException; 8 | import com.google.gson.JsonArray; 9 | import com.google.gson.JsonPrimitive; 10 | import com.google.inject.Inject; 11 | import java.util.NoSuchElementException; 12 | import java.util.Objects; 13 | 14 | public class SystemController extends BaseController { 15 | 16 | private final SystemService systemService; 17 | private final SystemCommander systemCommander; 18 | 19 | @Inject 20 | public SystemController(final Registry registry, final SystemService systemService) 21 | throws SystemException { 22 | super("system"); 23 | this.systemService = Objects.requireNonNull(systemService); 24 | registry.queueInitialMessage( 25 | makeMessage( 26 | "online", 27 | new JsonPrimitive(true), 28 | QualityOfService.AT_LEAST_ONCE, 29 | true 30 | ) 31 | ); 32 | registry.setWill( 33 | makeMessage( 34 | "online", 35 | new JsonPrimitive(false), 36 | QualityOfService.AT_LEAST_ONCE, 37 | true 38 | ) 39 | ); 40 | registry.subscribe(prefix + "commands/shutdown", this::shutdown); 41 | registry.subscribe(prefix + "commands/suspend", this::suspend); 42 | registry.subscribe(prefix + "commands/hibernate", this::hibernate); 43 | registry.subscribe(prefix + "commands/reboot", this::reboot); 44 | registry.subscribe(prefix + "commands/open", this::open); 45 | registry.subscribe(prefix + "commands/run", this::run); 46 | 47 | systemCommander = new SystemCommander(); 48 | systemCommander.parseConfig(); 49 | } 50 | 51 | public void shutdown(final Message message) { 52 | systemService.shutdown(); 53 | } 54 | 55 | void reboot(final Message message) { 56 | systemService.reboot(); 57 | } 58 | 59 | public void suspend(final Message message) { 60 | systemService.suspend(); 61 | } 62 | 63 | public void hibernate(final Message message) { 64 | systemService.hibernate(); 65 | } 66 | 67 | public void run(final Message message) { 68 | String command; 69 | String parameters; 70 | String workingDirectory; 71 | 72 | try { 73 | final JsonArray arguments = message.getPayload().get().getAsJsonArray(); 74 | command = arguments.get(0).getAsString(); 75 | parameters = arguments.size() > 1 ? arguments.get(1).getAsString() : ""; 76 | workingDirectory = arguments.size() > 2 ? arguments.get(2).getAsString() : null; 77 | } catch (final NoSuchElementException | IllegalStateException exception) { 78 | throw new IllegalArgumentException("Invalid arguments."); 79 | } 80 | 81 | if (systemCommander.isEnabled()) { 82 | String commander = systemCommander.getCommand(command); 83 | if (commander == null) { 84 | throw new SystemException("Invalid command."); 85 | } 86 | command = commander; 87 | } 88 | 89 | systemService.run(command, parameters, workingDirectory); 90 | } 91 | 92 | public void open(final Message message) { 93 | final String uri; 94 | try { 95 | uri = message.getPayload().get().getAsString(); 96 | } catch (final NoSuchElementException | IllegalStateException exception) { 97 | throw new IllegalArgumentException("Invalid arguments."); 98 | } 99 | systemService.open(uri); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/systems/system/SystemService.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.systems.system; 2 | 3 | import com.fatico.winthing.windows.SystemException; 4 | import com.fatico.winthing.windows.jna.Advapi32; 5 | import com.fatico.winthing.windows.jna.Kernel32; 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.inject.Inject; 8 | import com.sun.jna.platform.win32.Kernel32Util; 9 | import com.sun.jna.platform.win32.Shell32; 10 | import com.sun.jna.platform.win32.Tlhelp32; 11 | import com.sun.jna.platform.win32.WinDef; 12 | import com.sun.jna.platform.win32.WinNT; 13 | import com.sun.jna.platform.win32.WinUser; 14 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 15 | import java.util.HashMap; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | 20 | public class SystemService { 21 | 22 | private static final List REQUIRED_PRIVILEGES = ImmutableList.of( 23 | WinNT.SE_SHUTDOWN_NAME 24 | ); 25 | 26 | private final Kernel32 kernel32; 27 | private final Advapi32 advapi32; 28 | private final Shell32 shell32; 29 | 30 | @Inject 31 | public SystemService(final Kernel32 kernel32, final Advapi32 advapi32, 32 | final Shell32 shell32) throws SystemException { 33 | this.kernel32 = Objects.requireNonNull(kernel32); 34 | this.advapi32 = Objects.requireNonNull(advapi32); 35 | this.shell32 = Objects.requireNonNull(shell32); 36 | escalatePrivileges(REQUIRED_PRIVILEGES); 37 | } 38 | 39 | public void shutdown() throws SystemException { 40 | final boolean success = advapi32.InitiateSystemShutdown( 41 | null, 42 | null, 43 | new WinDef.DWORD(0), 44 | true, 45 | false 46 | ); 47 | if (!success) { 48 | throw new SystemException(Kernel32Util.formatMessage(kernel32.GetLastError())); 49 | } 50 | } 51 | 52 | public void reboot() throws SystemException { 53 | final boolean success = advapi32.InitiateSystemShutdown( 54 | null, 55 | null, 56 | new WinDef.DWORD(0), 57 | true, 58 | true 59 | ); 60 | if (!success) { 61 | throw new SystemException(Kernel32Util.formatMessage(kernel32.GetLastError())); 62 | } 63 | } 64 | 65 | public void suspend() throws SystemException { 66 | final boolean success = kernel32.SetSystemPowerState( 67 | true, 68 | false 69 | ); 70 | if (!success) { 71 | throw new SystemException(Kernel32Util.formatMessage(kernel32.GetLastError())); 72 | } 73 | } 74 | 75 | public void hibernate() throws SystemException { 76 | final boolean success = kernel32.SetSystemPowerState( 77 | false, 78 | false 79 | ); 80 | if (!success) { 81 | throw new SystemException(Kernel32Util.formatMessage(kernel32.GetLastError())); 82 | } 83 | } 84 | 85 | public void run(final String command, final String parameters, final String workingDirectory) 86 | throws SystemException { 87 | final WinDef.INT_PTR result = shell32.ShellExecute( 88 | null, 89 | "open", 90 | Objects.requireNonNull(command), 91 | Objects.requireNonNull(parameters), 92 | workingDirectory, 93 | WinUser.SW_SHOWNORMAL 94 | ); 95 | if (result.intValue() <= 32) { 96 | throw new SystemException("Could not run command: " + command + " " + parameters); 97 | } 98 | } 99 | 100 | public void open(final String uri) throws SystemException { 101 | final WinDef.INT_PTR result = shell32.ShellExecute( 102 | null, 103 | "open", 104 | Objects.requireNonNull(uri), 105 | null, 106 | null, 107 | WinUser.SW_SHOWNORMAL 108 | ); 109 | if (result.intValue() <= 32) { 110 | throw new SystemException("Cannot open URI: " + uri); 111 | } 112 | } 113 | 114 | @SuppressFBWarnings("DM_CONVERT_CASE") 115 | public Map findProcesses(final String nameFragment) { 116 | Objects.requireNonNull(nameFragment); 117 | 118 | final String lowercaseNameFragment = nameFragment.toLowerCase(); 119 | final Map processIds = new HashMap<>(); 120 | 121 | final WinNT.HANDLE snapshot = kernel32.CreateToolhelp32Snapshot( 122 | Tlhelp32.TH32CS_SNAPPROCESS, 123 | null 124 | ); 125 | try { 126 | final Tlhelp32.PROCESSENTRY32.ByReference entryReference = 127 | new Tlhelp32.PROCESSENTRY32.ByReference(); 128 | if (kernel32.Process32First(snapshot, entryReference)) { 129 | while (kernel32.Process32Next(snapshot, entryReference)) { 130 | final String processName = new String(entryReference.szExeFile).trim(); 131 | if (processName.toLowerCase().contains(lowercaseNameFragment)) { 132 | processIds.put(entryReference.th32ProcessID.intValue(), processName); 133 | } 134 | } 135 | } 136 | } finally { 137 | kernel32.CloseHandle(snapshot); 138 | } 139 | 140 | return processIds; 141 | } 142 | 143 | private void escalatePrivileges(final List requiredPrivilegeNames) 144 | throws SystemException { 145 | final WinNT.HANDLE accessToken; 146 | { 147 | final WinNT.HANDLEByReference tokenReference = new WinNT.HANDLEByReference(); 148 | final boolean success = advapi32.OpenProcessToken( 149 | kernel32.GetCurrentProcess(), 150 | WinNT.TOKEN_ADJUST_PRIVILEGES | WinNT.TOKEN_QUERY, 151 | tokenReference 152 | ); 153 | if (!success) { 154 | throw new SystemException("Cannot open access token"); 155 | } 156 | accessToken = tokenReference.getValue(); 157 | } 158 | 159 | final WinNT.TOKEN_PRIVILEGES privileges = new WinNT.TOKEN_PRIVILEGES( 160 | requiredPrivilegeNames.size() 161 | ); 162 | { 163 | privileges.PrivilegeCount.setValue(requiredPrivilegeNames.size()); 164 | int index = 0; 165 | for (final String privilegeName : requiredPrivilegeNames) { 166 | final WinNT.LUID luid = new WinNT.LUID(); 167 | { 168 | final boolean success = advapi32.LookupPrivilegeValue( 169 | null, 170 | privilegeName, 171 | luid 172 | ); 173 | if (!success) { 174 | throw new SystemException("Cannot find privilege " + privilegeName); 175 | } 176 | } 177 | privileges.Privileges[index] = new WinNT.LUID_AND_ATTRIBUTES(); 178 | privileges.Privileges[index].Luid = luid; 179 | privileges.Privileges[index].Attributes.setValue(WinNT.SE_PRIVILEGE_ENABLED); 180 | index++; 181 | } 182 | } 183 | 184 | { 185 | final boolean success = advapi32.AdjustTokenPrivileges( 186 | accessToken, 187 | false, 188 | privileges, 189 | privileges.size(), 190 | null, 191 | null 192 | ); 193 | if (!success) { 194 | throw new SystemException( 195 | "Cannot obtain required privileges: " 196 | + Kernel32Util.formatMessage(kernel32.GetLastError()) 197 | ); 198 | } 199 | } 200 | } 201 | 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/SystemException.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows; 2 | 3 | @SuppressWarnings("serial") 4 | public class SystemException extends RuntimeException { 5 | 6 | public SystemException(final String message) { 7 | super(message); 8 | } 9 | 10 | public SystemException(final String message, final Throwable cause) { 11 | super(message, cause); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/WindowsModule.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows; 2 | 3 | import com.fatico.winthing.windows.jna.Advapi32; 4 | import com.fatico.winthing.windows.jna.Kernel32; 5 | import com.fatico.winthing.windows.jna.User32; 6 | import com.google.inject.PrivateModule; 7 | import com.sun.jna.platform.win32.Shell32; 8 | 9 | public class WindowsModule extends PrivateModule { 10 | 11 | @Override 12 | protected void configure() { 13 | bind(User32.class).toInstance(User32.INSTANCE); 14 | expose(User32.class); 15 | 16 | bind(Kernel32.class).toInstance(Kernel32.INSTANCE); 17 | expose(Kernel32.class); 18 | 19 | bind(Advapi32.class).toInstance(Advapi32.INSTANCE); 20 | expose(Advapi32.class); 21 | 22 | bind(Shell32.class).toInstance(Shell32.INSTANCE); 23 | expose(Shell32.class); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/input/KeyboardKey.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows.input; 2 | 3 | import com.google.common.collect.ImmutableSet; 4 | import com.sun.jna.platform.win32.WinDef; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | import java.util.NoSuchElementException; 9 | 10 | @SuppressFBWarnings("DM_CONVERT_CASE") 11 | public enum KeyboardKey { 12 | 13 | CANCEL(3), 14 | BACK(8, "BACKSPACE"), 15 | TAB(9), 16 | CLEAR(12), 17 | RETURN(13, "ENTER"), 18 | SHIFT(16), 19 | CONTROL(17, "CTRL"), 20 | MENU(18, "ALT"), 21 | PAUSE(19), 22 | CAPITAL(20, "CAPSLOCK"), 23 | KANA(0x15), 24 | HANGEUL(0x15), 25 | HANGUL(0x15), 26 | JUNJA(0x17), 27 | FINAL(0x18), 28 | HANJA(0x19), 29 | KANJI(0x19), 30 | ESCAPE(0x1B, "ESC"), 31 | CONVERT(0x1C), 32 | NONCONVERT(0x1D), 33 | ACCEPT(0x1E), 34 | MODECHANGE(0x1F), 35 | SPACE(32, " "), 36 | PRIOR(33), 37 | NEXT(34), 38 | END(35), 39 | HOME(36), 40 | LEFT(37), 41 | UP(38), 42 | RIGHT(39), 43 | DOWN(40), 44 | SELECT(41), 45 | PRINT(42), 46 | EXECUTE(43), 47 | SNAPSHOT(44), 48 | INSERT(45), 49 | DELETE(46), 50 | HELP(47), 51 | 52 | NUM0(0x30, "0"), 53 | NUM1(0x31, "1"), 54 | NUM2(0x32, "2"), 55 | NUM3(0x33, "3"), 56 | NUM4(0x34, "4"), 57 | NUM5(0x35, "5"), 58 | NUM6(0x36, "6"), 59 | NUM7(0x37, "7"), 60 | NUM8(0x38, "8"), 61 | NUM9(0x39, "9"), 62 | 63 | A(0x41), 64 | B(0x42), 65 | C(0x43), 66 | D(0x44), 67 | E(0x45), 68 | F(0x46), 69 | G(0x47), 70 | H(0x48), 71 | I(0x49), 72 | J(0x4a), 73 | K(0x4b), 74 | L(0x4c), 75 | M(0x4d), 76 | N(0x4e), 77 | O(0x4f), 78 | P(0x50), 79 | Q(0x51), 80 | R(0x52), 81 | S(0x53), 82 | T(0x54), 83 | U(0x55), 84 | V(0x56), 85 | W(0x57), 86 | X(0x58), 87 | Y(0x59), 88 | Z(0x5a), 89 | 90 | LWIN(0x5B, "LEFT_WIN", "LEFT_WINDOWS"), 91 | RWIN(0x5C, "RIGHT_WIN", "RIGHT_WINDOWS"), 92 | APPS(0x5D), 93 | SLEEP(0x5F), 94 | NUMPAD0(0x60), 95 | NUMPAD1(0x61), 96 | NUMPAD2(0x62), 97 | NUMPAD3(0x63), 98 | NUMPAD4(0x64), 99 | NUMPAD5(0x65), 100 | NUMPAD6(0x66), 101 | NUMPAD7(0x67), 102 | NUMPAD8(0x68), 103 | NUMPAD9(0x69), 104 | MULTIPLY(0x6A), 105 | ADD(0x6B), 106 | SEPARATOR(0x6C), 107 | SUBTRACT(0x6D), 108 | DECIMAL(0x6E), 109 | DIVIDE(0x6F), 110 | F1(0x70), 111 | F2(0x71), 112 | F3(0x72), 113 | F4(0x73), 114 | F5(0x74), 115 | F6(0x75), 116 | F7(0x76), 117 | F8(0x77), 118 | F9(0x78), 119 | F10(0x79), 120 | F11(0x7A), 121 | F12(0x7B), 122 | F13(0x7C), 123 | F14(0x7D), 124 | F15(0x7E), 125 | F16(0x7F), 126 | F17(0x80), 127 | F18(0x81), 128 | F19(0x82), 129 | F20(0x83), 130 | F21(0x84), 131 | F22(0x85), 132 | F23(0x86), 133 | F24(0x87), 134 | NUMLOCK(0x90), 135 | SCROLL(0x91), 136 | LSHIFT(0xA0), 137 | RSHIFT(0xA1), 138 | LCONTROL(0xA2), 139 | RCONTROL(0xA3), 140 | LMENU(0xA4, "LEFT_ALT"), 141 | RMENU(0xA5, "RIGHT_ALT"), 142 | BROWSER_BACK(0xA6), 143 | BROWSER_FORWARD(0xA7), 144 | BROWSER_REFRESH(0xA8), 145 | BROWSER_STOP(0xA9), 146 | BROWSER_SEARCH(0xAA), 147 | BROWSER_FAVORITES(0xAB), 148 | BROWSER_HOME(0xAC), 149 | VOLUME_MUTE(0xAD), 150 | VOLUME_DOWN(0xAE), 151 | VOLUME_UP(0xAF), 152 | MEDIA_NEXT_TRACK(0xB0), 153 | MEDIA_PREV_TRACK(0xB1), 154 | MEDIA_STOP(0xB2), 155 | MEDIA_PLAY_PAUSE(0xB3), 156 | LAUNCH_MAIL(0xB4), 157 | LAUNCH_MEDIA_SELECT(0xB5), 158 | LAUNCH_APP1(0xB6), 159 | LAUNCH_APP2(0xB7), 160 | OEM_1(0xBA), 161 | OEM_PLUS(0xBB), 162 | OEM_COMMA(0xBC), 163 | OEM_MINUS(0xBD), 164 | OEM_PERIOD(0xBE), 165 | OEM_2(0xBF), 166 | OEM_3(0xC0), 167 | OEM_4(0xDB), 168 | OEM_5(0xDC), 169 | OEM_6(0xDD), 170 | OEM_7(0xDE), 171 | OEM_8(0xDF), 172 | OEM_102(0xE2), 173 | PROCESSKEY(0xE5), 174 | PACKET(0xE7), 175 | ATTN(0xF6), 176 | CRSEL(0xF7), 177 | EXSEL(0xF8), 178 | EREOF(0xF9), 179 | PLAY(0xFA), 180 | ZOOM(0xFB), 181 | NONAME(0xFC), 182 | PA1(0xFD), 183 | OEM_CLEAR(0xFE); 184 | 185 | private static final Map index = new HashMap<>(); 186 | 187 | static { 188 | for (final KeyboardKey key : KeyboardKey.values()) { 189 | index.put(key.name().toLowerCase(), key); 190 | for (final String alias : key.aliases) { 191 | index.put(alias.toLowerCase(), key); 192 | } 193 | } 194 | } 195 | 196 | private final WinDef.WORD virtualKeyCode; 197 | private final ImmutableSet aliases; 198 | 199 | KeyboardKey(final int virtualKeyCode, final String... aliases) { 200 | assert 0 < virtualKeyCode; 201 | assert virtualKeyCode < 0xFF; 202 | this.virtualKeyCode = new WinDef.WORD(virtualKeyCode); 203 | this.aliases = ImmutableSet.copyOf(aliases); 204 | } 205 | 206 | public WinDef.WORD getVirtualKeyCode() { 207 | return virtualKeyCode; 208 | } 209 | 210 | public static KeyboardKey getByCodename(final String codename) { 211 | final KeyboardKey key = index.get(codename.toLowerCase()); 212 | if (key == null) { 213 | throw new NoSuchElementException("Unknown key: " + codename); 214 | } 215 | return key; 216 | } 217 | 218 | } 219 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/input/MouseButton.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows.input; 2 | 3 | import com.sun.jna.platform.win32.WinDef; 4 | 5 | public enum MouseButton { 6 | 7 | LEFT(1), 8 | RIGHT(2), 9 | MIDDLE(4), 10 | X1(5), 11 | X2(6); 12 | 13 | private final WinDef.WORD virtualKeyCode; 14 | 15 | MouseButton(final int virtualKeyCode) { 16 | assert 0 < virtualKeyCode; 17 | assert virtualKeyCode < 0xFF; 18 | this.virtualKeyCode = new WinDef.WORD(virtualKeyCode); 19 | } 20 | 21 | public WinDef.WORD getVirtualKeyCode() { 22 | return virtualKeyCode; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/jna/Advapi32.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows.jna; 2 | 3 | import com.sun.jna.Native; 4 | import com.sun.jna.platform.win32.WinDef; 5 | import com.sun.jna.win32.W32APIOptions; 6 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 7 | 8 | @SuppressWarnings({"checkstyle:methodname", "checkstyle:parametername"}) 9 | @SuppressFBWarnings("NM_SAME_SIMPLE_NAME_AS_INTERFACE") 10 | public interface Advapi32 extends com.sun.jna.platform.win32.Advapi32 { 11 | 12 | Advapi32 INSTANCE = (Advapi32) Native.load( 13 | "advapi32", 14 | Advapi32.class, 15 | W32APIOptions.DEFAULT_OPTIONS 16 | ); 17 | 18 | boolean InitiateSystemShutdown( 19 | String lpMachineName, 20 | String lpMessage, 21 | WinDef.DWORD dwTimeout, 22 | boolean bForceAppsClosed, 23 | boolean bRebootAfterShutdown 24 | ); 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/jna/Kernel32.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows.jna; 2 | 3 | import com.sun.jna.Native; 4 | import com.sun.jna.win32.W32APIOptions; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | 7 | @SuppressWarnings({"checkstyle:methodname", "checkstyle:parametername"}) 8 | @SuppressFBWarnings("NM_SAME_SIMPLE_NAME_AS_INTERFACE") 9 | public interface Kernel32 extends com.sun.jna.platform.win32.Kernel32 { 10 | 11 | Kernel32 INSTANCE = (Kernel32) Native.load( 12 | "kernel32", 13 | Kernel32.class, 14 | W32APIOptions.DEFAULT_OPTIONS 15 | ); 16 | 17 | boolean SetSystemPowerState( 18 | boolean fSuspend, 19 | boolean fForce 20 | ); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/fatico/winthing/windows/jna/User32.java: -------------------------------------------------------------------------------- 1 | package com.fatico.winthing.windows.jna; 2 | 3 | import com.sun.jna.Native; 4 | import com.sun.jna.win32.W32APIOptions; 5 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 6 | 7 | @SuppressWarnings({"checkstyle:methodname", "checkstyle:parametername"}) 8 | @SuppressFBWarnings("NM_SAME_SIMPLE_NAME_AS_INTERFACE") 9 | public interface User32 extends com.sun.jna.platform.win32.User32 { 10 | 11 | User32 INSTANCE = (User32) Native.load( 12 | "user32", 13 | User32.class, 14 | W32APIOptions.DEFAULT_OPTIONS 15 | ); 16 | 17 | LRESULT SendMessage(HWND hWnd, int Msg, WPARAM wParam, LPARAM lParam); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | broker = "127.0.0.1:1883" 2 | username = mqtt 3 | password = mqtt 4 | clientid = WinThing 5 | prefix = winthing 6 | reconnect = 5 -------------------------------------------------------------------------------- /src/main/resources/favicon-green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiedlarek/winthing/7a565e58d5bec8c179bf139a9c9b93bf7079e94c/src/main/resources/favicon-green.png -------------------------------------------------------------------------------- /src/main/resources/favicon-red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiedlarek/winthing/7a565e58d5bec8c179bf139a9c9b93bf7079e94c/src/main/resources/favicon-red.png -------------------------------------------------------------------------------- /src/main/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiedlarek/winthing/7a565e58d5bec8c179bf139a9c9b93bf7079e94c/src/main/resources/favicon.ico -------------------------------------------------------------------------------- /src/main/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/msiedlarek/winthing/7a565e58d5bec8c179bf139a9c9b93bf7079e94c/src/main/resources/favicon.png -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss} %level [%thread/%logger{36}] %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | winthing.log 14 | false 15 | 16 | %d{HH:mm:ss} %level [%thread/%logger{36}] %msg%n 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------