├── .gitignore ├── CONTRIBUTING.md ├── ChangeLog.md ├── DCO.txt ├── LICENSE ├── Makefile ├── README.md ├── TODO ├── archlinux └── PKGBUILD ├── configure ├── debian ├── README.source ├── changelog ├── control ├── copyright ├── rules ├── source │ └── format └── watch ├── doc └── netevent.rst ├── examples ├── laptop-and-vm-with-systemd │ ├── 70-input-names.rules │ ├── daemon.ne2 │ ├── start-vm.sh │ └── vm0.service └── simple.ne2 └── src ├── bitfield.cpp ├── bitfield.h ├── daemon.cpp ├── iohandle.h ├── main.cpp ├── main.h ├── reader.cpp ├── socket.cpp ├── socket.h ├── types.h ├── utils.h └── writer.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.d 3 | *.1 4 | config.h 5 | config.log 6 | config.mak 7 | local-env.mak 8 | netevent 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Submission format 4 | 5 | Contributions should be made via pull requests on github. 6 | 7 | ## Licensing 8 | 9 | By submitting contributions to this project you agree to the licensing 10 | specified in the LICENSE file for each file you are contributing to. 11 | 12 | ## Developer Certificate of Origin (Signed-off-by lines) 13 | 14 | In order to contribute to the project you must certify that you are allowed to 15 | do so, as specified in the Developer Certificate of Origin version 1.1, a copy 16 | of which can be found in DCO.txt. 17 | To show that this is the case you have to sign-off your patch. This can be done 18 | with a simple "Signed-off-by" line a the end of the commit message as created 19 | via `git commit --signoff`. 20 | You must use your real name. Anonymous contributions are not acceptable. 21 | -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Release 2.2.2 2 | 3 | This is a maintenance release to fix builds with newer compilers. 4 | 5 | # Release 2.2.1 6 | 7 | * add `-V`/`--version` command line option. (#22) 8 | 9 | # Release 2.2 10 | 11 | * Writing outputs and grabbing input is now managed separately. 12 | * The `grab` command is deprecated and split into the following two new 13 | commands: 14 | * `grab-devices` for input device grabbing. 15 | * `write-events` for sending the events. 16 | * support out of tree builds 17 | 18 | # Prior to 2.2 19 | 20 | See git history. 21 | -------------------------------------------------------------------------------- /DCO.txt: -------------------------------------------------------------------------------- 1 | Developer Certificate of Origin 2 | Version 1.1 3 | 4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 5 | 1 Letterman Drive 6 | Suite D4700 7 | San Francisco, CA, 94129 8 | 9 | Everyone is permitted to copy and distribute verbatim copies of this 10 | license document, but changing it is not allowed. 11 | 12 | 13 | Developer's Certificate of Origin 1.1 14 | 15 | By making a contribution to this project, I certify that: 16 | 17 | (a) The contribution was created in whole or in part by me and I 18 | have the right to submit it under the open source license 19 | indicated in the file; or 20 | 21 | (b) The contribution is based upon previous work that, to the best 22 | of my knowledge, is covered under an appropriate open source 23 | license and I have the right under that license to submit that 24 | work with modifications, whether created in whole or in part 25 | by me, under the same open source license (unless I am 26 | permitted to submit under a different license), as indicated 27 | in the file; or 28 | 29 | (c) The contribution was provided directly to me by some other 30 | person who certified (a), (b) or (c) and I have not modified 31 | it. 32 | 33 | (d) I understand and agree that this project and the contribution 34 | are public and that a record of the contribution (including all 35 | personal information I submit with it, including my sign-off) is 36 | maintained indefinitely and may be redistributed consistent with 37 | this project or the open source license(s) involved. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include ./config.mak 2 | -include ./local-env.mak 3 | 4 | PREFIX ?= /usr/local 5 | BINDIR ?= $(PREFIX)/bin 6 | DATAROOTDIR = $(PREFIX)/share 7 | MANDIR = $(DATAROOTDIR)/man 8 | MAN1DIR = $(MANDIR)/man1 9 | 10 | SOURCEDIR ?= . 11 | 12 | CPPFLAGS ?= -g 13 | CPPFLAGS += -Wall -Werror -Wno-unknown-pragmas 14 | CXX ?= clang++ 15 | # Code should compile with c++11 as well, but c++14 may have stricter 16 | # attributes on some methods. 17 | CXXFLAGS += -std=c++14 18 | 19 | ifeq ($(CXX), clang++) 20 | CPPFLAGS += -Weverything \ 21 | -Wno-c++98-compat \ 22 | -Wno-c++98-compat-pedantic \ 23 | -Wno-padded \ 24 | -Wno-packed 25 | endif 26 | 27 | LDFLAGS ?= -g 28 | 29 | SANITIZE_FLAGS ?= 30 | 31 | CPPFLAGS += $(SANITIZE_FLAGS) 32 | LDFLAGS += $(SANITIZE_FLAGS) 33 | 34 | BINARY := netevent 35 | OBJECTS := src/main.o \ 36 | src/daemon.o \ 37 | src/writer.o \ 38 | src/reader.o \ 39 | src/socket.o \ 40 | src/bitfield.o 41 | 42 | MAN1PAGES-y := doc/netevent.1 43 | 44 | MAN1PAGES := $(MAN1PAGES-$(ENABLE_DOC)) 45 | 46 | all: $(BINARY) $(MAN1PAGES) 47 | 48 | config.h: 49 | ./configure 50 | 51 | Makefile: $(SOURCEDIR)/Makefile 52 | cp $< $@ 53 | +$(MAKE) $(MAKECMDGOALS) 54 | 55 | $(BINARY): $(OBJECTS) 56 | $(CXX) $(LDFLAGS) -o $@ $(OBJECTS) 57 | 58 | .cpp.o: 59 | $(CXX) $(CPPFLAGS) $(CXXFLAGS) -I. -c -o $@ $< -MMD -MT $@ -MF $(@:.o=.d) 60 | 61 | .SUFFIXES: .1 .rst 62 | .rst.1: 63 | $(RST2MAN) $< $@ 64 | 65 | .PHONY: install 66 | install: # Make sure we use config.mak, nest once: 67 | $(MAKE) install-do 68 | .PHONY: install-do 69 | install-do: $(BINARY) $(MAN1PAGES) 70 | install -dm755 $(DESTDIR)$(BINDIR) 71 | install -Tm755 $(BINARY) $(DESTDIR)$(BINDIR)/$(BINARY) 72 | ifeq ($(ENABLE_DOC), y) 73 | install -dm755 $(DESTDIR)$(MAN1DIR) 74 | install -m644 -t $(DESTDIR)$(MAN1DIR) $(MAN1PAGES) 75 | endif 76 | 77 | distclean: clean 78 | rm -f config.h 79 | 80 | clean: 81 | rm -f src/*.o src/*.d doc/*.1 82 | 83 | $(CURDIR)/doc $(CURDIR)/src: 84 | mkdir -p $@ 85 | 86 | $(OBJECTS): Makefile config.h | $(CURDIR)/src 87 | $(MAN1PAGES): $(CURDIR)/doc 88 | 89 | -include src/*.d 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netevent 2 | 3 | Netevent is a tool which can be used to share linux event devices with other 4 | machines (either via `/dev/uinput` or by implementing a client for the same 5 | protocol with other means). 6 | 7 | Originally it simply dumped device capabilities to stdout and afterwards 8 | behaved like running `cat /dev/input/eventX` in one mode, and in the other 9 | passed the parsed capabilities to `/dev/uinput` and then passing events 10 | through. 11 | 12 | Since managing this for multiple devices can become tedious when having more 13 | than one destination (and since the original grab/toggle/hotkey mechanisms were 14 | weird and literally targeted my personal use case), netevent2 now extends the 15 | protocol to contain packets which can contain more than one device and can add 16 | and remove devices on the fly. 17 | 18 | The original `cat` like behavior (although currently without hotkey support) 19 | is also available for debugging purposes (and the `create` mode supports both 20 | protocol versions). 21 | 22 | The main tool is now the `netevent daemon` which has a command socket (an 23 | optionally abstract unix socket) via which one can add devices, outputs and 24 | hotkeys on the fly. See the examples below. 25 | 26 | ## Compilation 27 | 28 | * optionally: `./configure --prefix=/usr` 29 | * `make` 30 | 31 | You can still just run `make` as before. However, to support the usual 32 | installation workflows, and to distinguish between systems with newer kernels 33 | where `/dev/uinput` has been extended with a `UI_DEV_SETUP` `ioctl`, a 34 | `./configure` script has been added to check for this and create a `config.h` 35 | as well as a `config.mak` for PREFIX/BINDIR/... (all of which can be passed as 36 | variables directly to `make` instead as well, along with the usual `DESTDIR`). 37 | 38 | ## Installation 39 | 40 | * `make install` or `make DESTDIR=/my/staging/dir install` 41 | 42 | Or: as previously, just put the `netevent` binary wherever. 43 | 44 | ## Usage 45 | 46 | See the DAEMON COMMANDS section in netevent(1) for details on the commands used 47 | in the setup scripts below. 48 | 49 | ### Examples 50 | 51 | See the `examples/` directory. Read the setup-example below to see how to adapt 52 | the hotkey lines to work with your devices. 53 | 54 | #### Simple example setup: sharing keyboard & mouse with a machine via ssh: 55 | 56 | Host side: 57 | 58 | * Preparation: Make sure we can access event devices as a user 59 | 60 | Usually this means running something like `gpasswd -a myuser input` 61 | 62 | * Step 1: Decide which /dev/input/eventXY devices to pass through. 63 | 64 | For consistent file names use something like: 65 | `/dev/input/by-id/usb-MyAwesomeKeyboard-event-kbd` 66 | `/dev/input/by-id/usb-BestMouseEver-event-mouse` 67 | 68 | * Step 2: Decide on a hotkey and find its event code: 69 | 70 | In the above example we want to use a key on the keyboard (unless you 71 | have an insane amount of mouse buttons...). 72 | `netevent` can be used to dump events in a readable way, run the `show` 73 | subcommand on the device and press the keys you want to use for hotkeys. 74 | If this is the same keyboard you're typing in the command with , prepend a 75 | sleep to avoid confusion when netevent picks up the release of the enter 76 | key. 77 | ``` 78 | $ sleep 0.3 && netevent show /dev/input/by-id/usb-...-event-kbd 79 | MSC:4:3829 80 | KEY:189:1 81 | SYN:0:0 82 | MSC:4:3829 83 | KEY:189:0 84 | SYN:0:0 85 | ``` 86 | 87 | * Step 3: Prepare a setup script for the daemon: 88 | 89 | ``` 90 | # file: netevent-setup.ne2 91 | # Add mouse & keyboard 92 | device add mymouse /dev/input/by-id/usb-BestMouseEver-event-mouse 93 | device add mykbd /dev/input/by-id/usb-MyAwesomeKeyboard-event-kbd 94 | 95 | # Add toggle hotkey (on press, and ignore the release event) 96 | hotkey add mykbd key:189:1 grab-devices toggle\; write-events toggle 97 | hotkey add mykbd key:189:0 nop 98 | 99 | # Connect to the two devices via password-less ssh 100 | output add myremote exec:ssh user@other-host netevent create 101 | # Select the output to write to 102 | use myremote 103 | ``` 104 | 105 | * Step 4: Run the netevent daemon: 106 | 107 | `$ netevent daemon -s netevent-setup.ne2 netevent-command.sock` 108 | 109 | You can now send additional commands to the daemon by connecting to the socket. 110 | For example via `socat READLINE UNIX-CONNECT:netevent-command.sock`. 111 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * Perhaps a per-input-device NETEVENT_GRABBING env var? 2 | Either multiple vars or one with a space separated `name=0|1` list... 3 | * Some TODOs found in the code? There should be at least one... 4 | * a) INotify to listen to devices being created? 5 | b) Use systemd service files to tell the daemon about new devices? 6 | * Broadcast a keep-alive/ping packet every some seconds. 7 | -------------------------------------------------------------------------------- /archlinux/PKGBUILD: -------------------------------------------------------------------------------- 1 | _pkgbase=netevent 2 | pkgname=netevent-git 3 | pkgver=59c40a7 4 | pkgrel=1 5 | pkgdesc="Event device viewing/cloning utility" 6 | url="https://github.com/Blub/netevent" 7 | arch=('i686' 'x86_64') 8 | license=('GPL') 9 | depends=() 10 | makedepends=('python-docutils') 11 | source=("git+https://github.com/Blub/netevent.git") 12 | sha256sums=('SKIP') 13 | 14 | pkgver() { 15 | cd "${srcdir}/netevent" 16 | git describe --always 17 | } 18 | 19 | prepare() { 20 | cd "$srcdir/$_pkgbase" 21 | ./configure --prefix=/usr 22 | } 23 | 24 | build() { 25 | cd "$srcdir/$_pkgbase" 26 | make 27 | } 28 | 29 | package() { 30 | cd "$srcdir/$_pkgbase" 31 | make DESTDIR="$pkgdir" install 32 | } 33 | -------------------------------------------------------------------------------- /configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | enable_doc=auto 4 | 5 | help() { 6 | cat <&2 22 | exit 1 23 | } 24 | 25 | logmsg() { 26 | echo "$@" 27 | echo "$@" >&4 28 | } 29 | 30 | logrun() { 31 | echo "$@" >&4 32 | "$@" >&4 2>&4 33 | logrun_res="$?" 34 | echo "---" >&4 35 | echo >&4 36 | return "$logrun_res" 37 | } 38 | 39 | for opt; do 40 | case "$opt" in 41 | -h|--h|--help) help; exit 0 ;; 42 | --prefix=*) PREFIX="${opt#--prefix=}" ;; 43 | --bindir=*) BINDIR="${opt#--bindir=}" ;; 44 | --datarootdir=*) DATAROOTDIR="${opt#--datarootdir=}" ;; 45 | --mandir=*) MANDIR="${opt#--mandir=}" ;; 46 | --man1dir=*) MAN1DIR="${opt#--man1dir=}" ;; 47 | --enable-doc=*) enable_doc="${doc#--enable-doc=}" ;; 48 | --enable-doc) enable_doc=yes ;; 49 | --disable-doc) enable_doc=no ;; 50 | *) die "Unknown option: $opt" ;; 51 | esac 52 | done 53 | 54 | rm -f config.log 55 | exec 4>config.log 56 | 57 | absdirname() 58 | (cd `dirname $1`; pwd) 59 | 60 | SOURCEDIR=`absdirname $0` 61 | 62 | NETEVENT_VERSION="$(sed -ne '/^# Release/{s/^# Release\s*//;p;q}' "$SOURCEDIR/ChangeLog.md")" 63 | 64 | PREFIX="${PREFIX:-/usr/local}" 65 | BINDIR="${BINDIR:-${PREFIX}/bin}" 66 | DATAROOTDIR="${DATAROOTDIR:-${PREFIX}/share}" 67 | MANDIR="${MANDIR:-${DATAROOTDIR}/man}" 68 | MAN1DIR="${MAN1DIR:-${MANDIR}/man1}" 69 | 70 | isautoyes() { 71 | case "$1" in 72 | auto|[Yy][Ee][Ss]|1|[Oo][Nn]|[Tt][Rr][Uu][Ee]) 73 | return 0 ;; 74 | *) 75 | return 1 ;; 76 | esac 77 | } 78 | 79 | if test -z "$CXX"; then 80 | if which clang++ >/dev/null 2>&1; then 81 | CXX=clang++ 82 | elif which g++ >/dev/null 2>&1; then 83 | CXX=g++ 84 | else 85 | echo 86 | echo "failed to find a compiler, please set \$CXX" >&2 87 | exit 1 88 | fi 89 | fi 90 | 91 | echo -n 'Checking for rst2man...' 92 | if isautoyes "$enable_doc"; then 93 | ENABLE_DOC=y 94 | if test -z "$RST2MAN"; then 95 | RST2MAN="$(which rst2man 2>/dev/null)" 96 | if [ $? -ne 0 ]; then 97 | ENABLE_DOC= 98 | RST2MAN= 99 | if [ "x$enable_doc" != "xauto" ]; then 100 | echo "failed to find rst2man" >&2 101 | exit 1 102 | fi 103 | fi 104 | fi 105 | else 106 | ENABLE_DOC= 107 | RST2MAN= 108 | fi 109 | if [ "x$ENABLE_DOC" = x ]; then 110 | echo 'no, disabling documentation building' 111 | else 112 | echo "$RST2MAN" 113 | fi 114 | 115 | trycc() { 116 | echo "$1" >.cfgtest.cpp 117 | logrun $CXX $CPPFLAGS $CXXFLAGS -c -o .cfgtest.o .cfgtest.cpp 118 | try_compile_result="$?" 119 | rm -f .cfgtest.o .cfgtest.cpp 120 | return $try_compile_result 121 | } 122 | 123 | UI_DEV_CKPROG="#include 124 | #include 125 | int main() { 126 | struct uinput_setup setup; 127 | ioctl(0, UI_DEV_SETUP); 128 | ioctl(0, UI_ABS_SETUP); 129 | return 0; 130 | } 131 | " 132 | 133 | echo -n 'Checking for UI_DEV_SETUP (kernel >= 4.4)...' 134 | if trycc "$UI_DEV_CKPROG"; then 135 | HAS_UI_DEV_SETUP='#define HAS_UI_DEV_SETUP' 136 | echo "ok" 137 | else 138 | HAS_UI_DEV_SETUP='/* #undef HAS_UI_DEV_SETUP */' 139 | echo "no (disabling)" 140 | fi 141 | 142 | rm -f config.h 143 | echo '#ifndef NETEVENT_2_CONFIG_H' >>config.h 144 | echo '#define NETEVENT_2_CONFIG_H' >>config.h 145 | echo >>config.h 146 | echo "#define NETEVENT_VERSION \"$NETEVENT_VERSION\"" >>config.h 147 | echo "$HAS_UI_DEV_SETUP" >>config.h 148 | echo >>config.h 149 | echo '#endif' >>config.h 150 | 151 | rm -f config.mak 152 | cat >config.mak <>config.mak "VPATH = ${SOURCEDIR}" 169 | cp "${SOURCEDIR}/Makefile" ./ 170 | fi 171 | -------------------------------------------------------------------------------- /debian/README.source: -------------------------------------------------------------------------------- 1 | 2 | To generate the upstream source tarball use: 3 | git archive --prefix=netevent/ -o ../netevent_.orig.tar.gz HEAD 4 | with the upstream target version. 5 | 6 | Then you can build it with dpkg-buildpackage or sbuild;, see official 7 | Debian documentation. 8 | 9 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | netevent (2.2.2-1) unreleased; urgency=medium 2 | 3 | * Maintenance release to fix builds with newer stricter compilers. 4 | 5 | -- Wolfgang Bumiller Sun, 24 Dec 2017 10:57:35 +0100 6 | 7 | netevent (2.2.1-1) unreleased; urgency=medium 8 | 9 | * Writing outputs and grabbing input is now managed separately. 10 | * The `grab` command is deprecated and split into the following two new 11 | commands: 12 | * `grab-devices` for input device grabbing. 13 | * `write-events` for sending the events. 14 | * support out of tree builds 15 | * Added --version/-V command line option 16 | 17 | -- Wolfgang Bumiller Sun, 24 Dec 2017 10:57:35 +0100 18 | 19 | netevent (2.1+git-1~local) unstable; urgency=medium 20 | 21 | * Non-maintainer upload. 22 | * Updates rule to adapt to non-standard configure script. 23 | * Bump debhelper compat level to 12. 24 | * Switch to source format 3.0. 25 | * Add missing misc:Depends. 26 | * Update copyright years. 27 | * Bumped Standards-Version to 4.5.0 (no changes required). 28 | * Add watch file. 29 | * Use Python 3 docutils by default since Python 2 is being dropped in 30 | Debian. 31 | * add README.source. 32 | 33 | -- Marc Dequènes (Duck) Thu, 23 Apr 2020 01:41:41 +0900 34 | 35 | netevent (2.0-1) unstable; urgency=medium 36 | 37 | * Initial release 38 | 39 | -- Wolfgang Bumiller Sun, 24 Dec 2017 10:57:35 +0100 40 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: netevent 2 | Section: utils 3 | Priority: optional 4 | Maintainer: Wolfgang Bumiller 5 | Build-Depends: 6 | debhelper-compat (= 12), 7 | python3-docutils | python-docutils 8 | Standards-Version: 4.5.0 9 | Homepage: https://github.com/Blub/netevent 10 | 11 | Package: netevent 12 | Architecture: any 13 | Depends: ${shlibs:Depends}, ${misc:Depends} 14 | Description: Utility to clone event devices 15 | This tool allows inspecting the input events created by event devices as well 16 | as cloning/sending such devices to clone them or share them with other 17 | machines. 18 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: netevent 3 | Source: 4 | 5 | Files: * 6 | Copyright: 2017-2021 Wolfgang Bumiller 7 | License: GPL-2+ 8 | This package is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | . 13 | This package is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | . 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see 20 | . 21 | On Debian systems, the complete text of the GNU General 22 | Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". 23 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # See debhelper(7) (uncomment to enable) 3 | # output every command that modifies files on the build system. 4 | #export DH_VERBOSE = 1 5 | 6 | # see FEATURE AREAS in dpkg-buildflags(1) 7 | #export DEB_BUILD_MAINT_OPTIONS = hardening=+all 8 | 9 | %: 10 | dh $@ 11 | 12 | override_dh_auto_configure: 13 | ./configure --prefix=/usr 14 | 15 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/watch: -------------------------------------------------------------------------------- 1 | version=4 2 | https://github.com/Blub/netevent/releases .*/(.*)\.tar\.gz 3 | -------------------------------------------------------------------------------- /doc/netevent.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | netevent 3 | ======== 4 | 5 | -------------------------------------- 6 | show, share, clone evdev event devices 7 | -------------------------------------- 8 | 9 | :Author: Wolfgang Bumiller 10 | :Manual section: 1 11 | :Manual group: netevent Manual 12 | 13 | .. TODO: email 14 | 15 | SYNOPSIS 16 | ======== 17 | 18 | ``netevent`` show *DEVICE* [\ *COUNT*\ ] 19 | 20 | ``netevent`` cat [\ *OPTIONS*\ ] *DEVICE* 21 | 22 | ``netevent`` create [\ *OPTIONS*\ ] *DEVICE* 23 | 24 | ``netevent`` daemon [\ *OPTIONS*\ ] *SOCKETNAME* 25 | 26 | ``netevent`` command *SOCKETNAME* *COMMAND* 27 | 28 | OPTIONS 29 | ======= 30 | 31 | Some options can be used on multiple commands. 32 | 33 | All subcommands: 34 | ---------------- 35 | 36 | ``-h, --help`` 37 | Show a short usage message. 38 | 39 | ``-V, --version`` 40 | Show the netevent version string. 41 | 42 | ``netevent create`` 43 | ---------------------------------------- 44 | 45 | ``--duplicates=``\ *MODE* 46 | Change how duplicate devices are to be treated. *MODE* can be: 47 | 48 | ``reject`` 49 | 50 | The default. If a device with an already existing ID is received, treat 51 | this as an error and exit. 52 | 53 | ``resume`` 54 | 55 | Assume the source was restarted and is sending the same device again. 56 | Currently this does not verify whether that's actually the case. 57 | 58 | ``replace`` 59 | 60 | Remove the previous device and replace it with the new one. 61 | Since ``resume`` does not verify the device, this is the preferred mode 62 | if the destination event device node does not need to be persistent. 63 | 64 | ``--listen=``\ *SOCKETNAME* 65 | Rather than reading from stdin, listen on the specified unix (or abstract 66 | if prefixed with "@") socket. 67 | 68 | ``--connect`` 69 | Used together with ``--listen`` this causes netevent to first try to 70 | connect to the socket. If successful, it'll pass events through to the 71 | instance it connected to. Otherwise, if ``--daemonize`` was also specified, 72 | it'll fork off a new instance to which it connects first. If 73 | ``--daemonize`` was not specified it'll return an error code. 74 | 75 | ``--on-close=end|accept`` 76 | When using ``--listen``, this option decides how to proceed after a client 77 | disconnects. The default is to ``accept`` a new client and resume according 78 | to the configured ``--duplicates`` mode. Alternatively ``end`` can be used 79 | to cause the main loop to exit successfully. 80 | 81 | ``--daemonize`` 82 | Run as a background daemon. When using ``--listen`` it may also desirable 83 | to run netevent in the background. 84 | 85 | ``netevent cat`` and ``netevent create`` 86 | ---------------------------------------- 87 | 88 | ``-l, --legacy`` 89 | Use a netevent 1 compatible protocol. 90 | 91 | ``--no-legacy`` 92 | Use a netevent 2 compatible protocol. This is the default. 93 | 94 | ``netevent cat`` and ``netevent show`` 95 | -------------------------------------- 96 | 97 | ``-g, --grab`` 98 | Grab the input device to prevent it from also firing events on the system. 99 | This is the default. 100 | 101 | ``-G, --no-grab`` 102 | Do not grab the input device. 103 | 104 | ``netevent daemon`` 105 | ------------------- 106 | 107 | ``-s, --source=``\ *FILE* 108 | Run commands from the specified file. Can be specified multiple times. 109 | This can be used to fully setup the daemon with outputs, devices and 110 | hotkeys. See the `DAEMON COMMANDS` section for details. 111 | 112 | DAEMON COMMANDS 113 | =============== 114 | 115 | ``action set`` *EVENT* *COMMAND* 116 | Queue a command when an event occurs. The command can contain semicolons 117 | to execute multiple commands. Multiple parameters will be concatenated with 118 | a space. 119 | 120 | The following events currently exist: 121 | 122 | * ``output-changed`` 123 | Executed on a ``use`` command or when an output device fails and a 124 | fallback is being activated. 125 | * ``write-changed`` 126 | Executed whenever the ``write-events`` command is used. 127 | * ``grab-changed`` 128 | Executed whenever the ``grab-devices`` command is used. 129 | * ``device-lost`` 130 | Executed whenever a device we are reading from disappears. 131 | 132 | These commands are executed immediately after such an event has occurred. 133 | Note that there's nothing preventing you from building an endless loop by 134 | adding event-triggering commands in this place, so, just don't. 135 | 136 | ``action remove`` *EVENT* 137 | Remove a command bound to an event. 138 | 139 | ``nop`` 140 | Nothing. Bind as hotkey to ignore an event and be explicit about it. 141 | 142 | ``grab-devices``\ *on*\ \|\ *off*\ \|\ *toggle* 143 | Set the grabbing state. Controls whether events are also fired locally. 144 | 145 | ``write-events``\ *on*\ \|\ *off*\ \|\ *toggle* 146 | Set the writing state. Controls whether events are passed to the current 147 | output. 148 | 149 | ``grab``\ *on*\ \|\ *off*\ \|\ *toggle* 150 | Deprecated. This is the old command which has been superseeded by the pair 151 | ``grab-devices`` and ``write-events``. 152 | 153 | ``use`` *OUTPUT* 154 | Set the current output. 155 | 156 | ``output add`` [``--resume``] *OUTPUT_NAME* *OUTPUT_SPEC* 157 | Add a new output. *OUTPUT_NAME* can be an arbitrary name used later for 158 | ``output remove`` or ``use`` commands. *OUTPUT_SPEC* can currently be 159 | either a file/fifo, a command to pipe to when prefixed with *exec:*, or the 160 | name of a unix or abstract socket when using *unix:/path* or 161 | *unix:@abstractName*. See the examples above. 162 | 163 | If the ``--resume`` parameter is provided, assume the destination already 164 | knows all the existing devices and do not recreate them. 165 | 166 | ``output remove`` *OUTPUT_NAME* 167 | Remove an existing output. 168 | 169 | ``output use`` *OUTPUT_NAME* 170 | Long version of ``use`` *OUTPUT_NAME*. 171 | 172 | ``exec`` *COMMAND* 173 | Execute a command. Mostly useful for hotkeys. 174 | 175 | ``exec&`` *COMMAND* 176 | Execute a command in the background. 177 | 178 | ``source`` *FILE* 179 | Execute daemon commands from a file. 180 | 181 | ``quit`` 182 | Cause the daemon to quit. 183 | 184 | ``hotkey add`` *DEVICE_NAME* *EVENT* *COMMAND* 185 | Add a hotkey to an existing device. *DEVICE* is the name used when 186 | adding the device via ``device add``. *EVENT* is an event specification 187 | of the form *TYPE*:*CODE*:*VALUE*, as printed out by ``netevent show``. 188 | *COMMAND* is a daemon command to be executed when the event is read. 189 | 190 | ``hotkey remove`` *DEVICE_NAME* *EVDENT* 191 | Remove a hotkey for an event on a device. 192 | 193 | ``device add`` *DEVICE_NAME* *EVENT_DEVICE_FILE* 194 | Register an evdev device. 195 | 196 | ``device remove`` *DEVICE_NAME* 197 | Remove an evdev device. 198 | 199 | ``device rename`` *DEVICE_NAME* *NEW_NAME* 200 | Rename a device. Useful when adding output of which the devices should have 201 | a recognizable name. 202 | 203 | ``device reset-name`` *DEVICE_NAME* 204 | Reset a device's name to its default. 205 | 206 | ``device set-persistent`` *DEVICE_NAME* *BOOL* 207 | Change whether a device's removal should be announced to the outputs. 208 | 209 | ``info`` 210 | Show current inputs, outputs, devices and hotkeys. 211 | 212 | DAEMON ENVIRONMENT VARIABLES 213 | ============================ 214 | 215 | The daemon will maintain the following environment variables to provide some 216 | information to commands executed via an ``exec`` hotkey: 217 | 218 | * ``NETEVENT_OUTPUT_NAME`` 219 | This will contain the name of the output currently in use. 220 | 221 | * ``NETEVENT_GRABBING`` 222 | This will be "1" if the daemon is currently grabbing, or "0" if it is not. 223 | Note that with multiple input devices, failure to grab an input device will 224 | cause this variable to be in an undefined state. 225 | 226 | * ``NETEVENT_WRITING`` 227 | This will be "1" if the daemon is currently writing, or "0" if it is not. 228 | 229 | KNOWN INTERACTIONS WITH X11 230 | =========================== 231 | 232 | When using the synaptics X11 driver, it can be configured to grab event 233 | devices, which will prevent netevent from grabbing them. You may need to 234 | change the ``GrabEventDevice`` option in your ``xorg.conf``. 235 | 236 | BUGS 237 | ==== 238 | 239 | Please report bugs to via https://github.com/Blub/netevent/issues\ . 240 | -------------------------------------------------------------------------------- /examples/laptop-and-vm-with-systemd/70-input-names.rules: -------------------------------------------------------------------------------- 1 | # /etc/udev/rules/70-input-names.rules 2 | # 3 | # This file is optional (see start-vm.sh) 4 | # 5 | # For event devices with a name matching 'vm*-*' create a symlink in 6 | # /dev/input/by-name. Also tag them for systemd as /input/by-name/ so we 7 | # can depend on them in systemd service files. 8 | 9 | # Filter via goto+labels to avoid huge lines 10 | KERNEL!="event*", GOTO="input_vm_names_end" 11 | SUBSYSTEM!="input", GOTO="input_vm_names_end" 12 | ENV{.INPUT_CLASS}!="?*", GOTO="input_vm_names_end" 13 | 14 | # Store name in .INPUT_NAME 15 | PROGRAM="/bin/cat /sys/class/input/%k/device/name", ENV{.INPUT_NAME}="%c" 16 | # Filter out by our desired pattern 17 | ENV{.INPUT_NAME}!="vm*-*", GOTO="input_vm_names_end" 18 | 19 | # Add a by-name symlink: 20 | SYMLINK+="/input/by-name/%c" 21 | 22 | # Add a systemd .device service unit: 23 | TAG+="systemd" 24 | # Alternatively we can add additional paths for the .device units 25 | # ENV{SYSTEMD_ALIAS}+="/input/by-name/%E{.INPUT_NAME}" 26 | 27 | LABEL="input_vm_names_end" 28 | -------------------------------------------------------------------------------- /examples/laptop-and-vm-with-systemd/daemon.ne2: -------------------------------------------------------------------------------- 1 | # Add mouse & keyboard 2 | device add my-mouse /dev/input/by-id/usb-MyCoolMouse-if01-event-mouse 3 | device add my-kbd /dev/input/by-id/usb-MyCoolKeyboard-event-kbd 4 | 5 | # Add my the laptop toutput: 6 | output add laptop exec:ssh myuser@mylaptop netevent create 7 | 8 | # Create local device clones, with recognizable names 9 | device rename my-mouse vm0-mouse 10 | device rename my-kbd vm0-kbd 11 | # Run a 'create' in the background (--daemon) so devices don't vanish if the 12 | # main daemon is stopped (since qemu's '-object input-linux' won't find it 13 | # again at runtime automatically, although we could instead send hmp commands 14 | # to re-add them at startup as an alternative setup) 15 | # Since we want to be able to reconnect, let it use a socket (--listen=@vm0), 16 | # tell it to accept new connections after EOF (--on-close=accept) and that new 17 | # connections should simply resume existing input devices (--duplicates=resume) 18 | # and finally that it should try to connect to an already existing instance 19 | # (--connect) before starting a new one. 20 | output add vm0 exec:netevent create --duplicates=resume --on-close=accept --listen=@vm0 --connect --daemonize 21 | # Afterwards we can reset the names, but this is optional as it only affects 22 | # netevent's internal state of the device names used when adding new outputs. 23 | device reset-name my-mouse 24 | device reset-name my-kbd 25 | 26 | # Add hotkeys to switch between host, laptop and the VM 27 | hotkey add my-kbd key:188:1 use vm0\; grab-devices on\; write-events on 28 | hotkey add my-kbd key:188:0 nop 29 | hotkey add my-kbd key:187:1 use laptop\; grab-devices on\; write-events on 30 | hotkey add my-kbd key:187:0 nop 31 | hotkey add my-kbd key:186:1 grab-devices off\; write-events off 32 | hotkey add my-kbd key:186:0 nop 33 | -------------------------------------------------------------------------------- /examples/laptop-and-vm-with-systemd/start-vm.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ### Variant A: 4 | ### If we're using the udev rules & systemd service file: 5 | ### dev-input-by\x2dname-vm0\x2dkbd.device 6 | ### and dev-input-by\x2dname-vm0\x2dmouse.device 7 | mouse_device="/dev/input/by-name/vm0-mouse" 8 | keyboard_device="/dev/input/by-name/vm0-kbd" 9 | ### 10 | ### 11 | ### 12 | ### Variant B: 13 | ### Alternatively: 14 | ### When not using udev rules + systemd service files, find the devices by 15 | ### name via sysfs: 16 | for i in /sys/class/input/event*; do 17 | name="$(<$i/device/name)" 18 | if [[ $name = vm0-kbd ]]; then 19 | keyboard_device="/dev/${i#/sys/class/}" 20 | elif [[ $name = vm0-mouse ]]; then 21 | mouse_device="/dev/${i#/sys/class/}" 22 | fi 23 | done 24 | if [[ -z $keyboard_device || -z $mouse_device ]]; then 25 | die "usage: vm0-kbd/mouse missing" 26 | fi 27 | ### 28 | ### 29 | ### 30 | 31 | exec qemu-system-x86_64 \ 32 | \ 33 | -device usb-tablet \ 34 | -object "input-linux,id=ev-kbd,evdev=$keyboard_device,repeat=on" \ 35 | -object "input-linux,id=ev-mouse,evdev=$mouse_device" 36 | 37 | # Be aware of issues with repeat=on in some qemu versions... 38 | -------------------------------------------------------------------------------- /examples/laptop-and-vm-with-systemd/vm0.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # Note that you can use %I instead of the fixed VM name '0'. 3 | Description=VM 0 4 | After=network.target 5 | # Also note that these could be added via drop-in vm@name.sevice.d/input.conf 6 | # files 7 | Requires=dev-input-by\x2dname-vm0\x2dkbd.device 8 | Requires=dev-input-by\x2dname-vm0\x2dmouse.device 9 | 10 | [Service] 11 | Type=forking 12 | ExecStart=/bin/bash /my/vm/vm-start.sh 13 | ExecStop=/bin/bash /some/stop/qemu/hmp/command 14 | User=vm-user 15 | Group=vm-user 16 | LimitMEMLOCK=infinity 17 | -------------------------------------------------------------------------------- /examples/simple.ne2: -------------------------------------------------------------------------------- 1 | # Add mouse & keyboard 2 | device add my-mouse /dev/input/by-id/usb-MyCoolMouse-if01-event-mouse 3 | device add my-kbd /dev/input/by-id/usb-MyCoolKeyboard-event-kbd 4 | 5 | # Add toggle hotkey (on press, and ignore the release event) 6 | hotkey add my-kbd key:189:1 grab-devices toggle\; write-events toggle 7 | hotkey add my-kbd key:189:0 nop 8 | 9 | # Add my usual output: 10 | output add laptop exec:ssh myuser@mylaptop netevent create 11 | # Select the output to write to 12 | use laptop 13 | -------------------------------------------------------------------------------- /src/bitfield.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include "main.h" 15 | #include "bitfield.h" 16 | 17 | void 18 | Bits::resize(size_t bitcount) 19 | { 20 | if (!bitcount) { 21 | ::free(data_); 22 | data_ = nullptr; 23 | bitcount_ = 0; 24 | return; 25 | } 26 | 27 | auto np = ::realloc(data_, (bitcount+7)/8); 28 | if (!np) 29 | throw ErrnoException("allocation failed"); 30 | data_ = reinterpret_cast(np); 31 | auto oldsize = byte_size(); 32 | auto newsize = byte_size(bitcount); 33 | if (oldsize < newsize) 34 | ::memset(data_ + oldsize, 0, newsize-oldsize); 35 | bitcount_ = bitcount; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/bitfield.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | 10 | #include 11 | 12 | struct Bits { 13 | Bits(); 14 | Bits(const Bits&) = delete; // this needs to be explicitly dup()ed 15 | Bits(Bits&& o); 16 | ~Bits(); 17 | Bits(size_t size); 18 | 19 | static constexpr size_t byte_size(size_t bitcount) noexcept; 20 | 21 | size_t size() const noexcept; 22 | size_t byte_size() const noexcept; 23 | uint8_t* data() noexcept; 24 | void resize(size_t bitcount); 25 | void resizeNE1Compat(size_t bitcount); 26 | void shrinkTo(size_t bitcount); 27 | 28 | // unsafe interface for temporarily! changing the count 29 | void setBitCount(size_t bitcount); 30 | 31 | struct BitAccess { 32 | BitAccess() = delete; 33 | 34 | explicit BitAccess(Bits *owner, size_t index); 35 | size_t index() const; 36 | 37 | operator bool() const; 38 | // iterating should include the iterator... 39 | BitAccess& operator*(); 40 | BitAccess& operator=(bool on); 41 | // This also serves as iterator: 42 | BitAccess& operator++(); 43 | BitAccess& operator--(); 44 | 45 | bool operator!=(const BitAccess& other); 46 | bool operator<(const BitAccess& other); 47 | 48 | private: 49 | Bits *owner_; 50 | size_t index_; 51 | }; 52 | 53 | BitAccess operator[](size_t idx); 54 | BitAccess begin(); 55 | BitAccess end(); 56 | Bits dup() const; 57 | 58 | Bits& operator=(Bits&& other); 59 | 60 | private: 61 | size_t bitcount_; 62 | uint8_t *data_; 63 | }; 64 | 65 | inline 66 | Bits::Bits() 67 | : bitcount_(0) 68 | , data_(nullptr) 69 | {} 70 | 71 | inline 72 | Bits::Bits(Bits&& o) 73 | : bitcount_(o.bitcount_) 74 | , data_(o.data_) 75 | { 76 | o.bitcount_ = 0; 77 | o.data_ = nullptr; 78 | } 79 | 80 | inline 81 | Bits::~Bits() 82 | { 83 | ::free(data_); 84 | } 85 | 86 | inline 87 | Bits::Bits(size_t size) 88 | : Bits() 89 | { 90 | resize(size); 91 | } 92 | 93 | inline size_t 94 | Bits::size() const noexcept 95 | { 96 | return bitcount_; 97 | } 98 | 99 | inline constexpr size_t 100 | Bits::byte_size(size_t bitcount) noexcept 101 | { 102 | return (bitcount+7) / 8; 103 | } 104 | 105 | inline size_t 106 | Bits::byte_size() const noexcept 107 | { 108 | return byte_size(bitcount_); 109 | } 110 | 111 | inline uint8_t* 112 | Bits::data() noexcept 113 | { 114 | return data_; 115 | } 116 | 117 | inline void 118 | Bits::resizeNE1Compat(size_t bitcount) 119 | { 120 | // this is not the same as 8 + bitcount because of integer 121 | // math, (bitcount/8)*8 aligns down! 122 | resize(8 * (1+bitcount/8)); 123 | } 124 | 125 | inline void 126 | Bits::shrinkTo(size_t bitcount) 127 | { 128 | if (bitcount < bitcount_) 129 | bitcount_ = bitcount; 130 | } 131 | 132 | // unsafe interface for when you temporarily change the count 133 | inline void 134 | Bits::setBitCount(size_t bitcount) 135 | { 136 | bitcount_ = bitcount; 137 | } 138 | 139 | inline 140 | Bits::BitAccess::BitAccess(Bits *owner, size_t index) 141 | : owner_(owner) 142 | , index_(index) 143 | {} 144 | 145 | inline size_t 146 | Bits::BitAccess::index() const 147 | { 148 | return index_; 149 | } 150 | 151 | inline 152 | Bits::BitAccess::operator bool() const 153 | { 154 | #pragma clang diagnostic push 155 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 156 | return owner_->data_[index_/8] & (1<<(index_&7)); 157 | #pragma clang diagnostic pop 158 | } 159 | 160 | // iterating should include the iterator... 161 | inline Bits::BitAccess& 162 | Bits::BitAccess::operator*() 163 | { 164 | return (*this); 165 | } 166 | 167 | inline Bits::BitAccess& 168 | Bits::BitAccess::operator=(bool on) 169 | { 170 | #pragma clang diagnostic push 171 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 172 | if (on) 173 | owner_->data_[index_/8] |= (1<<(index_&7)); 174 | else 175 | owner_->data_[index_/8] &= ~(1<<(index_&7)); 176 | #pragma clang diagnostic pop 177 | return (*this); 178 | } 179 | 180 | // This also serves as iterator: 181 | inline Bits::BitAccess& 182 | Bits::BitAccess::operator++() 183 | { 184 | ++index_; 185 | return (*this); 186 | } 187 | 188 | inline Bits::BitAccess& 189 | Bits::BitAccess::operator--() 190 | { 191 | --index_; 192 | return (*this); 193 | } 194 | 195 | inline bool 196 | Bits::BitAccess::operator!=(const BitAccess& other) 197 | { 198 | return owner_ != other.owner_ || 199 | index_ != other.index_; 200 | } 201 | 202 | inline bool 203 | Bits::BitAccess::operator<(const BitAccess& other) 204 | { 205 | return index_ < other.index_; 206 | } 207 | 208 | inline Bits::BitAccess 209 | Bits::operator[](size_t idx) 210 | { 211 | return BitAccess { this, idx }; 212 | } 213 | 214 | inline Bits::BitAccess 215 | Bits::begin() 216 | { 217 | return BitAccess { this, 0 }; 218 | } 219 | 220 | inline Bits::BitAccess 221 | Bits::end() 222 | { 223 | return BitAccess { this, bitcount_ }; 224 | } 225 | 226 | inline Bits& 227 | Bits::operator=(Bits&& other) 228 | { 229 | bitcount_ = other.bitcount_; 230 | data_ = other.data_; 231 | other.bitcount_ = 0; 232 | other.data_ = nullptr; 233 | return (*this); 234 | } 235 | 236 | inline Bits 237 | Bits::dup() const 238 | { 239 | Bits d { bitcount_ }; 240 | ::memcpy(d.data(), data_, byte_size()); 241 | return d; 242 | } 243 | -------------------------------------------------------------------------------- /src/daemon.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | #include 19 | using std::vector; 20 | using std::map; 21 | 22 | #include "main.h" 23 | 24 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 25 | 26 | #define OUTPUT_CHANGED_EVENT "output-changed" 27 | #define DEVICE_LOST_EVENT "device-lost" 28 | #define GRAB_CHANGED_EVENT "grab-changed" 29 | #define WRITE_CHANGED_EVENT "write-changed" 30 | 31 | static void 32 | usage_daemon [[noreturn]] (FILE *out, int exit_status) 33 | { 34 | ::fprintf(out, 35 | "usage: netevent daemon [options] SOCKETNAME\n" 36 | "options:\n" 37 | " -h, --help show this help message\n" 38 | " -s, --source=FILE run commands from FILE on startup\n" 39 | ); 40 | ::exit(exit_status); 41 | } 42 | 43 | struct FDCallbacks { 44 | function onRead; 45 | function onHUP; 46 | function onError; 47 | function onRemove; 48 | }; 49 | 50 | struct Command { 51 | int client_; 52 | string command_; 53 | }; 54 | 55 | struct Input { 56 | uint16_t id_; 57 | uniq device_; 58 | }; 59 | 60 | struct FILEHandle { 61 | FILE *file_; 62 | FILEHandle(FILE *file) : file_(file) {} 63 | FILEHandle(FILEHandle&& o) : file_(o.file_) { 64 | o.file_ = nullptr; 65 | } 66 | FILEHandle(const FILEHandle&) = delete; 67 | ~FILEHandle() { 68 | if (file_) 69 | ::fclose(file_); 70 | } 71 | }; 72 | 73 | struct HotkeyDef { 74 | uint16_t device; 75 | uint16_t type; 76 | uint16_t code; 77 | int32_t value; 78 | 79 | constexpr bool operator<(const HotkeyDef& r) const { 80 | return (device < r.device || (device == r.device && 81 | (type < r.type || (type == r.type && 82 | (code < r.code || (code == r.code && 83 | (value < r.value))))))); 84 | } 85 | }; 86 | 87 | #pragma clang diagnostic push 88 | #pragma clang diagnostic ignored "-Wexit-time-destructors" 89 | #pragma clang diagnostic ignored "-Wglobal-constructors" 90 | static bool gQuit = false; 91 | static vector gFDRemoveQueue; 92 | static vector gFDAddQueue; 93 | static map gFDCBs; 94 | static map gCommandClients; 95 | static vector gCommandQueue; 96 | static vector gInputIDFreeList; 97 | static map gInputs; 98 | static map gOutputs; 99 | static struct { 100 | int fd = -1; 101 | string name; 102 | } gCurrentOutput; 103 | static bool gWrite = false; 104 | static bool gGrab = false; 105 | static map gHotkeys; 106 | static map gEventCommands; 107 | #pragma clang diagnostic pop 108 | 109 | #if 0 110 | template 111 | static void 112 | vectorRemove(vector& vec, T&& value) 113 | { 114 | auto iter = vec.find(value); 115 | if (iter != vec.end()) 116 | vec.erase(iter); 117 | } 118 | #endif 119 | 120 | static void parseClientCommand(int clientfd, const char *cmd, size_t length); 121 | 122 | static void 123 | daemon_preExec() 124 | { 125 | gFDCBs.clear(); 126 | } 127 | 128 | #if 0 129 | template 130 | static void 131 | mapRemove(map& m, T key) 132 | { 133 | auto iter = m.find(key); 134 | if (iter != m.end()) 135 | m.erase(iter); 136 | } 137 | #endif 138 | 139 | static void 140 | removeFD(int fd) 141 | { 142 | if (fd < 0) 143 | return; 144 | if (std::find(gFDRemoveQueue.begin(), gFDRemoveQueue.end(), fd) 145 | == gFDRemoveQueue.end()) 146 | { 147 | gFDRemoveQueue.push_back(fd); 148 | } 149 | } 150 | 151 | static void 152 | removeOutput(int fd) { 153 | removeFD(fd); 154 | } 155 | 156 | static void 157 | removeOutput(const string& name) 158 | { 159 | auto iter = gOutputs.find(name); 160 | if (iter == gOutputs.end()) 161 | throw MsgException("no such output: %s", name.c_str()); 162 | removeOutput(iter->second.fd()); 163 | } 164 | 165 | static bool 166 | writeToOutput(int fd, const void *data, size_t size) 167 | { 168 | if (::write(fd, data, size) != static_cast(size)) { 169 | ::fprintf(stderr, "error writing to output, dropping\n"); 170 | removeOutput(fd); 171 | return false; 172 | } 173 | return true; 174 | } 175 | 176 | static void 177 | announceDeviceRemoval(Input& input) 178 | { 179 | NE2Packet pkt = {}; 180 | ::memset(reinterpret_cast(&pkt), 0, sizeof(pkt)); 181 | pkt.cmd = htobe16(uint16_t(NE2Command::RemoveDevice)); 182 | pkt.remove_device.id = htobe16(input.id_); 183 | 184 | for (auto& oi: gOutputs) 185 | (void)writeToOutput(oi.second.fd(), &pkt, sizeof(pkt)); 186 | } 187 | 188 | static void 189 | cleanupDeviceHotkeys(uint16_t id) 190 | { 191 | for (auto i = gHotkeys.begin(); i != gHotkeys.end();) { 192 | if (i->first.device == id) 193 | i = gHotkeys.erase(i); 194 | else 195 | ++i; 196 | } 197 | } 198 | 199 | static void 200 | processRemoveQueue() 201 | { 202 | for (int fd : gFDRemoveQueue) { 203 | auto cbs = gFDCBs.find(fd); 204 | if (cbs == gFDCBs.end()) 205 | throw Exception("FD without cleanup callback"); 206 | cbs->second.onRemove(); 207 | gFDCBs.erase(cbs); 208 | } 209 | 210 | gFDRemoveQueue.clear(); 211 | } 212 | 213 | static void 214 | disconnectClient(int fd) 215 | { 216 | removeFD(fd); 217 | } 218 | 219 | static void 220 | finishClientRemoval(int fd) { 221 | auto iter = gCommandClients.find(fd); 222 | if (iter != gCommandClients.end()) { 223 | gCommandClients.erase(iter); 224 | return; 225 | } 226 | throw Exception("finishClientRemoval: failed to find fd"); 227 | } 228 | 229 | static void 230 | queueCommand(int clientfd, string line) 231 | { 232 | gCommandQueue.emplace_back(Command{clientfd, std::move(line)}); 233 | } 234 | 235 | 236 | static void 237 | readCommand(FILE *file) 238 | { 239 | char *line = nullptr; 240 | scope (exit) { ::free(line); }; 241 | size_t len = 0; 242 | errno = 0; 243 | auto got = ::getline(&line, &len, file); 244 | if (got < 0) { 245 | if (errno) 246 | ::fprintf(stderr, 247 | "error reading from command client: %s\n", 248 | ::strerror(errno)); 249 | disconnectClient(::fileno(file)); 250 | return; 251 | } 252 | if (got == 0) { // EOF 253 | disconnectClient(::fileno(file)); 254 | return; 255 | } 256 | 257 | int fd = ::fileno(file); 258 | queueCommand(fd, line); 259 | } 260 | 261 | static void 262 | addFD(int fd, short events = POLLIN | POLLHUP | POLLERR) 263 | { 264 | gFDAddQueue.emplace_back(pollfd { fd, events, 0 }); 265 | } 266 | 267 | static void 268 | newCommandClient(Socket& server) 269 | { 270 | IOHandle h = server.accept(); 271 | int fd = h.fd(); 272 | 273 | FILE *buffd = ::fdopen(fd, "rb"); 274 | if (!buffd) 275 | throw ErrnoException("fdopen() failed"); 276 | FILEHandle bufhandle { buffd }; 277 | (void)h.release(); 278 | 279 | addFD(fd); 280 | gFDCBs[fd] = FDCallbacks { 281 | [buffd]() { readCommand(buffd); }, 282 | [fd]() { disconnectClient(fd); }, 283 | [fd]() { disconnectClient(fd); }, 284 | [fd]() { finishClientRemoval(fd); }, 285 | }; 286 | gCommandClients.emplace(fd, std::move(bufhandle)); 287 | } 288 | 289 | #pragma clang diagnostic push 290 | #pragma clang diagnostic ignored "-Wformat-nonliteral" 291 | static void 292 | toClient(int fd, const char *fmt, ...) 293 | { 294 | char buf[4096]; 295 | va_list ap; 296 | va_start(ap, fmt); 297 | auto length = ::vsnprintf(buf, sizeof(buf), fmt, ap); 298 | int err = errno; 299 | va_end(ap); 300 | if (length <= 0) { 301 | ::fprintf(stderr, "faield to format client response: %s\n", 302 | ::strerror(err)); 303 | disconnectClient(fd); 304 | } 305 | if (fd < 0) 306 | ::fwrite(buf, size_t(length), 1, stderr); 307 | else if (::write(fd, buf, size_t(length)) != length) { 308 | ::fprintf(stderr, 309 | "failed to write response to client command\n"); 310 | disconnectClient(fd); 311 | } 312 | } 313 | #pragma clang diagnostic pop 314 | 315 | static uint16_t 316 | getNextInputID() 317 | { 318 | if (gInputs.size() > UINT16_MAX) 319 | throw Exception( 320 | "too many input devices (... the heck are you doing?)"); 321 | 322 | if (gInputIDFreeList.empty()) 323 | return static_cast(gInputs.size()); 324 | 325 | auto next = gInputIDFreeList.back(); 326 | gInputIDFreeList.pop_back(); 327 | return static_cast(next); 328 | } 329 | 330 | static void 331 | freeInputID(uint16_t id) 332 | { 333 | gInputIDFreeList.push_back(id); 334 | } 335 | 336 | static void 337 | closeDevice(InDevice *device) 338 | { 339 | removeFD(device->fd()); 340 | } 341 | 342 | static void 343 | finishDeviceRemoval(InDevice *device) 344 | { 345 | for (auto i = gInputs.begin(); i != gInputs.end(); ++i) { 346 | if (i->second.device_.get() == device) { 347 | if (!device->persistent()) 348 | announceDeviceRemoval(i->second); 349 | cleanupDeviceHotkeys(i->second.id_); 350 | gInputs.erase(i); 351 | return; 352 | } 353 | } 354 | throw Exception("finishDeviceRemoval: failed to find device"); 355 | } 356 | 357 | static void 358 | fireEvent(int clientfd, const char *event) 359 | { 360 | auto iter = gEventCommands.find(event); 361 | if (iter == gEventCommands.end()) 362 | return; 363 | auto& cmd = iter->second; 364 | parseClientCommand(clientfd, cmd.c_str(), cmd.length()); 365 | } 366 | 367 | static void 368 | setEnvVar(const char *name, const char *value) 369 | { 370 | if (::setenv(name, value, 1) != 0) { 371 | ::fprintf(stderr, 372 | "error setting environment variable %s to %s: %s\n", 373 | name, value, ::strerror(errno)); 374 | } 375 | } 376 | 377 | static void 378 | useOutput(int clientfd, const string& name) 379 | { 380 | auto iter = gOutputs.find(name); 381 | if (iter == gOutputs.end()) 382 | throw MsgException("no such output: %s", name.c_str()); 383 | gCurrentOutput.fd = iter->second.fd(); 384 | gCurrentOutput.name = name; 385 | 386 | setEnvVar("NETEVENT_OUTPUT_NAME", name.c_str()); 387 | fireEvent(clientfd, OUTPUT_CHANGED_EVENT); 388 | } 389 | 390 | static bool 391 | tryHotkey(uint16_t device, uint16_t type, uint16_t code, int32_t value) 392 | { 393 | if (type >= EV_CNT) 394 | return false; 395 | HotkeyDef def { device, type, code, value }; 396 | auto cmd = gHotkeys.find(def); 397 | if (cmd == gHotkeys.end()) 398 | return false; 399 | queueCommand(-1, cmd->second); 400 | return true; 401 | } 402 | 403 | static void 404 | writeEvents(int clientfd, bool on) 405 | { 406 | gWrite = on; 407 | setEnvVar("NETEVENT_WRITING", on ? "1" : "0"); 408 | fireEvent(clientfd, WRITE_CHANGED_EVENT); 409 | } 410 | 411 | static void 412 | grab(int clientfd, bool on) 413 | { 414 | gGrab = on; 415 | setEnvVar("NETEVENT_GRABBING", on ? "1" : "0"); 416 | for (auto& i: gInputs) 417 | i.second.device_->grab(on); 418 | fireEvent(clientfd, GRAB_CHANGED_EVENT); 419 | } 420 | 421 | static void 422 | lostCurrentOutput() 423 | { 424 | gCurrentOutput.fd = -1; 425 | gCurrentOutput.name = ""; 426 | if (gWrite) 427 | writeEvents(-1, false); 428 | if (gGrab) 429 | grab(-1, false); 430 | } 431 | 432 | static void 433 | readFromDevice(InDevice *device, uint16_t id) 434 | { 435 | NE2Packet pkt = {}; 436 | try { 437 | if (!device->read(&pkt.event.event)) { 438 | fireEvent(-1, DEVICE_LOST_EVENT); 439 | return closeDevice(device); 440 | } 441 | } catch (const Exception& ex) { 442 | ::fprintf(stderr, "error reading device: %s\n", ex.what()); 443 | return closeDevice(device); 444 | } 445 | 446 | const auto& ev = pkt.event.event; 447 | if (tryHotkey(id, ev.type, ev.code, ev.value)) 448 | return; 449 | 450 | if (gCurrentOutput.fd == -1) 451 | return; 452 | 453 | if (!gWrite) 454 | return; 455 | 456 | pkt.cmd = htobe16(uint16_t(NE2Command::DeviceEvent)); 457 | pkt.event.id = htobe16(id); 458 | pkt.event.event.toNet(); 459 | if (mustWrite(gCurrentOutput.fd, &pkt, sizeof(pkt))) 460 | return; 461 | 462 | // on error we drop the output: 463 | ::fprintf(stderr, "error writing to output %s: %s\n", 464 | gCurrentOutput.name.c_str(), ::strerror(errno)); 465 | removeOutput(gCurrentOutput.fd); 466 | lostCurrentOutput(); 467 | } 468 | 469 | static bool 470 | announceDevice(Input& input, int fd) 471 | { 472 | try { 473 | input.device_->writeNE2AddDevice(fd, input.id_); 474 | return true; 475 | } catch (const Exception& ex) { 476 | ::fprintf(stderr, 477 | "error creating device on output, dropping: %s\n", 478 | ex.what()); 479 | removeOutput(fd); 480 | return false; 481 | } 482 | } 483 | 484 | static void 485 | announceDevice(Input& input) 486 | { 487 | for (auto& oi: gOutputs) 488 | announceDevice(input, oi.second.fd()); 489 | } 490 | 491 | static void 492 | announceAllDevices(int fd) 493 | { 494 | for (auto& i: gInputs) { 495 | if (!announceDevice(i.second, fd)) 496 | break; 497 | } 498 | } 499 | 500 | static void 501 | addDevice(const string& name, const char *path) 502 | { 503 | if (gInputs.find(name) != gInputs.end()) 504 | throw MsgException("output already exists: %s", name.c_str()); 505 | 506 | auto id = getNextInputID(); 507 | try { 508 | Input input { id, uniq { new InDevice { path } } }; 509 | InDevice *weakdevptr = input.device_.get(); 510 | int fd = weakdevptr->fd(); 511 | 512 | announceDevice(input); 513 | 514 | addFD(fd); 515 | gFDCBs[fd] = FDCallbacks { 516 | [=]() { readFromDevice(weakdevptr, id); }, 517 | [=]() { 518 | fireEvent(-1, DEVICE_LOST_EVENT); 519 | closeDevice(weakdevptr); 520 | }, 521 | [=]() { 522 | fireEvent(-1, DEVICE_LOST_EVENT); 523 | closeDevice(weakdevptr); 524 | }, 525 | [=]() { finishDeviceRemoval(weakdevptr); }, 526 | }; 527 | gInputs.emplace(name, std::move(input)); 528 | } catch (const std::exception&) { 529 | freeInputID(id); 530 | throw; 531 | } 532 | } 533 | 534 | static InDevice* 535 | findDevice(const string& name) 536 | { 537 | auto iter = gInputs.find(name); 538 | if (iter == gInputs.end()) 539 | throw MsgException("no such device: %s", name.c_str()); 540 | return iter->second.device_.get(); 541 | } 542 | 543 | static void 544 | removeDevice(const string& name) 545 | { 546 | closeDevice(findDevice(name)); 547 | } 548 | 549 | static void 550 | finishOutputRemoval(int fd) 551 | { 552 | if (gCurrentOutput.fd == fd) 553 | lostCurrentOutput(); 554 | for (auto i = gOutputs.begin(); i != gOutputs.end(); ++i) { 555 | if (i->second.fd() == fd) { 556 | gOutputs.erase(i); 557 | return; 558 | } 559 | } 560 | throw MsgException("finishOutputRemove: faile dot find fd"); 561 | } 562 | 563 | // TODO: 564 | // - tcp:IP PORT 565 | // - tcp:HOSTNAME PORT 566 | // - tcp:[IP] PORT 567 | // - tcp:[HOSTNAME] PORT 568 | // Unsafe but still useful. 569 | // - tcp:DESTINATION PORT CERT KEY CACERTorPATH 570 | // - tcp:[DESTINATION] PORT CERT KEY CACERTorPATH 571 | // Annoying & require an ssl lib but more useful than the non-ssl 572 | // variant... 573 | static void 574 | addOutput_Finish(const string& name, IOHandle handle, bool skip_announce) 575 | { 576 | int fd = handle.fd(); 577 | writeHello(fd); 578 | if (!skip_announce) 579 | announceAllDevices(fd); 580 | gOutputs.emplace(name, std::move(handle)); 581 | gFDCBs.emplace(fd, FDCallbacks { 582 | [fd]() { 583 | ::fprintf(stderr, "onRead on output"); 584 | removeFD(fd); 585 | }, 586 | [fd]() { removeFD(fd); }, 587 | [fd]() { removeFD(fd); }, 588 | [fd]() { finishOutputRemoval(fd); }, 589 | }); 590 | } 591 | 592 | static IOHandle 593 | addOutput_Open(const char *path) 594 | { 595 | // Use O_NDELAY to not hang on FIFOs. FIFOs should already be waiting 596 | // for our connection, we remove O_NONBLOCK below again. 597 | int fd = ::open(path, O_WRONLY | O_NDELAY | O_CLOEXEC); 598 | if (fd < 0) 599 | throw ErrnoException("open(%s)", path); 600 | IOHandle handle { fd }; 601 | 602 | int flags = ::fcntl(fd, F_GETFL); 603 | if (flags == -1) 604 | throw ErrnoException("failed to get output flags"); 605 | if (::fcntl(fd, F_SETFL, flags & ~(O_NONBLOCK)) != 0) 606 | throw ErrnoException("failed to remove O_NONBLOCK"); 607 | 608 | return handle; 609 | } 610 | 611 | static IOHandle 612 | addOutput_Exec(const char *path) 613 | { 614 | int pfd[2]; 615 | if (::pipe2(pfd, O_CLOEXEC) != 0) 616 | throw ErrnoException("pipe() failed"); 617 | IOHandle pr { pfd[0] }; 618 | IOHandle pw { pfd[1] }; 619 | 620 | pid_t pid = ::fork(); 621 | if (pid == -1) 622 | throw ErrnoException("fork() failed"); 623 | 624 | if (!pid) { 625 | pw.close(); 626 | pr.cloexec(false); // We need this one in our subprocess! 627 | 628 | if (pr.fd() != 0) { 629 | if (::dup2(pr.fd(), 0) != 0) { 630 | ::perror("dup2"); 631 | ::exit(-1); 632 | } 633 | pr.close(); 634 | } 635 | daemon_preExec(); 636 | ::execlp("/bin/sh", "/bin/sh", "-c", path, nullptr); 637 | ::perror("exec() failed"); 638 | ::exit(-1); 639 | } 640 | pr.close(); 641 | 642 | return pw; 643 | } 644 | 645 | static IOHandle 646 | addOutput_Unix(const char *path) 647 | { 648 | Socket socket; 649 | if (path[0] == '@') 650 | socket.connectUnix(path+1); 651 | else 652 | socket.connectUnix(path); 653 | return socket.intoIOHandle(); 654 | } 655 | 656 | static void 657 | addOutput(const string& name, const char *path, bool skip_announce) 658 | { 659 | if (gOutputs.find(name) != gOutputs.end()) 660 | throw MsgException("output already exists: %s", name.c_str()); 661 | 662 | IOHandle handle; 663 | if (::strncmp(path, "exec:", sizeof("exec:")-1) == 0) 664 | handle = addOutput_Exec(path+(sizeof("exec:")-1)); 665 | else if (::strncmp(path, "unix:", sizeof("unix:")-1) == 0) 666 | handle = addOutput_Unix(path+(sizeof("unix:")-1)); 667 | else 668 | handle = addOutput_Open(path); 669 | 670 | return addOutput_Finish(name, std::move(handle), skip_announce); 671 | } 672 | 673 | static void 674 | addOutput(int clientfd, const vector& args) 675 | { 676 | bool skip_announce = false; 677 | size_t at = 2; 678 | if (args.size() > at && args[at] == "--resume") { 679 | ++at; 680 | skip_announce = true; 681 | } 682 | 683 | if (at+1 >= args.size()) 684 | throw Exception( 685 | "'output add' requires a name and a path"); 686 | 687 | const string& name = args[at++]; 688 | 689 | string cmd = join(' ', args.begin()+ssize_t(at), args.end()); 690 | addOutput(name, cmd.c_str(), skip_announce); 691 | toClient(clientfd, "added output %s\n", name.c_str()); 692 | } 693 | 694 | static void 695 | writeCommand(int clientfd, const char *state) 696 | { 697 | if (parseBool(&gWrite, state)) { 698 | // nothing 699 | } 700 | else if (!::strcasecmp(state, "toggle")) 701 | { 702 | gWrite = !gWrite; 703 | } 704 | else 705 | throw MsgException("unknown write state: %s", state); 706 | writeEvents(clientfd, gWrite); 707 | } 708 | 709 | static void 710 | grabCommand(int clientfd, const char *state) 711 | { 712 | if (parseBool(&gGrab, state)) { 713 | // nothing 714 | } 715 | else if (!::strcasecmp(state, "toggle")) 716 | { 717 | gGrab = !gGrab; 718 | } 719 | else 720 | throw MsgException("unknown grab state: %s", state); 721 | grab(clientfd, gGrab); 722 | } 723 | 724 | static void 725 | addHotkey(uint16_t device, uint16_t type, uint16_t code, int32_t value, 726 | string command) 727 | { 728 | if (type >= EV_CNT) 729 | throw MsgException("unknown event type: %u", type); 730 | 731 | 732 | gHotkeys[HotkeyDef{device, type, code, value}] = std::move(command); 733 | } 734 | 735 | static void 736 | removeHotkey(uint16_t device, uint16_t type, uint16_t code, int32_t value) 737 | { 738 | if (type >= EV_CNT) 739 | throw MsgException("unknown event type: %u", type); 740 | gHotkeys.erase(HotkeyDef{device, type, code, value}); 741 | } 742 | 743 | static void 744 | shellCommand(const char *cmd, bool background) 745 | { 746 | pid_t pid = ::fork(); 747 | if (pid == -1) 748 | throw ErrnoException("fork() failed"); 749 | if (!pid) { 750 | daemon_preExec(); 751 | ::execlp("/bin/sh", "/bin/sh", "-c", cmd, nullptr); 752 | ::perror("exec() failed"); 753 | ::exit(-1); 754 | } 755 | if (background) 756 | return; 757 | 758 | int status = 0; 759 | do { 760 | // wait 761 | } while (::waitpid(pid, &status, 0) != pid); 762 | } 763 | 764 | static inline constexpr bool 765 | isWhite(char c) 766 | { 767 | return c == ' ' || c == '\t' || c == '\r' || c == '\n'; 768 | } 769 | 770 | static bool 771 | skipWhite(const char* &p) 772 | { 773 | while (isWhite(*p)) 774 | ++p; 775 | return *p != 0; 776 | } 777 | 778 | static string 779 | parseString(const char* &p) 780 | { 781 | string str; 782 | char quote = *p++; 783 | bool escape = false; 784 | while (*p && *p != quote) { 785 | if (escape) { 786 | escape = false; 787 | switch (*p) { 788 | case '\\': str.append(1, '\\'); break; 789 | case 't': str.append(1, '\t'); break; 790 | case 'r': str.append(1, '\r'); break; 791 | case 'n': str.append(1, '\n'); break; 792 | case 'f': str.append(1, '\f'); break; 793 | case 'v': str.append(1, '\v'); break; 794 | case 'b': str.append(1, '\b'); break; 795 | case '"': str.append(1, '\"'); break; 796 | case '\'': str.append(1, '\''); break; 797 | case '0': str.append(1, '\0'); break; 798 | default: 799 | str.append(1, '\\'); 800 | str.append(1, *p); 801 | break; 802 | } 803 | } else if (*p == '\\') { 804 | escape = true; 805 | } else { 806 | str.append(1, *p); 807 | } 808 | ++p; 809 | } 810 | if (*p) // skip quote 811 | ++p; 812 | return str; 813 | } 814 | 815 | static void 816 | clientCommand_Device(int clientfd, const vector& args) 817 | { 818 | if (args.size() < 2) 819 | throw Exception("'device': missing subcommand"); 820 | 821 | if (args[1] == "add") { 822 | if (args.size() != 4) 823 | throw Exception( 824 | "'device add' requires a name and a path"); 825 | addDevice(args[2], args[3].c_str()); 826 | toClient(clientfd, "added device %s\n", args[2].c_str()); 827 | } 828 | else if (args[1] == "remove") { 829 | if (args.size() != 3) 830 | throw Exception( 831 | "'device remove' requires a name"); 832 | removeDevice(args[2]); 833 | toClient(clientfd, "removing device %s\n", 834 | args[2].c_str()); 835 | } 836 | else if (args[1] == "rename") { 837 | if (args.size() != 4) 838 | throw Exception( 839 | "'device rename' requires a device and a name"); 840 | auto dev = findDevice(args[2]); 841 | dev->setName(args[3]); 842 | toClient(clientfd, "renamed device %s to %s\n", 843 | dev->realName().c_str(), args[3].c_str()); 844 | } 845 | else if (args[1] == "reset-name") { 846 | if (args.size() != 3) 847 | throw Exception( 848 | "'device reset-name' requires a device"); 849 | auto dev = findDevice(args[2]); 850 | dev->resetName(); 851 | toClient(clientfd, "reset name of device %s\n", 852 | dev->realName().c_str()); 853 | } 854 | else if (args[1] == "set-persistent") { 855 | if (args.size() != 4) 856 | throw Exception( 857 | "'device set-persistent' requires a device" 858 | " and a boolean"); 859 | auto dev = findDevice(args[2]); 860 | bool value; 861 | if (parseBool(&value, args[3].c_str())) { 862 | dev->persistent(value); 863 | if (value) 864 | toClient(clientfd, 865 | "device %s made persistent\n", 866 | args[2].c_str()); 867 | else 868 | toClient(clientfd, 869 | "device %s made removable\n", 870 | args[2].c_str()); 871 | } else { 872 | toClient(clientfd, "not a boolean: '%s'\n", 873 | args[3].c_str()); 874 | } 875 | } 876 | else 877 | throw MsgException("unknown device subcommand: %s", 878 | args[1].c_str()); 879 | } 880 | 881 | static void 882 | clientCommand_Output(int clientfd, const vector& args) 883 | { 884 | if (args.size() < 2) 885 | throw Exception("'output': missing subcommand"); 886 | 887 | if (args[1] == "add") { 888 | addOutput(clientfd, args); 889 | } 890 | else if (args[1] == "remove") { 891 | if (args.size() != 3) 892 | throw Exception( 893 | "'output remove' requires a name"); 894 | removeOutput(args[2]); 895 | toClient(clientfd, "removing output %s\n", 896 | args[2].c_str()); 897 | } 898 | else if (args[1] == "use") { 899 | if (args.size() != 3) 900 | throw Exception( 901 | "'output use' requires a name"); 902 | useOutput(clientfd, args[2]); 903 | toClient(clientfd, "output = %s\n", 904 | gCurrentOutput.name.c_str()); 905 | } 906 | else 907 | throw MsgException("unknown output subcommand: %s", 908 | args[1].c_str()); 909 | } 910 | 911 | static void 912 | clientCommand_Hotkey(int clientfd, const vector& args) 913 | { 914 | if (args.size() < 2) 915 | throw Exception("'hotkey': missing subcommand"); 916 | 917 | if (args[1] == "add") { 918 | if (args.size() < 5) 919 | throw Exception( 920 | "'hotkey add' requires" 921 | " a device, a hotkey and a command"); 922 | auto input = gInputs.find(args[2]); 923 | if (input == gInputs.end()) 924 | throw MsgException("no such device: %s", 925 | args[2].c_str()); 926 | 927 | const auto& hotkeydef = args[3]; 928 | auto dot1 = hotkeydef.find(':'); 929 | if (dot1 == hotkeydef.npos || dot1 >= hotkeydef.length()-1) 930 | throw MsgException("invalid hotkey definition: %s", 931 | hotkeydef.c_str()); 932 | auto dot2 = hotkeydef.find(':', dot1+1); 933 | if (dot2 == hotkeydef.npos || dot2 >= hotkeydef.length()-1) 934 | throw MsgException("invalid hotkey definition: %s", 935 | hotkeydef.c_str()); 936 | 937 | 938 | unsigned int type = String2EV(hotkeydef.c_str(), dot1); 939 | if (type == unsigned(-1)) 940 | throw MsgException("no such event type: %s", 941 | hotkeydef.c_str()); 942 | if (type > EV_MAX) 943 | throw MsgException("bad event type: %u", type); 944 | 945 | unsigned long code = 0xffff+1; 946 | if (!parseULong(&code, hotkeydef.c_str() + dot1+1, dot2-dot1-1) 947 | || code > 0xffff) 948 | throw MsgException("bad event code: %s", 949 | hotkeydef.c_str() + dot1+1); 950 | long value; 951 | if (!parseLong(&value, hotkeydef.c_str() + dot2+1, size_t(-1))) 952 | throw MsgException("bad event value: %s", 953 | hotkeydef.c_str() + dot2+1); 954 | 955 | string cmd = join(' ', args.begin()+4, args.end()); 956 | addHotkey(input->second.id_, 957 | uint16_t(type), uint16_t(code), int32_t(value), 958 | cmd.c_str()); 959 | toClient(clientfd, 960 | "added hotkey %u:%u:%i for device %u\n", 961 | type, code, value, input->second.id_); 962 | } 963 | else if (args[1] == "remove") { 964 | if (args.size() != 4) 965 | throw Exception( 966 | "'hotkey remove': requires device and event code"); 967 | 968 | auto input = gInputs.find(args[2]); 969 | if (input == gInputs.end()) 970 | throw MsgException("no such device: %s", 971 | args[2].c_str()); 972 | 973 | const auto& hotkeydef = args[3]; 974 | auto dot1 = hotkeydef.find(':'); 975 | if (dot1 == hotkeydef.npos || dot1 >= hotkeydef.length()-1) 976 | throw MsgException("invalid hotkey definition: %s", 977 | hotkeydef.c_str()); 978 | auto dot2 = hotkeydef.find(':', dot1+1); 979 | if (dot2 == hotkeydef.npos || dot2 >= hotkeydef.length()-1) 980 | throw MsgException("invalid hotkey definition: %s", 981 | hotkeydef.c_str()); 982 | 983 | 984 | unsigned int type = String2EV(hotkeydef.c_str(), dot1); 985 | if (type == unsigned(-1)) 986 | throw MsgException("no such event type: %s", 987 | hotkeydef.c_str()); 988 | if (type > EV_MAX) 989 | throw MsgException("bad event type: %u", type); 990 | 991 | unsigned long code = 0xffff+1; 992 | if (!parseULong(&code, hotkeydef.c_str() + dot1+1, dot2-dot1-1) 993 | || code > 0xffff) 994 | throw MsgException("bad event code: %s", 995 | hotkeydef.c_str() + dot1+1); 996 | long value; 997 | if (!parseLong(&value, hotkeydef.c_str() + dot2+1, size_t(-1))) 998 | throw MsgException("bad event value: %s", 999 | hotkeydef.c_str() + dot2+1); 1000 | 1001 | removeHotkey(input->second.id_, 1002 | uint16_t(type), uint16_t(code), int32_t(value)); 1003 | toClient(clientfd, 1004 | "removed hotkey %u:%u:%i for device %u\n", 1005 | type, code, value, input->second.id_); 1006 | 1007 | } 1008 | else 1009 | throw MsgException("unknown hotkey subcommand: %s", 1010 | args[1].c_str()); 1011 | } 1012 | 1013 | static void 1014 | clientCommand_Info(int clientfd, const vector& args) 1015 | { 1016 | (void)args; 1017 | 1018 | toClient(clientfd, "Grab-devices: %s\n", gGrab ? "on" : "off"); 1019 | toClient(clientfd, "Write-events: %s\n", gWrite ? "on" : "off"); 1020 | toClient(clientfd, "Inputs: %zu\n", gInputs.size()); 1021 | for (auto& i: gInputs) { 1022 | toClient(clientfd, " %u: %s: %i\n", 1023 | i.second.id_, 1024 | i.first.c_str(), 1025 | i.second.device_->fd()); 1026 | } 1027 | 1028 | toClient(clientfd, "Outputs: %zu\n", gOutputs.size()); 1029 | for (auto& i: gOutputs) { 1030 | toClient(clientfd, " %s: %i\n", 1031 | i.first.c_str(), 1032 | i.second.fd()); 1033 | } 1034 | 1035 | toClient(clientfd, "Current output: %i: %s\n", 1036 | gCurrentOutput.fd, gCurrentOutput.name.c_str()); 1037 | 1038 | toClient(clientfd, "Hotkeys:\n"); 1039 | for (const auto& hi: gHotkeys) { 1040 | toClient(clientfd, " %u: %s:%u:%i => %s\n", 1041 | hi.first.device, 1042 | EV2String(hi.first.type), 1043 | hi.first.code, 1044 | hi.first.value, 1045 | hi.second.c_str()); 1046 | } 1047 | toClient(clientfd, "Event actions:\n"); 1048 | for (const auto& i: gEventCommands) { 1049 | toClient(clientfd, " '%s': %s\n", 1050 | i.first.c_str(), 1051 | i.second.c_str()); 1052 | } 1053 | } 1054 | 1055 | static void 1056 | clientCommand_Action(int clientfd, const vector& args) 1057 | { 1058 | if (args.size() < 2) 1059 | throw Exception("'action': missing subcommand"); 1060 | const string& cmd = args[1]; 1061 | 1062 | if (args.size() < 3) 1063 | throw Exception("'action': missing action"); 1064 | const string& action = args[2]; 1065 | 1066 | if (cmd == "remove") { 1067 | if (args.size() != 3) 1068 | throw Exception("'action': excess parameters"); 1069 | auto iter = gEventCommands.find(action); 1070 | if (iter == gEventCommands.end()) 1071 | return; 1072 | gEventCommands.erase(iter); 1073 | toClient(clientfd, "removed on-'%s' command\n", action.c_str()); 1074 | } 1075 | else if (cmd == "set") { 1076 | if (args.size() < 4) 1077 | throw Exception("'action': missing command"); 1078 | string cmdstring = join(' ', args.begin()+3, args.end()); 1079 | auto iter = gEventCommands.find(action); 1080 | if (iter != gEventCommands.end()) 1081 | toClient(clientfd, "replaced on-'%s' command\n", 1082 | action.c_str()); 1083 | else 1084 | toClient(clientfd, "added on-'%s' command\n", 1085 | action.c_str()); 1086 | gEventCommands[action] = std::move(cmdstring); 1087 | } 1088 | else 1089 | throw MsgException("'action': unknown subcommand: %s", 1090 | cmd.c_str()); 1091 | } 1092 | 1093 | static void sourceCommandFile(int clientfd, const char *path); 1094 | static void 1095 | clientCommand(int clientfd, const vector& args) 1096 | { 1097 | if (args.empty()) 1098 | return; 1099 | 1100 | if (args[0] == "nop") { 1101 | } else if (args[0] == "device") 1102 | clientCommand_Device(clientfd, args); 1103 | else if (args[0] == "output") 1104 | clientCommand_Output(clientfd, args); 1105 | else if (args[0] == "hotkey") 1106 | clientCommand_Hotkey(clientfd, args); 1107 | else if (args[0] == "action") 1108 | clientCommand_Action(clientfd, args); 1109 | else if (args[0] == "info") 1110 | clientCommand_Info(clientfd, args); 1111 | else if (args[0] == "write-events") { 1112 | if (args.size() != 2) 1113 | throw Exception("'write-events' requires 1 parameter"); 1114 | writeCommand(clientfd, args[1].c_str()); 1115 | //toClient(clientfd, "write-events = %u\n", gWrite ? 1 : 0); 1116 | } 1117 | else if (args[0] == "grab-devices") { 1118 | if (args.size() != 2) 1119 | throw Exception("'grab-devices' requires 1 parameter"); 1120 | grabCommand(clientfd, args[1].c_str()); 1121 | //toClient(clientfd, "grab-devices = %u\n", gGrab ? 1 : 0); 1122 | } 1123 | else if (args[0] == "grab") { 1124 | if (args.size() != 2) 1125 | throw Exception("'grab' requires 1 parameter"); 1126 | grabCommand(clientfd, args[1].c_str()); 1127 | writeCommand(clientfd, args[1].c_str()); 1128 | toClient(clientfd, 1129 | "Warning: the command grab is deprecated," 1130 | " use grab-devices and write-events instead.\n"); 1131 | } 1132 | else if (args[0] == "use") { 1133 | if (args.size() != 2) 1134 | throw Exception("'use' requires 1 parameter"); 1135 | useOutput(clientfd, args[1]); 1136 | //toClient(clientfd, "output = %s\n", 1137 | // gCurrentOutput.name.c_str()); 1138 | } 1139 | else if (args[0] == "exec" || args[0] == "exec&") { 1140 | bool background = args[0] == "exec&"; 1141 | if (args.size() < 2) 1142 | throw Exception("'exec' requires 1 parameter"); 1143 | string cmd = join(' ', args.begin()+1, args.end()); 1144 | shellCommand(cmd.c_str(), background); 1145 | } 1146 | else if (args[0] == "source") { 1147 | if (args.size() != 2) 1148 | throw Exception("'source' requires 1 parameter"); 1149 | sourceCommandFile(clientfd, args[1].c_str()); 1150 | } 1151 | else if (args[0] == "quit") { 1152 | gQuit = true; 1153 | } 1154 | else 1155 | throw MsgException("unknown command: %s", args[0].c_str()); 1156 | 1157 | if (clientfd < 0) 1158 | return; 1159 | // If it came from an actual client we send an OK back 1160 | toClient(clientfd, "Ok.\n"); 1161 | } 1162 | 1163 | static void 1164 | parseClientCommand(int clientfd, const char *cmd, size_t length) 1165 | { 1166 | if (!length) 1167 | return; 1168 | 1169 | auto end = cmd + length; 1170 | 1171 | if (!skipWhite(cmd)) 1172 | return; 1173 | 1174 | vector args; 1175 | bool escape = false; 1176 | while (cmd < end) { 1177 | if (!skipWhite(cmd)) 1178 | break; 1179 | 1180 | if (!escape) { 1181 | if (*cmd == '\\') { 1182 | ++cmd; 1183 | escape = true; 1184 | continue; 1185 | } else if (*cmd == ';') { 1186 | ++cmd; 1187 | if (!args.empty()) { 1188 | clientCommand(clientfd, args); 1189 | args.clear(); 1190 | } 1191 | continue; 1192 | } 1193 | else if (*cmd == '"' || *cmd == '\'') { 1194 | args.emplace_back(parseString(cmd)); 1195 | continue; 1196 | } 1197 | } 1198 | 1199 | escape = false; 1200 | string arg; 1201 | auto beg = cmd; 1202 | do { 1203 | if (!escape && *cmd == '\\') { 1204 | arg.append(beg, cmd); 1205 | ++cmd; 1206 | beg = cmd; 1207 | escape = true; 1208 | } else { 1209 | ++cmd; 1210 | escape = false; 1211 | } 1212 | } while (*cmd && !isWhite(*cmd) && (escape || *cmd != ';')); 1213 | arg.append(beg, cmd); 1214 | args.emplace_back(std::move(arg)); 1215 | escape = false; 1216 | } 1217 | 1218 | if (!args.empty()) 1219 | clientCommand(clientfd, args); 1220 | } 1221 | 1222 | static void 1223 | processCommandQueue() 1224 | { 1225 | for (const auto& command: gCommandQueue) { 1226 | try { 1227 | parseClientCommand(command.client_, 1228 | command.command_.c_str(), 1229 | command.command_.length()); 1230 | } catch (const Exception& ex) { 1231 | toClient(command.client_, 1232 | "ERROR: %s\n", ex.what()); 1233 | } 1234 | } 1235 | gCommandQueue.clear(); 1236 | } 1237 | 1238 | static void 1239 | sourceCommandFile(int clientfd, const char *path) 1240 | { 1241 | FILE *file = ::fopen(path, "rbe"); 1242 | if (!file) 1243 | throw ErrnoException("open(%s)", path); 1244 | char *line = nullptr; 1245 | 1246 | scope (exit) { 1247 | ::fclose(file); 1248 | ::free(line); 1249 | }; 1250 | 1251 | size_t bufsize = 0; 1252 | ssize_t length; 1253 | while ((length = ::getline(&line, &bufsize, file)) != -1) { 1254 | if (!length) 1255 | continue; 1256 | line[--length] = 0; 1257 | const char *p = line; 1258 | while (*p && isspace(*p)) { 1259 | ++p; 1260 | --length; 1261 | } 1262 | if (!*p || *p == '#') 1263 | continue; 1264 | parseClientCommand(clientfd, p, size_t(length)); 1265 | } 1266 | if (::feof(file)) 1267 | return; 1268 | if (errno) 1269 | throw ErrnoException("error reading from %s", path); 1270 | } 1271 | 1272 | static void 1273 | signull(int sig) 1274 | { 1275 | switch (sig) { 1276 | case SIGTERM: 1277 | case SIGQUIT: 1278 | case SIGINT: 1279 | // but this is actually the default 1280 | default: 1281 | gQuit = true; 1282 | break; 1283 | case SIGCHLD: 1284 | { 1285 | int status = 0; 1286 | do { 1287 | // reap zombies 1288 | } while (::waitpid(-1, &status, WNOHANG) > 0); 1289 | break; 1290 | } 1291 | } 1292 | } 1293 | 1294 | int 1295 | cmd_daemon(int argc, char **argv) 1296 | { 1297 | static struct option longopts[] = { 1298 | { "help", no_argument, nullptr, 'h' }, 1299 | { "source", required_argument, nullptr, 's' }, 1300 | { nullptr, 0, nullptr, 0 } 1301 | }; 1302 | 1303 | vector command_files; 1304 | 1305 | int c, optindex = 0; 1306 | opterr = 1; 1307 | while (true) { 1308 | c = ::getopt_long(argc, argv, "hls:", longopts, &optindex); 1309 | if (c == -1) 1310 | break; 1311 | 1312 | switch (c) { 1313 | case 'h': 1314 | usage_daemon(stdout, EXIT_SUCCESS); 1315 | // break; usage is [[noreturn]] 1316 | case 's': 1317 | command_files.push_back(optarg); 1318 | break; 1319 | case '?': 1320 | break; 1321 | default: 1322 | ::fprintf(stderr, "getopt error\n"); 1323 | return -1; 1324 | } 1325 | } 1326 | 1327 | if (optind+1 != argc) { 1328 | ::fprintf(stderr, "missing socket name\n"); 1329 | return 2; 1330 | } 1331 | 1332 | const char *sockname = argv[optind++]; 1333 | 1334 | signal(SIGINT, signull); 1335 | signal(SIGTERM, signull); 1336 | signal(SIGQUIT, signull); 1337 | signal(SIGCHLD, signull); 1338 | signal(SIGPIPE, SIG_IGN); 1339 | 1340 | Socket server; 1341 | if (sockname[0] == '@') 1342 | server.listenUnix(&sockname[1]); 1343 | else { 1344 | (void)::unlink(sockname); 1345 | server.listenUnix(sockname); 1346 | } 1347 | 1348 | vector pfds; 1349 | pfds.resize(1); 1350 | pfds[0].fd = server.fd(); 1351 | 1352 | gFDCBs[server.fd()] = FDCallbacks { 1353 | [&]() { newCommandClient(server); }, 1354 | [ ]() { gQuit = true; }, 1355 | [ ]() { gQuit = true; }, 1356 | [ ]() { throw Exception("removed server socket"); }, 1357 | }; 1358 | 1359 | for (auto& i: pfds) { 1360 | i.events = POLLIN | POLLHUP | POLLERR; 1361 | i.revents = 0; 1362 | } 1363 | 1364 | for (auto file: command_files) 1365 | sourceCommandFile(-1, file); 1366 | command_files.clear(); 1367 | command_files.shrink_to_fit(); 1368 | while (!gQuit) { 1369 | processCommandQueue(); 1370 | 1371 | if (!gFDAddQueue.empty()) { 1372 | pfds.insert(pfds.end(), gFDAddQueue.begin(), 1373 | gFDAddQueue.end()); 1374 | gFDAddQueue.clear(); 1375 | } 1376 | 1377 | pfds.erase( 1378 | std::remove_if(pfds.begin(), pfds.end(), 1379 | [](struct pollfd& pfd) { 1380 | return std::find(gFDRemoveQueue.begin(), 1381 | gFDRemoveQueue.end(), 1382 | pfd.fd) 1383 | != gFDRemoveQueue.end(); 1384 | } 1385 | ), 1386 | pfds.end()); 1387 | processRemoveQueue(); 1388 | 1389 | // after processing commands, we may want to quit: 1390 | if (gQuit) 1391 | break; 1392 | 1393 | auto got = ::poll(pfds.data(), nfds_t(pfds.size()), -1); 1394 | if (got == -1) { 1395 | if (errno == EINTR) { 1396 | ::fprintf(stderr, "interrupted\n"); 1397 | continue; 1398 | } 1399 | throw ErrnoException("poll interrupted"); 1400 | } 1401 | if (!got) 1402 | ::fprintf(stderr, "empty poll?\n"); 1403 | 1404 | for (auto& i: pfds) { 1405 | auto cbs = gFDCBs.find(i.fd); 1406 | auto revents = i.revents; 1407 | i.revents = 0; 1408 | 1409 | if (cbs == gFDCBs.end()) 1410 | throw Exception( 1411 | "internal: callback map broken"); 1412 | 1413 | if (revents & POLLERR) 1414 | cbs->second.onError(); 1415 | if (gQuit) break; 1416 | if (revents & POLLHUP) 1417 | cbs->second.onHUP(); 1418 | if (gQuit) break; 1419 | if (revents & POLLIN) 1420 | cbs->second.onRead(); 1421 | if (gQuit) break; 1422 | } 1423 | 1424 | } 1425 | ::fprintf(stderr, "shutting down\n"); 1426 | 1427 | gFDRemoveQueue.clear(); 1428 | gFDCBs.clear(); // destroy possible captures 1429 | gCommandClients.clear(); // disconnect all clients 1430 | 1431 | return 0; 1432 | } 1433 | -------------------------------------------------------------------------------- /src/iohandle.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | 10 | #include 11 | #include 12 | 13 | struct IOHandle { 14 | IOHandle(const IOHandle&) = delete; 15 | 16 | constexpr IOHandle(int fd); 17 | constexpr IOHandle(); 18 | IOHandle(IOHandle&& o); 19 | ~IOHandle(); 20 | 21 | int fd() const noexcept; 22 | ssize_t read(void *buf, size_t count); 23 | ssize_t write(const void *buf, size_t count); 24 | void close(); 25 | int release() noexcept; 26 | void cloexec(bool on); 27 | 28 | IOHandle& operator=(IOHandle&& o); 29 | 30 | operator bool() const noexcept; 31 | 32 | private: 33 | int fd_; 34 | }; 35 | 36 | inline constexpr 37 | IOHandle::IOHandle(int fd) 38 | : fd_(fd) 39 | { 40 | } 41 | 42 | inline constexpr 43 | IOHandle::IOHandle() 44 | : fd_(-1) 45 | { 46 | } 47 | 48 | inline 49 | IOHandle::IOHandle(IOHandle&& o) 50 | : IOHandle(o.fd_) 51 | { 52 | o.fd_ = -1; 53 | } 54 | 55 | inline 56 | IOHandle::~IOHandle() { 57 | if (fd_ != -1) 58 | ::close(fd_); 59 | } 60 | 61 | inline int 62 | IOHandle::fd() const noexcept 63 | { 64 | return fd_; 65 | } 66 | 67 | inline ssize_t 68 | IOHandle::read(void *buf, size_t count) 69 | { 70 | return ::read(fd_, buf, count); 71 | } 72 | 73 | inline ssize_t 74 | IOHandle::write(const void *buf, size_t count) 75 | { 76 | return ::write(fd_, buf, count); 77 | } 78 | 79 | inline void 80 | IOHandle::close() 81 | { 82 | if (fd_ != -1) { 83 | ::close(fd_); 84 | fd_ = -1; 85 | } 86 | } 87 | 88 | inline int 89 | IOHandle::release() noexcept 90 | { 91 | int fd = fd_; 92 | fd_ = -1; 93 | return fd; 94 | } 95 | 96 | inline IOHandle& 97 | IOHandle::operator=(IOHandle&& o) 98 | { 99 | this->close(); 100 | fd_ = o.fd_; 101 | o.fd_ = -1; 102 | return (*this); 103 | } 104 | 105 | inline 106 | IOHandle::operator bool() const noexcept 107 | { 108 | return fd_ != -1; 109 | } 110 | 111 | inline void 112 | IOHandle::cloexec(bool on) 113 | { 114 | int flags = ::fcntl(fd_, F_GETFD); 115 | if (on) 116 | flags |= FD_CLOEXEC; 117 | else 118 | flags &= ~(FD_CLOEXEC); 119 | if (::fcntl(fd_, F_SETFD, flags) < 0) 120 | throw ErrnoException("failed to set FD_CLOEXEC flags"); 121 | } 122 | -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | using std::map; 15 | 16 | #include "main.h" 17 | 18 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 19 | 20 | const char* 21 | Exception::what() const noexcept { 22 | return msg_; 23 | } 24 | 25 | DeviceException::DeviceException(const char *msg) 26 | : Exception(msg) 27 | {} 28 | 29 | MsgException::MsgException(MsgException&& o) 30 | : Exception(msgbuf_) // we have to copy instead of moving 31 | { 32 | ::strncpy(msgbuf_, o.msgbuf_, sizeof(msgbuf_)); 33 | } 34 | 35 | #pragma clang diagnostic push 36 | #pragma clang diagnostic ignored "-Wformat-nonliteral" 37 | MsgException::MsgException(const char *msg, ...) 38 | : Exception(msgbuf_) 39 | { 40 | va_list ap; 41 | va_start(ap, msg); 42 | ::vsnprintf(msgbuf_, sizeof(msgbuf_), msg, ap); 43 | va_end(ap); 44 | } 45 | 46 | ErrnoException::ErrnoException(ErrnoException&& o) 47 | : Exception(msgbuf_) // we have to copy instead of moving 48 | { 49 | ::strncpy(msgbuf_, o.msgbuf_, sizeof(msgbuf_)); 50 | errno_ = o.errno_; 51 | } 52 | 53 | ErrnoException::ErrnoException(const char *msg, ...) 54 | : Exception(msgbuf_) 55 | { 56 | errno_ = errno; 57 | va_list ap; 58 | va_start(ap, msg); 59 | int end = ::vsnprintf(msgbuf_, sizeof(msgbuf_), msg, ap); 60 | va_end(ap); 61 | if (end < 0) 62 | end = 0; 63 | ::snprintf(msgbuf_ + end, 64 | sizeof(msgbuf_)-size_t(end), 65 | ": %s", ::strerror(errno_)); 66 | msgbuf_[sizeof(msgbuf_)-1] = 0; 67 | } 68 | #pragma clang diagnostic pop 69 | 70 | static void 71 | usage [[noreturn]] (FILE *out, int exit_status) 72 | { 73 | ::fprintf(out, 74 | "usage: netevent \n" 75 | "commands:\n" 76 | " show DEVICE [COUNT] show up to COUNT input events of DEVICE\n" 77 | " cat [OPTIONS] DEVICE dump device in netevent 1 or 2 comaptible way\n" 78 | " create [OPTIONS] create a device\n" 79 | " daemon [OPTIONS] SOCK run a device daemon\n" 80 | " command SOCK send a runtime command to a daemon\n" 81 | ); 82 | ::exit(exit_status); 83 | } 84 | 85 | static void 86 | version [[noreturn]] (FILE *out, int exit_status) 87 | { 88 | ::fprintf(out, 89 | "netevent version " NETEVENT_VERSION "\n" 90 | ); 91 | ::exit(exit_status); 92 | } 93 | 94 | // ::strtoul() doesn't support a maxlen and I want to be able to use this in 95 | // the middle of command parsing... or stuff. 96 | static bool 97 | parseLongParse(long *out, const char *s, size_t maxlen) 98 | { 99 | *out = 0; 100 | if (s[0] == '0' && maxlen > 1 && s[1] == 'x') { 101 | s += 2; 102 | maxlen -= 2; 103 | while (*s && maxlen) { 104 | if (*s >= '0' && *s <= '9') { 105 | *out = 0x10 * *out + (*s - '0'); 106 | ++s; 107 | --maxlen; 108 | } else if (*s >= 'a' && *s <= 'f') { 109 | *out = 0x10 * *out + (*s - 'a' + 0xa); 110 | ++s; 111 | --maxlen; 112 | } else if (*s >= 'A' && *s <= 'F') { 113 | *out = 0x10 * *out + (*s - 'A' + 0xA); 114 | ++s; 115 | --maxlen; 116 | } else 117 | return false; 118 | } 119 | } else if (s[0] == '0' && maxlen > 1) { 120 | ++s; 121 | --maxlen; 122 | while (*s && maxlen) { 123 | if (*s >= '0' && *s <= '7') { 124 | *out = 8 * *out + (*s - '0'); 125 | ++s; 126 | --maxlen; 127 | } else 128 | return false; 129 | } 130 | } else { 131 | while (*s && maxlen) { 132 | if (*s >= '0' && *s <= '9') { 133 | *out = 10 * *out + (*s - '0'); 134 | ++s; 135 | --maxlen; 136 | } else 137 | return false; 138 | } 139 | } 140 | return true; 141 | } 142 | 143 | static bool 144 | parseLongDo(long *out, const char *s, size_t maxlen, bool allowSigned) 145 | { 146 | #if 0 147 | char *end = nullptr; 148 | 149 | errno = 0; 150 | // no support for non-zero-terminated strings :-( 151 | *out = ::strtoul(s, &end, 0); 152 | return !(errno || !end || *end); 153 | #endif 154 | 155 | if (!maxlen || !*s) 156 | return false; 157 | 158 | while (maxlen && (*s == ' ' || *s == '\t')) { 159 | ++s; 160 | --maxlen; 161 | } 162 | 163 | bool negative = false; 164 | if (*s == '+' || (allowSigned && *s == '-')) { 165 | negative = *s == '-'; 166 | ++s; 167 | --maxlen; 168 | } 169 | 170 | while (!maxlen || !(*s >= '0' && *s <= '9')) 171 | return false; 172 | 173 | if (!parseLongParse(out, s, maxlen)) 174 | return false; 175 | if (negative) 176 | *out = -*out; 177 | return true; 178 | } 179 | 180 | bool 181 | parseLong(long *out, const char *s, size_t maxlen) 182 | { 183 | return parseLongDo(out, s, maxlen, true); 184 | } 185 | 186 | bool 187 | parseULong(unsigned long *out, const char *s, size_t maxlen) 188 | { 189 | long o; 190 | if (parseLongDo(&o, s, maxlen, false)) { 191 | *out = static_cast(o); 192 | return true; 193 | } 194 | return false; 195 | } 196 | 197 | bool 198 | parseBool(bool *out, const char *s) 199 | { 200 | if (!::strcasecmp(s, "1") || 201 | !::strcasecmp(s, "on") || 202 | !::strcasecmp(s, "yes") || 203 | !::strcasecmp(s, "true")) 204 | { 205 | *out = true; 206 | return true; 207 | } 208 | else if (!::strcasecmp(s, "0") || 209 | !::strcasecmp(s, "no") || 210 | !::strcasecmp(s, "off") || 211 | !::strcasecmp(s, "false")) 212 | { 213 | *out = false; 214 | return true; 215 | } 216 | // don't touch *out 217 | return false; 218 | } 219 | 220 | unsigned int 221 | String2EV(const char* text, size_t length) 222 | { 223 | if (length > 3 && ::strncasecmp(text, "EV_", 3) == 0) { 224 | text += 3; 225 | length -= 3; 226 | } 227 | for (const auto& i: kEVMap) { 228 | if (::strncasecmp(text, i.name, length) == 0) 229 | return i.num; 230 | } 231 | unsigned long num; 232 | if (parseULong(&num, text, length)) 233 | return unsigned(num); 234 | return unsigned(-1); 235 | } 236 | 237 | void 238 | writeHello(int fd) 239 | { 240 | NE2Packet pkt = {}; 241 | ::memset(reinterpret_cast(&pkt), 0, sizeof(pkt)); 242 | pkt.cmd = htobe16(uint16_t(NE2Command::Hello)); 243 | ::memcpy(pkt.hello.magic, kNE2Hello, sizeof(pkt.hello.magic)); 244 | pkt.hello.version = htobe16(kNE2Version); 245 | if (!mustWrite(fd, &pkt, sizeof(pkt))) 246 | throw ErrnoException("failed to write hello packet"); 247 | } 248 | 249 | static void 250 | checkHello(NE2Packet& pkt) 251 | { 252 | if (pkt.cmd != uint16_t(NE2Command::Hello)) 253 | throw MsgException( 254 | "protocol error: got %u instead of %u (Hello)\n", 255 | unsigned(pkt.cmd), unsigned(NE2Command::Hello)); 256 | if (::memcmp(pkt.hello.magic, kNE2Hello, sizeof(pkt.hello.magic)) != 0) { 257 | throw MsgException("protocol error: bad hello packet magic"); 258 | } 259 | pkt.hello.version = be16toh(pkt.hello.version); 260 | if (pkt.hello.version != kNE2Version) 261 | throw MsgException( 262 | "protocol version mismatch: got %u, expected %u\n", 263 | pkt.hello.version, kNE2Version); 264 | } 265 | 266 | static void 267 | readHello(int fd) 268 | { 269 | NE2Packet pkt = {}; 270 | if (!mustRead(fd, &pkt, sizeof(pkt))) 271 | throw ErrnoException("error while expecting hello packet"); 272 | pkt.cmd = htobe16(pkt.cmd); 273 | checkHello(pkt); 274 | } 275 | 276 | static void 277 | usage_show [[noreturn]] (FILE *out, int exit_status) 278 | { 279 | ::fprintf(out, 280 | "usage: netevent show [options] DEVICE [COUNT]\n" 281 | "options:\n" 282 | " -h, --help show this help message\n" 283 | " -g, --grab grab the device\n" 284 | " -G, --no-grab do not grab the device (default)\n" 285 | ); 286 | ::exit(exit_status); 287 | } 288 | 289 | static int 290 | cmd_show(int argc, char **argv) 291 | { 292 | static struct option longopts[] = { 293 | { "help", no_argument, nullptr, 'h' }, 294 | { "grab", no_argument, nullptr, 'g' }, 295 | { "no-grab", no_argument, nullptr, 'G' }, 296 | { nullptr, 0, nullptr, 0 } 297 | }; 298 | 299 | bool optGrab = false; 300 | int c, optindex = 0; 301 | opterr = 1; 302 | while (true) { 303 | c = ::getopt_long(argc, argv, "hgG", longopts, &optindex); 304 | if (c == -1) 305 | break; 306 | 307 | switch (c) { 308 | case 'h': 309 | usage_show(stdout, EXIT_SUCCESS); 310 | 311 | case 'g': optGrab = true; break; 312 | case 'G': optGrab = false; break; 313 | 314 | case '?': 315 | break; 316 | default: 317 | ::fprintf(stderr, "getopt error\n"); 318 | return -1; 319 | } 320 | } 321 | 322 | if (::optind+1 != argc && ::optind+2 != argc) 323 | usage_show(stderr, EXIT_FAILURE); 324 | 325 | unsigned long maxcount = 10; 326 | if (::optind+2 == argc && 327 | !parseULong(&maxcount, argv[::optind+1], size_t(-1))) 328 | { 329 | ::fprintf(stderr, "bad count: %s\n", argv[::optind+1]); 330 | return 2; 331 | } 332 | 333 | InDevice dev {argv[::optind]}; 334 | if (optGrab) 335 | dev.grab(true); 336 | InputEvent ev; 337 | unsigned long count = 0; 338 | while (count++ < maxcount && dev.read(&ev)) { 339 | ::printf("%s:%u:%i\n", 340 | EV2String(ev.type), ev.code, ev.value); 341 | } 342 | 343 | return 0; 344 | } 345 | 346 | static void 347 | usage_cat [[noreturn]] (FILE *out, int exit_status) 348 | { 349 | ::fprintf(out, 350 | "usage: netevent cat [options] DEVICE\n" 351 | "options:\n" 352 | " -h, --help show this help message\n" 353 | " -l, --legacy run in netevent 1 compatible mode\n" 354 | " --no-legacy run in netevent 2 mode (default)\n" 355 | " -g, --grab grab the device (default)\n" 356 | " -G, --no-grab do not grab the device\n" 357 | ); 358 | ::exit(exit_status); 359 | } 360 | 361 | static int 362 | cmd_cat(int argc, char **argv) 363 | { 364 | static struct option longopts[] = { 365 | { "help", no_argument, nullptr, 'h' }, 366 | { "legacy", no_argument, nullptr, 'l' }, 367 | { "no-legacy", no_argument, nullptr, 0x1000 }, 368 | { "grab", no_argument, nullptr, 'g' }, 369 | { "no-grab", no_argument, nullptr, 'G' }, 370 | { nullptr, 0, nullptr, 0 } 371 | }; 372 | 373 | bool optLegacyMode = false; 374 | bool optGrab = true; 375 | 376 | int c, optindex = 0; 377 | opterr = 1; 378 | while (true) { 379 | c = ::getopt_long(argc, argv, "hlgG", longopts, &optindex); 380 | if (c == -1) 381 | break; 382 | 383 | switch (c) { 384 | case 'h': 385 | usage_cat(stdout, EXIT_SUCCESS); 386 | // break; usage is [[noreturn]] 387 | // 388 | case 'l': optLegacyMode = true; break; 389 | case 0x1000: optLegacyMode = false; break; 390 | 391 | case 'g': optGrab = true; break; 392 | case 'G': optGrab = false; break; 393 | 394 | case '?': 395 | break; 396 | default: 397 | ::fprintf(stderr, "getopt error\n"); 398 | return -1; 399 | } 400 | } 401 | 402 | if (::optind >= argc) { 403 | ::fprintf(stderr, "missing device name\n"); 404 | usage_cat(stderr, EXIT_FAILURE); 405 | } 406 | 407 | // Worth adding support for multiple devices via NE2 protocol? 408 | // Just use the daemon? 409 | if (::optind+1 != argc) { 410 | ::fprintf(stderr, "too many parameters\n"); 411 | usage_cat(stderr, EXIT_FAILURE); 412 | } 413 | 414 | InDevice dev {argv[::optind]}; 415 | if (optGrab) 416 | dev.grab(true); 417 | if (optLegacyMode) { 418 | dev.writeNeteventHeader(1); 419 | InputEvent ev; 420 | while (dev.read(&ev)) { 421 | if (!mustWrite(1, &ev, sizeof(ev))) { 422 | ::fprintf(stderr, "write failed: %s\n", 423 | ::strerror(errno)); 424 | return 2; 425 | } 426 | } 427 | } else { 428 | writeHello(1); 429 | dev.writeNE2AddDevice(1, 0); 430 | NE2Packet pkt = {}; 431 | pkt.cmd = htobe16(uint16_t(NE2Command::DeviceEvent)); 432 | pkt.event.id = htobe16(0); 433 | while (dev.read(&pkt.event.event)) { 434 | pkt.event.event.toNet(); 435 | if (!mustWrite(1, &pkt, sizeof(pkt))) { 436 | ::fprintf(stderr, "write failed: %s\n", 437 | ::strerror(errno)); 438 | return 2; 439 | } 440 | } 441 | } 442 | return 0; 443 | } 444 | 445 | static void 446 | doDaemonize(bool can_close) 447 | { 448 | if (::daemon(1, can_close ? 0 : 1) != 0) 449 | throw ErrnoException("daemon() failed"); 450 | } 451 | 452 | static void 453 | usage_create [[noreturn]] (FILE *out, int exit_status) 454 | { 455 | ::fprintf(out, 456 | "usage: netevent create [options]\n" 457 | "options:\n" 458 | " -h, --help show this help message\n" 459 | " -l, --legacy run in netevent 1 compatible mode\n" 460 | " --no-legacy run in netevent 2 mode (default)\n" 461 | " --duplicates=MODE how to deal with duplicate devices\n" 462 | " --listen=SOCKSPEC listen on a socket instead of reading from stdin\n" 463 | " --connect try to connect before creating a new instance\n" 464 | " --on-close=end|accept whether to exit or restart on EOF\n" 465 | " --daemonize fork off into the background\n" 466 | "duplicate device modes:\n" 467 | " reject treat duplicates as errors and exit (default)\n" 468 | " resume assume the devices are equivalent and resume them\n" 469 | " replace remove the previous device and create a new one\n" 470 | ); 471 | ::exit(exit_status); 472 | } 473 | 474 | static int 475 | cmd_create_legacy() 476 | { 477 | // FIXME: if anybody needs it, the --hotkey, --toggler etc. stuff 478 | // could be added here ... 479 | auto out = OutDevice::newFromNeteventStream(0); 480 | InputEvent ev; 481 | while (true) { 482 | if (!mustRead(0, &ev, sizeof(ev))) { 483 | if (!errno) 484 | break; 485 | throw ErrnoException("read error"); 486 | } 487 | out->write(ev); 488 | } 489 | return 0; 490 | } 491 | 492 | static int 493 | cat(int from, int to) 494 | { 495 | char buf[1024]; 496 | ssize_t got; 497 | while ((got = ::read(from, buf, sizeof(buf))) > 0) { 498 | if (!mustWrite(to, buf, size_t(got))) 499 | throw ErrnoException("write error"); 500 | } 501 | if (got < 0) 502 | throw ErrnoException("read error"); 503 | return 0; 504 | } 505 | 506 | static int 507 | cmd_create(int argc, char **argv) 508 | { 509 | static struct option longopts[] = { 510 | { "help", no_argument, nullptr, 'h' }, 511 | { "legacy", no_argument, nullptr, 'l' }, 512 | { "no-legacy", no_argument, nullptr, 0x1000 }, 513 | { "duplicates", required_argument, nullptr, 'd' }, 514 | { "listen", required_argument, nullptr, 0x1001 }, 515 | { "on-close", required_argument, nullptr, 0x1002 }, 516 | { "daemonize", no_argument, nullptr, 0x1003 }, 517 | { "no-daemonize", no_argument, nullptr, 0xf003 }, 518 | { "connect", no_argument, nullptr, 0x1004 }, 519 | { "no-connect", no_argument, nullptr, 0xf004 }, 520 | { nullptr, 0, nullptr, 0 } 521 | }; 522 | 523 | bool no_legacy = false; 524 | bool optLegacyMode = false; 525 | bool optDaemonize = false; 526 | bool optConnect = false; 527 | enum class DuplicateMode { Reject, Resume, Replace } 528 | optDuplicates = DuplicateMode::Reject; 529 | enum class CloseAction { End, Accept } 530 | optOnClose = CloseAction::Accept; 531 | 532 | const char *optListen = nullptr; 533 | 534 | int c, optindex = 0; 535 | opterr = 1; 536 | while (true) { 537 | c = ::getopt_long(argc, argv, "hld:", longopts, &optindex); 538 | if (c == -1) 539 | break; 540 | 541 | switch (c) { 542 | case 'h': 543 | usage_create(stdout, EXIT_SUCCESS); 544 | // break; usage is [[noreturn]] 545 | case 'l': optLegacyMode = true; break; 546 | case 0x1000: optLegacyMode = false; break; 547 | case 0x1003: optDaemonize = true; break; 548 | case 0xf003: optDaemonize = false; break; 549 | case 0x1004: optConnect = true; break; 550 | case 0xf004: optConnect = false; break; 551 | case 'd': 552 | no_legacy = true; 553 | if (!::strcasecmp(optarg, "reject")) 554 | optDuplicates = DuplicateMode::Reject; 555 | else if (!::strcasecmp(optarg, "resume")) 556 | optDuplicates = DuplicateMode::Resume; 557 | else if (!::strcasecmp(optarg, "replace")) 558 | optDuplicates = DuplicateMode::Replace; 559 | else { 560 | ::fprintf(stderr, 561 | "invalid mode for duplicate devices\n" 562 | "should be 'reject', 'resume' or 'replace'\n"); 563 | usage_create(stderr, EXIT_FAILURE); 564 | } 565 | break; 566 | case 0x1001: 567 | no_legacy = true; 568 | optListen = optarg; 569 | break; 570 | case 0x1002: 571 | no_legacy = true; 572 | if (!::strcasecmp(optarg, "end")) 573 | optOnClose = CloseAction::End; 574 | else if (!::strcasecmp(optarg, "accept")) 575 | optOnClose = CloseAction::Accept; 576 | else { 577 | ::fprintf(stderr, 578 | "invalid on-close action\n" 579 | "should be 'end' or 'accept'\n"); 580 | usage_create(stderr, EXIT_FAILURE); 581 | } 582 | break; 583 | case '?': 584 | break; 585 | default: 586 | ::fprintf(stderr, "getopt error\n"); 587 | return -1; 588 | } 589 | } 590 | 591 | if (optLegacyMode && no_legacy) { 592 | ::fprintf(stderr, 593 | "legacy mode does not support the provided parameters\n"); 594 | return 2; 595 | } 596 | 597 | if (optConnect && !optListen) { 598 | ::fprintf(stderr, 599 | "--connect requires --listen\n"); 600 | return 2; 601 | } 602 | 603 | if (::optind != argc) { 604 | ::fprintf(stderr, "too many arguments\n"); 605 | usage_create(stderr, EXIT_FAILURE); 606 | } 607 | 608 | if (optLegacyMode) { 609 | if (optDaemonize) 610 | doDaemonize(optListen); 611 | return cmd_create_legacy(); 612 | } 613 | 614 | map> devices; 615 | 616 | int infd = 0; 617 | IOHandle outhandle; 618 | Socket serversock; 619 | IOHandle inhandle; 620 | 621 | if (optConnect) { 622 | try { 623 | if (optListen[0] == '@') 624 | serversock.connectUnix(optListen+1); 625 | else 626 | serversock.connectUnix(optListen); 627 | outhandle = serversock.release(); 628 | } catch (const ErrnoException& ex) { 629 | if (!optDaemonize || ex.error() != ECONNREFUSED) 630 | throw; 631 | } 632 | if (outhandle) { 633 | return cat(0, outhandle.fd()); 634 | } 635 | int p[2]; 636 | if (::pipe(p) != 0) 637 | throw ErrnoException("pipe() failed"); 638 | inhandle = p[0]; 639 | IOHandle p1 { p[1] }; 640 | 641 | pid_t dmn = ::fork(); 642 | if (dmn == -1) 643 | throw ErrnoException("fork() failed"); 644 | 645 | if (dmn) { 646 | inhandle.close(); 647 | // wait for the daemonization 648 | int status; 649 | (void)::waitpid(dmn, &status, 0); 650 | return cat(0, p1.fd()); 651 | } 652 | // this is the regular netevent process 653 | // its first client will not come from the socket, but 654 | // from the above created pipe() 655 | infd = inhandle.fd(); 656 | p1.close(); 657 | } 658 | 659 | if (optListen) { 660 | if (optListen[0] == '@') 661 | serversock.listenUnix(optListen+1); 662 | else 663 | serversock.listenUnix(optListen); 664 | } 665 | 666 | if (optDaemonize) 667 | doDaemonize(optListen); 668 | 669 | if (optListen) { 670 | if (!inhandle) { 671 | inhandle = serversock.accept(); 672 | infd = inhandle.fd(); 673 | } 674 | if (optOnClose == CloseAction::End) 675 | serversock.close(); 676 | } 677 | 678 | readHello(infd); 679 | NE2Packet pkt = {}; 680 | Resume: 681 | while (mustRead(infd, &pkt, sizeof(pkt))) { 682 | pkt.cmd = be16toh(pkt.cmd); 683 | #pragma clang diagnostic push 684 | #pragma clang diagnostic ignored "-Wcovered-switch-default" 685 | switch (static_cast(pkt.cmd)) { 686 | case NE2Command::Hello: 687 | checkHello(pkt); 688 | break; 689 | case NE2Command::KeepAlive: 690 | break; 691 | case NE2Command::AddDevice: 692 | { 693 | pkt.add_device.id = be16toh(pkt.add_device.id); 694 | pkt.add_device.dev_info_size = 695 | be16toh(pkt.add_device.dev_info_size); 696 | pkt.add_device.dev_name_size = 697 | be16toh(pkt.add_device.dev_name_size); 698 | 699 | if (optDuplicates == DuplicateMode::Replace) { 700 | auto dev = 701 | OutDevice::newFromNE2AddCommand(infd, pkt); 702 | devices[pkt.add_device.id] = std::move(dev); 703 | break; 704 | } 705 | 706 | auto old = devices.find(pkt.add_device.id); 707 | if (old == devices.end()) { 708 | auto dev = 709 | OutDevice::newFromNE2AddCommand(infd, pkt); 710 | devices[pkt.add_device.id] = std::move(dev); 711 | break; 712 | } 713 | 714 | if (optDuplicates == DuplicateMode::Reject) 715 | throw MsgException( 716 | "protocol error: duplicate device %u", 717 | pkt.add_device.id); 718 | 719 | if (optDuplicates == DuplicateMode::Resume) { 720 | OutDevice::skipNE2AddCommand(infd, pkt); 721 | break; 722 | } 723 | 724 | throw Exception("unhandled --duplicates mode"); 725 | } 726 | case NE2Command::RemoveDevice: 727 | { 728 | auto id = be16toh(pkt.remove_device.id); 729 | auto iter = devices.find(id); 730 | if (iter == devices.end()) 731 | throw MsgException( 732 | "protocol error: missing device %u", id); 733 | devices.erase(iter); 734 | break; 735 | } 736 | case NE2Command::DeviceEvent: 737 | { 738 | auto id = be16toh(pkt.event.id); 739 | auto iter = devices.find(id); 740 | if (iter == devices.end()) 741 | throw MsgException( 742 | "protocol error: missing device %u", id); 743 | pkt.event.event.toHost(); 744 | iter->second->write(pkt.event.event); 745 | break; 746 | } 747 | default: 748 | throw MsgException( 749 | "protocol error: unknown packet type %u", 750 | pkt.cmd); 751 | } 752 | #pragma clang diagnostic pop 753 | } 754 | if (errno) 755 | throw ErrnoException("read error"); 756 | // Otherwise we are at EOF, if we're in listen mode, accept another 757 | // client. 758 | if (serversock) { 759 | inhandle.close(); 760 | inhandle = serversock.accept(); 761 | infd = inhandle.fd(); 762 | goto Resume; 763 | } 764 | return 0; 765 | } 766 | 767 | static int 768 | cmd_command(int argc, char **argv) 769 | { 770 | if (argc < 3) { 771 | ::fprintf(stderr, 772 | "usage: netevent command SOCKETNAME COMMAND\n"); 773 | return 2; 774 | } 775 | 776 | const char *sockname = argv[1]; 777 | if (!sockname[0]) { 778 | ::fprintf(stderr, "bad socket name\n"); 779 | return 3; 780 | } 781 | 782 | string command = join(' ', argv+2, argv+argc); 783 | if (!command.length()) // okay 784 | return 0; 785 | 786 | Socket sock; 787 | if (sockname[0] == '@') 788 | sock.connectUnix(&sockname[1]); 789 | else { 790 | (void)::unlink(sockname); 791 | sock.connectUnix(sockname); 792 | } 793 | 794 | if (!mustWrite(sock.fd(), command.c_str(), command.length())) { 795 | ::fprintf(stderr, "failed to send command: %s\n", 796 | ::strerror(errno)); 797 | } 798 | sock.shutdown(false); 799 | char buf[1024]; 800 | ssize_t got; 801 | while ((got = ::read(sock.fd(), buf, sizeof(buf))) > 0) { 802 | if (!mustWrite(1, buf, size_t(got))) 803 | return -1; // means we lost our output, screw that 804 | } 805 | if (got < 0) { 806 | ::fprintf(stderr, "failed to read response: %s\n", 807 | ::strerror(errno)); 808 | return 2; 809 | } 810 | return 0; 811 | } 812 | 813 | unsigned long kUISetBitIOC[EV_MAX] = {0}; 814 | unsigned long kBitLength[EV_MAX] = {0}; 815 | int 816 | main(int argc, char **argv) 817 | { 818 | // C++ doesn't have designated initializers so... 819 | kUISetBitIOC[EV_KEY] = UI_SET_KEYBIT; 820 | kUISetBitIOC[EV_REL] = UI_SET_RELBIT; 821 | kUISetBitIOC[EV_ABS] = UI_SET_ABSBIT; 822 | kUISetBitIOC[EV_MSC] = UI_SET_MSCBIT; 823 | kUISetBitIOC[EV_LED] = UI_SET_LEDBIT; 824 | kUISetBitIOC[EV_SND] = UI_SET_SNDBIT; 825 | kUISetBitIOC[EV_FF ] = UI_SET_FFBIT; 826 | kUISetBitIOC[EV_SW ] = UI_SET_SWBIT; 827 | kBitLength[EV_KEY] = NLONGS(KEY_CNT); 828 | kBitLength[EV_REL] = NLONGS(REL_CNT); 829 | kBitLength[EV_ABS] = NLONGS(ABS_CNT); 830 | kBitLength[EV_MSC] = NLONGS(MSC_CNT); 831 | kBitLength[EV_LED] = NLONGS(LED_CNT); 832 | kBitLength[EV_SND] = NLONGS(SND_CNT); 833 | kBitLength[EV_FF ] = NLONGS(FF_CNT ); 834 | kBitLength[EV_SW ] = NLONGS(SW_CNT ); 835 | 836 | if (argc < 2) 837 | usage(stderr, EXIT_FAILURE); 838 | 839 | if (!::strcmp(argv[1], "-V") || 840 | !::strcmp(argv[1], "--version")) 841 | version(stdout, EXIT_SUCCESS); 842 | 843 | if (!::strcmp(argv[1], "-h") || 844 | !::strcmp(argv[1], "-?") || 845 | !::strcmp(argv[1], "--help")) 846 | usage(stdout, EXIT_SUCCESS); 847 | 848 | ::optind = 1; 849 | try { 850 | if (!::strcmp(argv[1], "show")) 851 | return cmd_show(argc-1, argv+1); 852 | if (!::strcmp(argv[1], "cat")) 853 | return cmd_cat(argc-1, argv+1); 854 | if (!::strcmp(argv[1], "create")) 855 | return cmd_create(argc-1, argv+1); 856 | if (!::strcmp(argv[1], "daemon")) 857 | return cmd_daemon(argc-1, argv+1); 858 | if (!::strcmp(argv[1], "command")) 859 | return cmd_command(argc-1, argv+1); 860 | } catch (const Exception& ex) { 861 | ::fprintf(stderr, "error: %s\n", ex.what()); 862 | return 2; 863 | } catch (const std::exception& ex) { 864 | ::fprintf(stderr, "unhandled error: %s\n", ex.what()); 865 | return 2; 866 | } 867 | 868 | fprintf(stderr, "unknown command: %s\n", argv[1]); 869 | usage(stderr, EXIT_FAILURE); 870 | } 871 | -------------------------------------------------------------------------------- /src/main.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #pragma clang diagnostic push 18 | #pragma clang diagnostic ignored "-Wreserved-id-macro" 19 | #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" 20 | #include 21 | #include 22 | #pragma clang diagnostic pop 23 | #if defined(__FreeBSD__) 24 | #include 25 | #else 26 | #include 27 | #endif 28 | 29 | #include 30 | #include 31 | #include 32 | #include 33 | 34 | #include "config.h" 35 | #include "types.h" 36 | #include "iohandle.h" 37 | #include "socket.h" 38 | #include "bitfield.h" 39 | #include "utils.h" 40 | 41 | // Until c++23 is everywhere available and I give a damn about dealing with 42 | // this... just shut up... I don't keep up with this stuff anymore, I write 43 | // mostly rust code nowadays... 44 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 45 | 46 | #define Packed __attribute__((packed)) 47 | 48 | #define LONG_BITS (sizeof(long) * 8) 49 | #define NLONGS(x) (((x) + LONG_BITS - 1) / LONG_BITS) 50 | 51 | template> 52 | using uniq = std::unique_ptr; 53 | using std::function; 54 | 55 | int cmd_daemon(int argc, char **argv); 56 | 57 | // C++ doesn't have designated initializers so this is filled in main() 58 | extern bool gUse_UI_DEV_SETUP; 59 | extern unsigned long kUISetBitIOC[EV_MAX]; 60 | extern unsigned long kBitLength[EV_MAX]; 61 | 62 | // "Internal" input event, equal to the usual 64 bit struct input_event 63 | // because struct timeval varies between architectures 64 | struct InputEvent { 65 | uint64_t tv_sec; 66 | uint32_t tv_usec; 67 | uint16_t type; 68 | uint16_t code; 69 | int32_t value; 70 | uint32_t padding = 0; 71 | 72 | inline void toHost() { 73 | tv_sec = be64toh(tv_sec); 74 | tv_usec = be32toh(tv_usec); 75 | type = be16toh(type); 76 | code = be16toh(code); 77 | value = static_cast(be32toh(value)); 78 | } 79 | inline void toNet() { 80 | tv_sec = htobe64(tv_sec); 81 | tv_usec = htobe32(tv_usec); 82 | type = htobe16(type); 83 | code = htobe16(code); 84 | value = static_cast(htobe32(value)); 85 | } 86 | } Packed; 87 | 88 | static const char kNE2Hello[8] = { 'N', 'E', '2', 'H', 89 | 'e', 'l', 'l', 'o', }; 90 | static const uint16_t kNE2Version = 2; 91 | 92 | enum class NE2Command : uint16_t { 93 | KeepAlive = 0, 94 | AddDevice = 1, 95 | RemoveDevice = 2, 96 | DeviceEvent = 3, 97 | Hello = 4, 98 | }; 99 | 100 | struct NE2Packet { 101 | // -Wnested-anon-types 102 | struct Event { 103 | uint16_t cmd; 104 | uint16_t id; 105 | InputEvent event; 106 | } Packed; 107 | struct AddDevice { 108 | uint16_t cmd; 109 | uint16_t id; 110 | uint16_t dev_info_size; 111 | uint16_t dev_name_size; 112 | } Packed; 113 | struct RemoveDevice { 114 | uint16_t cmd; 115 | uint16_t id; 116 | } Packed; 117 | struct Hello { 118 | uint16_t cmd; 119 | uint16_t version; 120 | char magic[8]; 121 | } Packed; 122 | union { 123 | uint16_t cmd; 124 | Event event; 125 | AddDevice add_device; 126 | RemoveDevice remove_device; 127 | Hello hello; 128 | } Packed; 129 | }; 130 | 131 | void writeHello(int fd); 132 | 133 | unsigned int String2EV(const char* name, size_t length); 134 | 135 | #pragma clang diagnostic push 136 | #pragma clang diagnostic ignored "-Wweak-vtables" 137 | struct DeviceException : Exception { 138 | DeviceException(const char *msg); 139 | }; 140 | #pragma clang diagnostic pop 141 | 142 | struct OutDevice { 143 | OutDevice() = delete; 144 | OutDevice(OutDevice&&) = delete; 145 | OutDevice(const OutDevice&) = delete; 146 | OutDevice(const string& name, struct input_id id); 147 | ~OutDevice(); 148 | 149 | void create(); 150 | void setupAbsoluteAxis(uint16_t code, const struct input_absinfo& ai); 151 | void setEventBit(uint16_t type); 152 | void setGenericBit(unsigned long what, uint16_t bit, 153 | const char *errmsg, ...); 154 | 155 | static uniq newFromNeteventStream(int fd); 156 | static uniq newFromNE2AddCommand(int fd, NE2Packet&); 157 | static void skipNE2AddCommand(int fd, NE2Packet&); 158 | 159 | int fd() const noexcept { 160 | return fd_; 161 | } 162 | 163 | void write(const InputEvent& ev); 164 | 165 | private: 166 | static uniq newFromNE2AddCommand(int, NE2Packet&, bool); 167 | void assertNotCreated(const char *errmsg) const; 168 | 169 | template 170 | void ctl(unsigned long req, T&& data, const char *errmsg, ...) const; 171 | 172 | private: 173 | int fd_; 174 | struct uinput_user_dev user_dev_; 175 | bool created_ = false; 176 | }; 177 | 178 | struct InDevice { 179 | InDevice() = delete; 180 | InDevice(InDevice&&); 181 | InDevice(const InDevice&) = delete; 182 | InDevice(const string& path); 183 | ~InDevice(); 184 | 185 | void writeNeteventHeader(int fd); 186 | void writeNE2AddDevice(int fd, uint16_t id); 187 | 188 | void setName(const string&); 189 | void resetName(); // Set to original name (remembered in name_) 190 | 191 | void persistent(bool on) noexcept; 192 | bool persistent() const noexcept { 193 | return persistent_; 194 | } 195 | 196 | void grab(bool on); 197 | bool grab() const noexcept { 198 | return grabbing_; 199 | } 200 | 201 | bool read(InputEvent *out); 202 | bool eof() const noexcept { 203 | return eof_; 204 | } 205 | 206 | int fd() const noexcept { 207 | return fd_; 208 | } 209 | 210 | const char* name() const noexcept { 211 | return user_dev_.name; 212 | } 213 | 214 | const string& realName() const noexcept { 215 | return name_; 216 | } 217 | 218 | private: 219 | template 220 | int ctl(unsigned long req, T&& data, const char *errmsg, ...) const; 221 | 222 | private: 223 | int fd_; 224 | bool eof_ = false; 225 | bool grabbing_ = false; 226 | bool persistent_ = false; 227 | struct uinput_user_dev user_dev_; 228 | string name_; 229 | Bits evbits_; 230 | }; 231 | 232 | inline void 233 | InDevice::persistent(bool on) noexcept { 234 | persistent_ = on; 235 | } 236 | 237 | struct { 238 | unsigned int num; 239 | const char *const name; 240 | } static const 241 | kEVMap[] = { 242 | { EV_SYN, "SYN" }, 243 | { EV_KEY, "KEY" }, 244 | { EV_REL, "REL" }, 245 | { EV_ABS, "ABS" }, 246 | { EV_MSC, "MSC" }, 247 | { EV_SW , "SW " }, 248 | { EV_LED, "LED" }, 249 | { EV_SND, "SND" }, 250 | { EV_REP, "REP" }, 251 | { EV_FF, "FF" }, 252 | { EV_PWR, "PWR" }, 253 | { EV_FF_STATUS, "FF_STATUS" }, 254 | }; 255 | 256 | static inline constexpr const char* 257 | EV2String(unsigned int ev) 258 | { 259 | return (ev < sizeof(kEVMap)/sizeof(kEVMap[0])) 260 | ? kEVMap[ev].name 261 | : ""; 262 | } 263 | -------------------------------------------------------------------------------- /src/reader.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #include 9 | 10 | #include "main.h" 11 | 12 | InDevice::InDevice(InDevice&& o) 13 | : fd_(o.fd_) 14 | , eof_(o.eof_) 15 | , grabbing_(o.grabbing_) 16 | , user_dev_(o.user_dev_) 17 | , name_(std::move(o.name_)) 18 | , evbits_(std::move(o.evbits_)) 19 | { 20 | o.fd_ = -1; 21 | } 22 | 23 | #pragma clang diagnostic push 24 | #pragma clang diagnostic ignored "-Wformat-nonliteral" 25 | template 26 | int 27 | InDevice::ctl(unsigned long req, T&& data, const char *errmsg, ...) const 28 | { 29 | int rc = ::ioctl(fd_, req, data); 30 | if (rc == -1) { 31 | char buf[1024]; 32 | va_list ap; 33 | va_start(ap, errmsg); 34 | ::vsnprintf(buf, sizeof(buf), errmsg, ap); 35 | va_end(ap); 36 | throw ErrnoException("%s", buf); 37 | } 38 | return rc; 39 | } 40 | #pragma clang diagnostic pop 41 | 42 | InDevice::InDevice(const string& path) 43 | : evbits_(EV_MAX) 44 | { 45 | ::memset(&user_dev_, 0, sizeof(user_dev_)); 46 | 47 | fd_ = ::open(path.c_str(), O_RDONLY | O_CLOEXEC); 48 | if (fd_ < 0) 49 | throw ErrnoException("open(%s)", path.c_str()); 50 | 51 | ctl(EVIOCGNAME(sizeof(user_dev_.name)), user_dev_.name, 52 | "failed to query device name"); 53 | name_.assign(user_dev_.name, 54 | ::strnlen(user_dev_.name, sizeof(user_dev_.name))); 55 | 56 | ctl(EVIOCGID, &user_dev_.id, "failed to query device id"); 57 | 58 | ctl(EVIOCGBIT(EV_SYN, evbits_.byte_size()), evbits_.data(), 59 | "failed to query event device capabilities"); 60 | } 61 | 62 | InDevice::~InDevice() 63 | { 64 | if (fd_ != -1) 65 | ::close(fd_); 66 | } 67 | 68 | void 69 | InDevice::grab(bool on) 70 | { 71 | int data = on ? 1 : 0; 72 | int rc = ::ioctl(fd_, EVIOCGRAB, data); 73 | if (rc == 0) { 74 | grabbing_ = on; 75 | return; 76 | } 77 | if (on == grabbing_) { 78 | // we wouldn't have caused a change, so we expect errors: 79 | if ( (on && errno == EBUSY) || 80 | (!on && errno == EINVAL) ) 81 | { 82 | return; 83 | } 84 | } 85 | throw ErrnoException( 86 | on ? "failed to grab input device" 87 | : "failed to release input device"); 88 | } 89 | 90 | bool 91 | InDevice::read(InputEvent *out) 92 | { 93 | struct input_event ev; 94 | if (!mustRead(fd_, &ev, sizeof(ev))) { 95 | if (!errno) { 96 | eof_ = true; 97 | throw Exception("unexpected EOF"); 98 | } 99 | throw ErrnoException("failed to read from device"); 100 | } 101 | 102 | #ifdef input_event_sec 103 | out->tv_sec = uint64_t(ev.input_event_sec); 104 | out->tv_usec = uint32_t(ev.input_event_usec); 105 | #else 106 | out->tv_sec = uint64_t(ev.time.tv_sec); 107 | out->tv_usec = uint32_t(ev.time.tv_usec); 108 | #endif 109 | 110 | out->type = ev.type; 111 | out->code = ev.code; 112 | out->value = ev.value; 113 | return !eof_; 114 | } 115 | 116 | void 117 | InDevice::setName(const string& name) 118 | { 119 | if (name.length() >= sizeof(user_dev_.name)) 120 | throw MsgException("name too long (%zu > %zu)", 121 | name.length(), 122 | sizeof(user_dev_.name)-1); 123 | ::memcpy(user_dev_.name, name.c_str(), name.length()); 124 | ::memset(&user_dev_.name[name.length()], 0, 125 | sizeof(user_dev_.name) - name.length()); 126 | } 127 | 128 | void 129 | InDevice::resetName() 130 | { 131 | setName(name_); 132 | } 133 | 134 | void 135 | InDevice::writeNeteventHeader(int fd) 136 | { 137 | uint16_t strsz = sizeof(user_dev_); 138 | struct iovec iov[4]; 139 | iov[0].iov_base = &strsz; 140 | iov[0].iov_len = sizeof(strsz); 141 | iov[1].iov_base = user_dev_.name; 142 | iov[1].iov_len = sizeof(user_dev_.name); 143 | iov[2].iov_base = &user_dev_.id; 144 | iov[2].iov_len = sizeof(user_dev_.id); 145 | iov[3].iov_base = evbits_.data(); 146 | iov[3].iov_len = evbits_.byte_size(); 147 | ssize_t len = 0; 148 | for (const auto& i: iov) 149 | len += i.iov_len; 150 | 151 | if (::writev(fd, iov, sizeof(iov)/sizeof(iov[0])) != len) 152 | throw ErrnoException("failed to write device header"); 153 | 154 | static 155 | struct { 156 | uint16_t code; 157 | uint32_t max; 158 | const char *what; 159 | } 160 | const kEntryTypes[] = { // order matters 161 | { EV_KEY, KEY_MAX, "key" }, 162 | { EV_ABS, ABS_MAX, "abs" }, 163 | { EV_REL, REL_MAX, "rel" }, 164 | { EV_MSC, MSC_MAX, "msc" }, 165 | { EV_SW, SW_MAX, "sw" }, 166 | { EV_LED, LED_MAX, "led" }, 167 | }; 168 | 169 | Bits entrybits; 170 | for (const auto& type : kEntryTypes) { 171 | if (!evbits_[type.code]) 172 | continue; 173 | entrybits.resizeNE1Compat(type.max); 174 | ::memset(entrybits.data(), 0, entrybits.byte_size()); 175 | ctl(EVIOCGBIT(type.code, entrybits.byte_size()), 176 | entrybits.data(), 177 | "failed to query %s event bits", type.what); 178 | if (!mustWrite(fd, entrybits.data(), entrybits.byte_size())) 179 | throw ErrnoException("failed to write %s bits", 180 | type.what); 181 | } 182 | 183 | static 184 | struct { 185 | uint16_t code; 186 | uint32_t max; 187 | const char *what; 188 | unsigned long ioc; 189 | } 190 | const kStateTypes[] = { // order matters 191 | // NOTE: netevent 1 compatible lengths! 192 | { EV_KEY, KEY_MAX, "key", EVIOCGKEY(1+KEY_MAX/8) }, 193 | { EV_LED, LED_MAX, "led", EVIOCGLED(1+LED_MAX/8) }, 194 | { EV_SW, SW_MAX, "sw", EVIOCGSW (1+SW_MAX /8) }, 195 | }; 196 | for (const auto& type : kStateTypes) { 197 | if (!evbits_[type.code]) 198 | continue; 199 | entrybits.resizeNE1Compat(type.max); 200 | ::memset(entrybits.data(), 0, entrybits.byte_size()); 201 | ctl(type.ioc, entrybits.data(), 202 | "failed to query %s state bits", type.what); 203 | if (!mustWrite(fd, entrybits.data(), entrybits.byte_size())) 204 | throw ErrnoException("failed to write %s state", 205 | type.what); 206 | } 207 | 208 | if (evbits_[EV_ABS]) { 209 | struct input_absinfo ai; 210 | for (size_t i = 0; i != ABS_MAX; ++i) { 211 | ctl(EVIOCGABS(i), &ai, 212 | "failed to get abs axis %zu info", i); 213 | if (!mustWrite(fd, &ai, sizeof(ai))) 214 | throw ErrnoException( 215 | "failed to write absolute axis %zu", 216 | i); 217 | } 218 | } 219 | } 220 | 221 | void 222 | InDevice::writeNE2AddDevice(int fd, uint16_t id) 223 | { 224 | struct iovec iov[5]; 225 | 226 | NE2Packet pkt = {}; 227 | ::memset(reinterpret_cast(&pkt), 0, sizeof(pkt)); 228 | 229 | pkt.cmd = htobe16(uint16_t(NE2Command::AddDevice)); 230 | pkt.add_device.id = htobe16(id); 231 | pkt.add_device.dev_info_size = htobe16(sizeof(user_dev_)); 232 | pkt.add_device.dev_name_size = htobe16(sizeof(user_dev_.name)); 233 | 234 | iov[0].iov_base = &pkt; 235 | iov[0].iov_len = sizeof(pkt); 236 | 237 | iov[1].iov_base = user_dev_.name; 238 | iov[1].iov_len = sizeof(user_dev_.name); 239 | 240 | struct { 241 | uint16_t bustype; 242 | uint16_t vendor; 243 | uint16_t product; 244 | uint16_t version; 245 | } dev_id = { 246 | htobe16(user_dev_.id.bustype), 247 | htobe16(user_dev_.id.vendor), 248 | htobe16(user_dev_.id.product), 249 | htobe16(user_dev_.id.version), 250 | }; 251 | iov[2].iov_base = &dev_id; 252 | iov[2].iov_len = sizeof(dev_id); 253 | 254 | uint16_t evbitsize = htobe16(evbits_.size()); 255 | iov[3].iov_base = &evbitsize; 256 | iov[3].iov_len = sizeof(evbitsize); 257 | 258 | iov[4].iov_base = evbits_.data(); 259 | iov[4].iov_len = evbits_.byte_size(); 260 | 261 | ssize_t len = 0; 262 | for (const auto& i: iov) 263 | len += i.iov_len; 264 | 265 | if (::writev(fd, iov, sizeof(iov)/sizeof(iov[0])) != len) 266 | throw ErrnoException("failed to write device header"); 267 | 268 | // NOTE: must not be resized, we use setBitCount here 269 | Bits entrybits { 0xFFFF }; 270 | 271 | // remember available abs axis bits 272 | Bits absbits; 273 | 274 | uint16_t netbitcount; 275 | iov[0].iov_base = &netbitcount; 276 | iov[0].iov_len = sizeof(netbitcount); 277 | iov[1].iov_base = entrybits.data(); 278 | for (auto ev: evbits_) { 279 | // Only transfer bits which matter: 280 | if (!ev || !kUISetBitIOC[ev.index()]) 281 | continue; 282 | auto count = kBitLength[ev.index()] * LONG_BITS; 283 | entrybits.setBitCount(size_t(count)); 284 | ctl(EVIOCGBIT(ev.index(), entrybits.byte_size()), 285 | entrybits.data(), 286 | "failed to query bits for event type %zu", 287 | ev.index()); 288 | netbitcount = htobe16(uint16_t(count)); 289 | iov[1].iov_len = entrybits.byte_size(); 290 | len = ssize_t(iov[0].iov_len + iov[1].iov_len); 291 | if (::writev(fd, iov, 2) != len) { 292 | throw ErrnoException( 293 | "failed to write bits for event type %zu", 294 | ev.index()); 295 | } 296 | if (ev.index() == EV_ABS) 297 | absbits = entrybits.dup(); 298 | } 299 | 300 | // absolute axis information: 301 | struct { 302 | int32_t value; 303 | int32_t minimum; 304 | int32_t maximum; 305 | int32_t fuzz; 306 | int32_t flat; 307 | int32_t resolution; 308 | } ai; 309 | for (auto abs : absbits) { 310 | if (!abs) 311 | continue; 312 | struct input_absinfo hostai; 313 | ctl(EVIOCGABS(abs.index()), &hostai, 314 | "failed to query abs axis %zu info", abs.index()); 315 | ai.value = int32_t(htobe32(hostai.value)); 316 | ai.minimum = int32_t(htobe32(hostai.minimum)); 317 | ai.maximum = int32_t(htobe32(hostai.maximum)); 318 | ai.fuzz = int32_t(htobe32(hostai.fuzz)); 319 | ai.flat = int32_t(htobe32(hostai.flat)); 320 | ai.resolution = int32_t(htobe32(hostai.resolution)); 321 | if (!mustWrite(fd, &ai, sizeof(ai))) 322 | throw ErrnoException( 323 | "failed to write absolute axis %zu", abs.index()); 324 | } 325 | 326 | // The next thing in the protocol will be the state, but we currently 327 | // don't bother, and don't know for how many things we want the state 328 | // in the future, so we send a bitfield for the types we will send the 329 | // state for, which we zero out for now: 330 | Bits statebits {evbits_.size()}; 331 | if (!mustWrite(fd, statebits.data(), statebits.byte_size())) 332 | throw ErrnoException("failed to write empty state bitfield"); 333 | } 334 | -------------------------------------------------------------------------------- /src/socket.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #include "socket.h" 9 | #include 10 | #include 11 | #include 12 | 13 | // see main.h 14 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 15 | 16 | Socket::~Socket() 17 | { 18 | this->close(); 19 | } 20 | 21 | void 22 | Socket::close() 23 | { 24 | if (fd_ != -1) { 25 | ::close(fd_); 26 | fd_ = -1; 27 | if (path_.length()) { 28 | if (unlink_) { 29 | unlink_ = false; 30 | ::unlink(path_.c_str()); 31 | } 32 | path_.clear(); 33 | } 34 | } 35 | } 36 | 37 | void 38 | Socket::openUnixStream() 39 | { 40 | close(); 41 | fd_ = ::socket(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0); 42 | if (fd_ < 0) 43 | throw ErrnoException("failed to open socket"); 44 | } 45 | 46 | template 47 | void 48 | Socket::bindUnix(const string& path) 49 | { 50 | openUnixStream(); 51 | 52 | struct sockaddr_un addr; 53 | if (path.length() >= sizeof(addr.sun_path)) 54 | throw MsgException("path too long (%zu >= %zu): '%s'", 55 | path.length(), sizeof(addr.sun_path), 56 | path.c_str()); 57 | 58 | addr.sun_family = AF_UNIX; 59 | uint8_t *data; 60 | if (Abstract) { 61 | addr.sun_path[0] = 0; 62 | data = reinterpret_cast(&addr.sun_path[1]); 63 | } else { 64 | data = reinterpret_cast(&addr.sun_path[0]); 65 | } 66 | ::memcpy(data, path.c_str(), path.length()); 67 | data[path.length()] = 0; 68 | 69 | auto beg = reinterpret_cast(&addr); 70 | auto end = data + path.length(); 71 | 72 | if (!Abstract) 73 | (void)::unlink(path.c_str()); 74 | if (::bind(fd_, reinterpret_cast(&addr), 75 | Abstract ? socklen_t(end-beg) : socklen_t(sizeof(addr))) 76 | != 0) 77 | throw ErrnoException("failed to bind to %s%s", 78 | (Abstract ? "@" : ""), path.c_str()); 79 | if (!Abstract) { 80 | path_ = path; 81 | unlink_ = true; 82 | } 83 | } 84 | template void Socket::bindUnix(const string& path); 85 | template void Socket::bindUnix(const string& path); 86 | 87 | void 88 | Socket::listen() 89 | { 90 | if (::listen(fd_, 5) != 0) 91 | throw ErrnoException("failed to listen on %s%s", 92 | path_.c_str()); 93 | } 94 | 95 | template 96 | void 97 | Socket::connectUnix(const string& path) 98 | { 99 | openUnixStream(); 100 | 101 | struct sockaddr_un addr; 102 | if (path.length() >= sizeof(addr.sun_path)) 103 | throw MsgException("path too long (%zu >= %zu): '%s'", 104 | path.length(), sizeof(addr.sun_path), 105 | path.c_str()); 106 | 107 | addr.sun_family = AF_UNIX; 108 | uint8_t *data; 109 | if (Abstract) { 110 | addr.sun_path[0] = 0; 111 | data = reinterpret_cast(&addr.sun_path[1]); 112 | } else { 113 | data = reinterpret_cast(&addr.sun_path[0]); 114 | } 115 | ::memcpy(data, path.c_str(), path.length()); 116 | data[path.length()] = 0; 117 | 118 | auto beg = reinterpret_cast(&addr); 119 | auto end = data + path.length(); 120 | 121 | if (!Abstract) 122 | path_ = path; 123 | if (::connect(fd_, reinterpret_cast(&addr), 124 | Abstract ? socklen_t(end-beg) : socklen_t(sizeof(addr))) 125 | != 0) 126 | throw ErrnoException("failed to connect to %s%s", 127 | (Abstract ? "@" : ""), path.c_str()); 128 | } 129 | template void Socket::connectUnix(const string& path); 130 | template void Socket::connectUnix(const string& path); 131 | 132 | IOHandle 133 | Socket::accept() 134 | { 135 | struct sockaddr_un un; 136 | socklen_t slen = sizeof(un); 137 | int client = ::accept4(fd_, reinterpret_cast(&un), 138 | &slen, SOCK_CLOEXEC); 139 | if (client < 0) 140 | throw ErrnoException("failed to accept client"); 141 | return {client}; 142 | } 143 | 144 | void 145 | Socket::shutdown(bool read_end) 146 | { 147 | if (::shutdown(fd_, read_end ? SHUT_RD : SHUT_WR) != 0) 148 | throw ErrnoException("shutdown() on socket failed"); 149 | } 150 | -------------------------------------------------------------------------------- /src/socket.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | 10 | #include "types.h" 11 | #include "iohandle.h" 12 | 13 | struct Socket { 14 | Socket(); 15 | Socket(const Socket&) = delete; 16 | ~Socket(); 17 | 18 | void openUnixStream(); 19 | void close(); 20 | template void bindUnix(const std::string& path); 21 | template void listenUnix(const std::string& path); 22 | void listen(); 23 | template void connectUnix(const std::string& path); 24 | IOHandle accept(); 25 | void shutdown(bool read_end); 26 | 27 | int fd() const noexcept; 28 | int release() noexcept; 29 | IOHandle intoIOHandle() noexcept; 30 | 31 | operator bool() const noexcept; 32 | 33 | private: 34 | int fd_; 35 | std::string path_; 36 | bool unlink_ = false; 37 | }; 38 | extern template void Socket::bindUnix(const std::string& path); 39 | extern template void Socket::bindUnix(const std::string& path); 40 | extern template void Socket::connectUnix(const std::string& path); 41 | extern template void Socket::connectUnix(const std::string& path); 42 | 43 | inline 44 | Socket::Socket() 45 | : fd_(-1) 46 | , path_() 47 | {} 48 | 49 | inline int 50 | Socket::fd() const noexcept 51 | { 52 | return fd_; 53 | } 54 | 55 | inline 56 | Socket::operator bool() const noexcept 57 | { 58 | return fd_ != -1; 59 | } 60 | 61 | inline int 62 | Socket::release() noexcept 63 | { 64 | int fd = fd_; 65 | fd_ = -1; 66 | return fd; 67 | } 68 | 69 | inline IOHandle 70 | Socket::intoIOHandle() noexcept 71 | { 72 | return { release() }; 73 | } 74 | 75 | template 76 | inline void 77 | Socket::listenUnix(const std::string& path) { 78 | bindUnix(path); 79 | return listen(); 80 | } 81 | -------------------------------------------------------------------------------- /src/types.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | 10 | #include 11 | 12 | using std::string; 13 | 14 | #pragma clang diagnostic push 15 | #pragma clang diagnostic ignored "-Wweak-vtables" 16 | struct Exception : std::exception { 17 | Exception(const char *msg) : msg_(msg) {} 18 | const char *what() const noexcept override; 19 | protected: 20 | Exception() : Exception(nullptr) {} 21 | private: 22 | const char *msg_; 23 | }; 24 | 25 | struct MsgException : Exception { 26 | MsgException(const MsgException&) = delete; 27 | MsgException(MsgException&& o); 28 | MsgException(const char *msg, ...); 29 | private: 30 | char msgbuf_[4096]; 31 | }; 32 | 33 | struct ErrnoException : Exception { 34 | ErrnoException(const ErrnoException&) = delete; 35 | ErrnoException(ErrnoException&& o); 36 | ErrnoException(const char *msg, ...); 37 | int error() const noexcept { return errno_; } 38 | private: 39 | int errno_; 40 | char msgbuf_[4096]; 41 | }; 42 | #pragma clang diagnostic pop 43 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #pragma once 9 | #pragma clang diagnostic push 10 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 11 | 12 | struct ScopeGuard { 13 | ScopeGuard() = delete; 14 | ScopeGuard(ScopeGuard&& o) : f_(std::move(o.f_)) {} 15 | ScopeGuard(const ScopeGuard&) = delete; 16 | ScopeGuard(std::function f) : f_(f) {} 17 | ~ScopeGuard() { f_(); } 18 | private: 19 | std::function f_; 20 | }; 21 | struct ScopeGuardHelper { // actually gets rid of an unused-variable warning 22 | constexpr ScopeGuardHelper() {} 23 | ScopeGuard operator+(std::function f) { 24 | return {std::move(f)}; 25 | } 26 | }; 27 | #define NE2_CPP_CAT_INDIR(X,Y) X ## Y 28 | #define NE2_CPP_CAT(X,Y) NE2_CPP_CAT_INDIR(X, Y) 29 | #define NE2_CPP_ADDLINE(X) NE2_CPP_CAT(X, __LINE__) 30 | #define scope(exit) \ 31 | auto NE2_CPP_ADDLINE(ne2_scopeguard) = ScopeGuardHelper{}+[&]() 32 | 33 | static inline 34 | bool 35 | mustRead(int fd, void *buf, size_t length) 36 | { 37 | while (length) { 38 | auto got = ::read(fd, buf, length); 39 | if (got == 0) 40 | errno = 0; 41 | if (got <= 0) 42 | return false; 43 | if (size_t(got) > length) { 44 | errno = EFAULT; 45 | return false; 46 | } 47 | #pragma clang diagnostic push 48 | #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" 49 | buf = reinterpret_cast( 50 | reinterpret_cast(buf) + got); 51 | #pragma clang diagnostic pop 52 | length -= size_t(got); 53 | } 54 | return true; 55 | } 56 | 57 | static inline 58 | bool 59 | mustWrite(int fd, const void *buf, size_t length) 60 | { 61 | return ::write(fd, buf, length) == ssize_t(length); 62 | } 63 | 64 | bool parseULong(unsigned long *out, const char *s, size_t maxlen); 65 | bool parseLong(long *out, const char *s, size_t maxlen); 66 | bool parseBool(bool *out, const char *s); 67 | 68 | template 69 | static inline string 70 | join(char c, Iter&& i, Iter&& end) 71 | { 72 | string s; 73 | for (; i != end; ++i) { 74 | if (s.length()) 75 | s.append(1, c); 76 | s.append(*i); 77 | } 78 | return s; 79 | } 80 | 81 | #pragma clang diagnostic pop 82 | -------------------------------------------------------------------------------- /src/writer.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * netevent - low-level event-device sharing 3 | * 4 | * Copyright (C) 2017-2021 Wolfgang Bumiller 5 | * 6 | * SPDX-License-Identifier: GPL-2.0-or-later 7 | */ 8 | #include 9 | #include "main.h" 10 | 11 | #ifdef HAS_UI_DEV_SETUP 12 | bool gUse_UI_DEV_SETUP = true; 13 | #else 14 | bool gUse_UI_DEV_SETUP = false; 15 | #endif 16 | 17 | static const char*const gDevicePaths[] = { 18 | "/dev/uinput", 19 | "/dev/input/uinput", 20 | "/dev/misc/uinput", 21 | }; 22 | 23 | OutDevice::~OutDevice() 24 | { 25 | (void)::ioctl(fd_, UI_DEV_DESTROY); 26 | ::close(fd_); 27 | } 28 | 29 | void 30 | OutDevice::assertNotCreated(const char *errmsg) const 31 | { 32 | if (created_) 33 | throw DeviceException(errmsg); 34 | } 35 | 36 | OutDevice::OutDevice(const string& name, struct input_id id) 37 | { 38 | if (name.length() >= sizeof(user_dev_.name)) 39 | throw DeviceException("device name too long"); 40 | 41 | for (const char *path : gDevicePaths) { 42 | fd_ = ::open(path, O_WRONLY | O_NDELAY | O_CLOEXEC); 43 | if (fd_ < 0) { 44 | if (errno != ENOENT) 45 | throw ErrnoException( 46 | "error opening uinput device"); 47 | continue; 48 | } 49 | break; 50 | } 51 | if (fd_ < 0) 52 | throw DeviceException("cannot find uinput device node"); 53 | 54 | ::memset(&user_dev_, 0, sizeof(user_dev_)); 55 | ::memcpy(user_dev_.name, name.c_str(), name.length()); 56 | ::memcpy(&user_dev_.id, &id, sizeof(id)); 57 | 58 | // Being explicit here: we currently don't support force feedback. 59 | // (I have no way to test it, and don't want to) 60 | user_dev_.ff_effects_max = 0; 61 | #ifdef HAS_UI_DEV_SETUP 62 | if (!gUse_UI_DEV_SETUP) 63 | return; 64 | struct uinput_setup setup; 65 | ::memset(&setup, 0, sizeof(setup)); 66 | ::memcpy(&setup.id, &user_dev_.id, sizeof(user_dev_.id)); 67 | ::memcpy(setup.name, name.c_str(), name.length()); 68 | setup.ff_effects_max = user_dev_.ff_effects_max; 69 | if (::ioctl(fd_, UI_DEV_SETUP, &setup) == 0) 70 | return; 71 | if (errno == EINVAL) { 72 | // Deal with the case where we're compiled with newer headers 73 | // while running with an older kernel. 74 | gUse_UI_DEV_SETUP = false; 75 | } else { 76 | throw ErrnoException("failed to setup uinput device"); 77 | } 78 | #endif 79 | } 80 | 81 | void 82 | OutDevice::setEventBit(uint16_t type) 83 | { 84 | assertNotCreated("trying to enable event type"); 85 | ctl(UI_SET_EVBIT, type, 86 | "failed to enable input bit %u", static_cast(type)); 87 | } 88 | 89 | #pragma clang diagnostic push 90 | #pragma clang diagnostic ignored "-Wformat-nonliteral" 91 | template 92 | void 93 | OutDevice::ctl(unsigned long req, T&& data, const char *errmsg, ...) const 94 | { 95 | if (::ioctl(fd_, req, data) == -1) { 96 | char buf[1024]; 97 | va_list ap; 98 | va_start(ap, errmsg); 99 | ::vsnprintf(buf, sizeof(buf), errmsg, ap); 100 | va_end(ap); 101 | throw ErrnoException(errmsg); 102 | } 103 | } 104 | 105 | void 106 | OutDevice::setGenericBit(unsigned long what, uint16_t bit, 107 | const char *errmsg, ...) 108 | { 109 | if (created_) { 110 | char buf[1024]; 111 | va_list ap; 112 | va_start(ap, errmsg); 113 | ::vsnprintf(buf, sizeof(buf), errmsg, ap); 114 | va_end(ap); 115 | throw ErrnoException("device already created", errmsg); 116 | } 117 | if (::ioctl(fd_, what, bit) == -1) { 118 | char buf[1024]; 119 | va_list ap; 120 | va_start(ap, errmsg); 121 | ::vsnprintf(buf, sizeof(buf), errmsg, ap); 122 | va_end(ap); 123 | throw ErrnoException(errmsg); 124 | } 125 | } 126 | #pragma clang diagnostic pop 127 | 128 | void 129 | OutDevice::setupAbsoluteAxis(uint16_t code, const struct input_absinfo& info) 130 | { 131 | assertNotCreated("trying to set absolute axis"); 132 | #ifdef HAS_UI_DEV_SETUP 133 | if (gUse_UI_DEV_SETUP) { 134 | struct uinput_abs_setup data = { code, info }; 135 | ctl(UI_ABS_SETUP, &data, "failed to setup device axis information"); 136 | return; 137 | } 138 | #endif 139 | user_dev_.absmax[code] = info.maximum; 140 | user_dev_.absmin[code] = info.minimum; 141 | user_dev_.absfuzz[code] = info.fuzz; 142 | user_dev_.absflat[code] = info.flat; 143 | } 144 | 145 | void 146 | OutDevice::create() 147 | { 148 | if (! gUse_UI_DEV_SETUP && 149 | ::write(fd_, &user_dev_, sizeof(user_dev_)) != sizeof(user_dev_)) 150 | { 151 | throw ErrnoException("failed to upload device info"); 152 | } 153 | if (::ioctl(fd_, UI_DEV_CREATE) == -1) 154 | throw ErrnoException("failed to create device"); 155 | created_ = true; 156 | } 157 | 158 | uniq 159 | OutDevice::newFromNeteventStream(int fd) 160 | { 161 | // old netevent protocol: 162 | uint16_t size; 163 | struct uinput_user_dev userdev; 164 | if (!mustRead(fd, &size, sizeof(size))) 165 | throw ErrnoException("i/o error"); 166 | 167 | if (size != sizeof(userdev)) 168 | throw DeviceException( 169 | "protocol error: struct uinput_user_dev size mismatch"); 170 | 171 | ::memset(&userdev, 0, sizeof(userdev)); 172 | if (!mustRead(fd, &userdev.name, sizeof(userdev.name))) 173 | throw ErrnoException("error reading device name"); 174 | if (!mustRead(fd, &userdev.id, sizeof(userdev.id))) 175 | throw ErrnoException("error reading device id"); 176 | 177 | uniq dev { 178 | new OutDevice(string(userdev.name, 179 | ::strnlen(userdev.name, sizeof(userdev.name))), 180 | userdev.id) 181 | }; 182 | 183 | // NOTE: netevent1 used 1+max/8 everywhere for array sizes 184 | Bits bits; 185 | bits.resizeNE1Compat(EV_MAX); 186 | if (!mustRead(fd, bits.data(), bits.byte_size())) 187 | throw ErrnoException("error reading event bits"); 188 | bits.shrinkTo(EV_MAX); 189 | if (EV_FF < bits.size()) 190 | bits[EV_FF] = 0; 191 | for (auto b : bits) 192 | if (b) 193 | dev->setEventBit(uint16_t(b.index())); 194 | 195 | // netevent protocol 1 contained (only) key, abs, rel, msc, sw, led 196 | // data here 197 | 198 | static 199 | struct { 200 | uint16_t code; 201 | uint32_t max; 202 | const char *what; 203 | unsigned long ioc; 204 | } 205 | const kEntryTypes[] = { // order matters 206 | { EV_KEY, KEY_MAX, "key", UI_SET_KEYBIT }, 207 | { EV_ABS, ABS_MAX, "abs", UI_SET_ABSBIT }, 208 | { EV_REL, REL_MAX, "rel", UI_SET_RELBIT }, 209 | { EV_MSC, MSC_MAX, "msc", UI_SET_MSCBIT }, 210 | { EV_SW, SW_MAX, "sw", UI_SET_SWBIT }, 211 | { EV_LED, LED_MAX, "led", UI_SET_LEDBIT }, 212 | }; 213 | 214 | Bits entrybits; 215 | Bits absbits; 216 | for (const auto& type : kEntryTypes) { 217 | // NOTE: netevent used an array of [1 + MAX/8] for each bit 218 | // field 219 | if (!bits[type.code]) 220 | continue; 221 | entrybits.resizeNE1Compat(type.max); 222 | if (!mustRead(fd, entrybits.data(), entrybits.byte_size())) 223 | throw ErrnoException("error reading %s bits", 224 | type.what); 225 | entrybits.shrinkTo(type.max); 226 | for (auto b : entrybits) { 227 | if (b) 228 | dev->setGenericBit( 229 | type.ioc, uint16_t(b.index()), 230 | "failed to set %s bit", type.what); 231 | } 232 | // remember the absolute bits: 233 | if (type.code == EV_ABS) 234 | absbits = std::move(entrybits); 235 | } 236 | 237 | // netevent 1 dumps the key state, LED state and SW state at this point 238 | // note that netevent 1 didn't actually apply it, but rather enabled 239 | // the corresponding codes, we simply skip this step 240 | static 241 | struct { 242 | uint16_t code; 243 | uint32_t max; 244 | const char *what; 245 | } 246 | const kStateTypes[] = { // order matters 247 | { EV_KEY, KEY_MAX, "key" }, 248 | { EV_LED, LED_MAX, "led" }, 249 | { EV_SW, SW_MAX, "sw" }, 250 | }; 251 | for (const auto& type : kStateTypes) { 252 | // NOTE: netevent used an array of [1 + MAX/8] for each bit 253 | // field 254 | if (!bits[type.code]) 255 | continue; 256 | entrybits.resizeNE1Compat(type.max); 257 | if (!mustRead(fd, entrybits.data(), entrybits.byte_size())) 258 | throw ErrnoException("error reading %s state", 259 | type.what); 260 | // we discard this data 261 | } 262 | 263 | if (bits[EV_ABS]) { 264 | struct input_absinfo ai; 265 | for (size_t i = 0; i != ABS_MAX; ++i) { 266 | if (!mustRead(fd, &ai, sizeof(ai))) 267 | throw ErrnoException( 268 | "failed to read absolute axis %zu", i); 269 | if (absbits[i]) 270 | dev->setupAbsoluteAxis(uint16_t(i), ai); 271 | } 272 | } 273 | 274 | dev->create(); 275 | 276 | return dev; 277 | } 278 | 279 | uniq 280 | OutDevice::newFromNE2AddCommand(int fd, NE2Packet& pkt, bool skip) 281 | { 282 | if (pkt.cmd != static_cast(NE2Command::AddDevice)) 283 | throw Exception("internal error: wrong packet"); 284 | 285 | struct uinput_user_dev userdev; 286 | if (pkt.add_device.dev_info_size != sizeof(userdev)) 287 | throw DeviceException( 288 | "protocol error: struct uinput_user_dev size mismatch"); 289 | if (pkt.add_device.dev_name_size != sizeof(userdev.name)) 290 | throw DeviceException( 291 | "protocol error: struct input device name size mismatch"); 292 | 293 | ::memset(&userdev, 0, sizeof(userdev)); 294 | if (!mustRead(fd, &userdev.name, sizeof(userdev.name))) 295 | throw ErrnoException("error reading device name"); 296 | struct { 297 | uint16_t bustype; 298 | uint16_t vendor; 299 | uint16_t product; 300 | uint16_t version; 301 | } dev_id; 302 | if (!mustRead(fd, &dev_id, sizeof(dev_id))) 303 | throw ErrnoException("error reading device id"); 304 | userdev.id.bustype = be16toh(dev_id.bustype); 305 | userdev.id.vendor = be16toh(dev_id.vendor); 306 | userdev.id.product = be16toh(dev_id.product); 307 | userdev.id.version = be16toh(dev_id.version); 308 | 309 | uniq dev { 310 | skip ? nullptr : 311 | new OutDevice(string(userdev.name, 312 | ::strnlen(userdev.name, sizeof(userdev.name))), 313 | userdev.id) 314 | }; 315 | 316 | uint16_t evbitsize = 0; 317 | if (!mustRead(fd, &evbitsize, sizeof(evbitsize))) 318 | throw ErrnoException("failed to read type bitfield size"); 319 | evbitsize = be16toh(evbitsize); 320 | if (evbitsize != EV_MAX) 321 | throw MsgException( 322 | "protocol error: event type count mismatch, got %u != %u", 323 | evbitsize, EV_MAX); 324 | 325 | Bits evbits; 326 | evbits.resize(EV_MAX); 327 | if (!mustRead(fd, evbits.data(), evbits.byte_size())) 328 | throw ErrnoException("error reading event bits"); 329 | if (dev) { 330 | for (auto bit : evbits) 331 | if (bit && bit.index() != EV_FF) 332 | dev->setEventBit(uint16_t(bit.index())); 333 | } 334 | 335 | Bits entrybits; 336 | Bits absbits; 337 | for (auto ev : evbits) { 338 | if (!ev || !kUISetBitIOC[ev.index()]) 339 | continue; 340 | uint16_t count; 341 | if (!mustRead(fd, &count, sizeof(count))) 342 | throw ErrnoException( 343 | "failed to read type %zu bit count", 344 | ev.index()); 345 | count = be16toh(count); 346 | entrybits.resize(count); 347 | if (!mustRead(fd, entrybits.data(), entrybits.byte_size())) 348 | throw ErrnoException( 349 | "failed to read type %zu bit field", 350 | ev.index()); 351 | 352 | auto ioc = kUISetBitIOC[ev.index()]; 353 | if (dev) { 354 | for (auto b : entrybits) { 355 | if (b) 356 | dev->setGenericBit( 357 | ioc, uint16_t(b.index()), 358 | "failed to set %zu bit %zu", 359 | ev.index(), b.index()); 360 | } 361 | } 362 | 363 | if (ev.index() == EV_ABS) 364 | absbits = std::move(entrybits); 365 | } 366 | 367 | struct { 368 | int32_t value; 369 | int32_t minimum; 370 | int32_t maximum; 371 | int32_t fuzz; 372 | int32_t flat; 373 | int32_t resolution; 374 | } ai; 375 | for (auto abs : absbits) { 376 | if (!abs) 377 | continue; 378 | if (!mustRead(fd, &ai, sizeof(ai))) 379 | throw ErrnoException( 380 | "failed to read absolute axis %zu", abs.index()); 381 | if (!dev) 382 | continue; 383 | struct input_absinfo hostai; 384 | hostai.value = int32_t(be32toh(ai.value)); 385 | hostai.minimum = int32_t(be32toh(ai.minimum)); 386 | hostai.maximum = int32_t(be32toh(ai.maximum)); 387 | hostai.fuzz = int32_t(be32toh(ai.fuzz)); 388 | hostai.flat = int32_t(be32toh(ai.flat)); 389 | hostai.resolution = int32_t(be32toh(ai.resolution)); 390 | dev->setupAbsoluteAxis(uint16_t(abs.index()), hostai); 391 | } 392 | 393 | // Skip the state: 394 | Bits statebits {EV_MAX}; 395 | if (!mustRead(fd, statebits.data(), statebits.byte_size())) 396 | throw ErrnoException("failed to read state bitfield"); 397 | for (auto i : statebits) { 398 | if (i) { 399 | ::fprintf(stderr, "got unexpected state bits\n"); 400 | break; 401 | } 402 | } 403 | 404 | if (dev) 405 | dev->create(); 406 | 407 | return dev; 408 | } 409 | 410 | void 411 | OutDevice::skipNE2AddCommand(int fd, NE2Packet& pkt) 412 | { 413 | (void)newFromNE2AddCommand(fd, pkt, true); 414 | } 415 | 416 | uniq 417 | OutDevice::newFromNE2AddCommand(int fd, NE2Packet& pkt) 418 | { 419 | return newFromNE2AddCommand(fd, pkt, false); 420 | } 421 | 422 | void 423 | OutDevice::write(const InputEvent& ie) 424 | { 425 | // We do not support these: 426 | if (ie.type == EV_FF) 427 | return; 428 | struct input_event ev; 429 | 430 | #ifdef input_event_sec 431 | ev.input_event_sec = time_t(ie.tv_sec); 432 | ev.input_event_usec = ie.tv_usec; 433 | #else 434 | ev.time.tv_sec = time_t(ie.tv_sec); 435 | ev.time.tv_usec = ie.tv_usec; 436 | #endif 437 | 438 | ev.type = ie.type; 439 | ev.code = ie.code; 440 | ev.value = ie.value; 441 | if (!mustWrite(fd_, &ev, sizeof(ev))) 442 | throw ErrnoException("failed to write event"); 443 | } 444 | --------------------------------------------------------------------------------