diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dbeaf58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +ve/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dc994e9 --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +all: capture playback + +bin: + mkdir -p bin + +capture: bin capture.cpp + g++ capture.c -o bin/capture + +clean: + rm -Rf bin + +playback: bin playback.cpp + g++ playback.c -o bin/playback diff --git a/README.md b/README.md index b7395ba..c5bedbe 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # symkey -A simple play tool for capturing keyboard and mouse and replaying them. Useful for Hypixel Skyblock \ No newline at end of file +This is a quick-and-dirty project for programmatically controlling keyboard and mouse. +I use it primarily to autoplay Minecraft on Hypixel Skyblock. +Shhh, don't tell. diff --git a/capnproto-example/Makefile b/capnproto-example/Makefile new file mode 100644 index 0000000..f520545 --- /dev/null +++ b/capnproto-example/Makefile @@ -0,0 +1,20 @@ +all: client + +CAPNPROTO=/home/eliribble/src/capnproto +CXX=g++ +LIBS=\ + kj \ + kj-async \ + capnp \ + capnp-rpc + +calculator.capnp.h: calculator.capnp + capnp compile -oc++ calculator.capnp + + +CLIENT_SRCS=calculator-client.c++ calculator.capnp.c++ +client: $(CLIENT_SRCS) calculator.capnp.h + $(CXX) $(CLIENT_SRCS) -L $(CAPNPROTO)/c++/.libs $(addprefix -l,$(LIBS)) -o client + +clean: + rm client diff --git a/capnproto-example/calculator-client.c++ b/capnproto-example/calculator-client.c++ new file mode 100644 index 0000000..5d84529 --- /dev/null +++ b/capnproto-example/calculator-client.c++ @@ -0,0 +1,367 @@ +// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "calculator.capnp.h" +#include +#include +#include +#include + +class PowerFunction final: public Calculator::Function::Server { + // An implementation of the Function interface wrapping pow(). Note that + // we're implementing this on the client side and will pass a reference to + // the server. The server will then be able to make calls back to the client. + +public: + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == 2, "Wrong number of parameters."); + context.getResults().setValue(pow(params[0], params[1])); + return kj::READY_NOW; + } +}; + +int main(int argc, const char* argv[]) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " HOST:PORT\n" + "Connects to the Calculator server at the given address and " + "does some RPCs." << std::endl; + return 1; + } + + capnp::EzRpcClient client(argv[1]); + Calculator::Client calculator = client.getMain(); + + // Keep an eye on `waitScope`. Whenever you see it used is a place where we + // stop and wait for the server to respond. If a line of code does not use + // `waitScope`, then it does not block! + auto& waitScope = client.getWaitScope(); + + { + // Make a request that just evaluates the literal value 123. + // + // What's interesting here is that evaluate() returns a "Value", which is + // another interface and therefore points back to an object living on the + // server. We then have to call read() on that object to read it. + // However, even though we are making two RPC's, this block executes in + // *one* network round trip because of promise pipelining: we do not wait + // for the first call to complete before we send the second call to the + // server. + + std::cout << "Evaluating a literal... "; + std::cout.flush(); + + // Set up the request. + auto request = calculator.evaluateRequest(); + request.getExpression().setLiteral(123); + + // Send it, which returns a promise for the result (without blocking). + auto evalPromise = request.send(); + + // Using the promise, create a pipelined request to call read() on the + // returned object, and then send that. + auto readPromise = evalPromise.getValue().readRequest().send(); + + // Now that we've sent all the requests, wait for the response. Until this + // point, we haven't waited at all! + auto response = readPromise.wait(waitScope); + KJ_ASSERT(response.getValue() == 123); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request to evaluate 123 + 45 - 67. + // + // The Calculator interface requires that we first call getOperator() to + // get the addition and subtraction functions, then call evaluate() to use + // them. But, once again, we can get both functions, call evaluate(), and + // then read() the result -- four RPCs -- in the time of *one* network + // round trip, because of promise pipelining. + + std::cout << "Using add and subtract... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client subtract = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "subtract" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::SUBTRACT); + subtract = request.send().getFunc(); + } + + // Build the request to evaluate 123 + 45 - 67. + auto request = calculator.evaluateRequest(); + + auto subtractCall = request.getExpression().initCall(); + subtractCall.setFunction(subtract); + auto subtractParams = subtractCall.initParams(2); + subtractParams[1].setLiteral(67); + + auto addCall = subtractParams[0].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setLiteral(123); + addParams[1].setLiteral(45); + + // Send the evaluate() request, read() the result, and wait for read() to + // finish. + auto evalPromise = request.send(); + auto readPromise = evalPromise.getValue().readRequest().send(); + + auto response = readPromise.wait(waitScope); + KJ_ASSERT(response.getValue() == 101); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request to evaluate 4 * 6, then use the result in two more + // requests that add 3 and 5. + // + // Since evaluate() returns its result wrapped in a `Value`, we can pass + // that `Value` back to the server in subsequent requests before the first + // `evaluate()` has actually returned. Thus, this example again does only + // one network round trip. + + std::cout << "Pipelining eval() calls... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client multiply = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "multiply" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::MULTIPLY); + multiply = request.send().getFunc(); + } + + // Build the request to evaluate 4 * 6 + auto request = calculator.evaluateRequest(); + + auto multiplyCall = request.getExpression().initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[0].setLiteral(4); + multiplyParams[1].setLiteral(6); + + auto multiplyResult = request.send().getValue(); + + // Use the result in two calls that add 3 and add 5. + + auto add3Request = calculator.evaluateRequest(); + auto add3Call = add3Request.getExpression().initCall(); + add3Call.setFunction(add); + auto add3Params = add3Call.initParams(2); + add3Params[0].setPreviousResult(multiplyResult); + add3Params[1].setLiteral(3); + auto add3Promise = add3Request.send().getValue().readRequest().send(); + + auto add5Request = calculator.evaluateRequest(); + auto add5Call = add5Request.getExpression().initCall(); + add5Call.setFunction(add); + auto add5Params = add5Call.initParams(2); + add5Params[0].setPreviousResult(multiplyResult); + add5Params[1].setLiteral(5); + auto add5Promise = add5Request.send().getValue().readRequest().send(); + + // Now wait for the results. + KJ_ASSERT(add3Promise.wait(waitScope).getValue() == 27); + KJ_ASSERT(add5Promise.wait(waitScope).getValue() == 29); + + std::cout << "PASS" << std::endl; + } + + { + // Our calculator interface supports defining functions. Here we use it + // to define two functions and then make calls to them as follows: + // + // f(x, y) = x * 100 + y + // g(x) = f(x, x + 1) * 2; + // f(12, 34) + // g(21) + // + // Once again, the whole thing takes only one network round trip. + + std::cout << "Defining functions... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + Calculator::Function::Client multiply = nullptr; + Calculator::Function::Client f = nullptr; + Calculator::Function::Client g = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + { + // Get the "multiply" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::MULTIPLY); + multiply = request.send().getFunc(); + } + + { + // Define f. + auto request = calculator.defFunctionRequest(); + request.setParamCount(2); + + { + // Build the function body. + auto addCall = request.getBody().initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[1].setParameter(1); // y + + auto multiplyCall = addParams[0].initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[0].setParameter(0); // x + multiplyParams[1].setLiteral(100); + } + + f = request.send().getFunc(); + } + + { + // Define g. + auto request = calculator.defFunctionRequest(); + request.setParamCount(1); + + { + // Build the function body. + auto multiplyCall = request.getBody().initCall(); + multiplyCall.setFunction(multiply); + auto multiplyParams = multiplyCall.initParams(2); + multiplyParams[1].setLiteral(2); + + auto fCall = multiplyParams[0].initCall(); + fCall.setFunction(f); + auto fParams = fCall.initParams(2); + fParams[0].setParameter(0); + + auto addCall = fParams[1].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setParameter(0); + addParams[1].setLiteral(1); + } + + g = request.send().getFunc(); + } + + // OK, we've defined all our functions. Now create our eval requests. + + // f(12, 34) + auto fEvalRequest = calculator.evaluateRequest(); + auto fCall = fEvalRequest.initExpression().initCall(); + fCall.setFunction(f); + auto fParams = fCall.initParams(2); + fParams[0].setLiteral(12); + fParams[1].setLiteral(34); + auto fEvalPromise = fEvalRequest.send().getValue().readRequest().send(); + + // g(21) + auto gEvalRequest = calculator.evaluateRequest(); + auto gCall = gEvalRequest.initExpression().initCall(); + gCall.setFunction(g); + gCall.initParams(1)[0].setLiteral(21); + auto gEvalPromise = gEvalRequest.send().getValue().readRequest().send(); + + // Wait for the results. + KJ_ASSERT(fEvalPromise.wait(waitScope).getValue() == 1234); + KJ_ASSERT(gEvalPromise.wait(waitScope).getValue() == 4244); + + std::cout << "PASS" << std::endl; + } + + { + // Make a request that will call back to a function defined locally. + // + // Specifically, we will compute 2^(4 + 5). However, exponent is not + // defined by the Calculator server. So, we'll implement the Function + // interface locally and pass it to the server for it to use when + // evaluating the expression. + // + // This example requires two network round trips to complete, because the + // server calls back to the client once before finishing. In this + // particular case, this could potentially be optimized by using a tail + // call on the server side -- see CallContext::tailCall(). However, to + // keep the example simpler, we haven't implemented this optimization in + // the sample server. + + std::cout << "Using a callback... "; + std::cout.flush(); + + Calculator::Function::Client add = nullptr; + + { + // Get the "add" function from the server. + auto request = calculator.getOperatorRequest(); + request.setOp(Calculator::Operator::ADD); + add = request.send().getFunc(); + } + + // Build the eval request for 2^(4+5). + auto request = calculator.evaluateRequest(); + + auto powCall = request.getExpression().initCall(); + powCall.setFunction(kj::heap()); + auto powParams = powCall.initParams(2); + powParams[0].setLiteral(2); + + auto addCall = powParams[1].initCall(); + addCall.setFunction(add); + auto addParams = addCall.initParams(2); + addParams[0].setLiteral(4); + addParams[1].setLiteral(5); + + // Send the request and wait. + auto response = request.send().getValue().readRequest() + .send().wait(waitScope); + KJ_ASSERT(response.getValue() == 512); + + std::cout << "PASS" << std::endl; + } + + return 0; +} diff --git a/capnproto-example/calculator-server.c++ b/capnproto-example/calculator-server.c++ new file mode 100644 index 0000000..c2593be --- /dev/null +++ b/capnproto-example/calculator-server.c++ @@ -0,0 +1,215 @@ +// Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +// Licensed under the MIT License: +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#include "calculator.capnp.h" +#include +#include +#include +#include + +typedef unsigned int uint; + +kj::Promise readValue(Calculator::Value::Client value) { + // Helper function to asynchronously call read() on a Calculator::Value and + // return a promise for the result. (In the future, the generated code might + // include something like this automatically.) + + return value.readRequest().send() + .then([](capnp::Response result) { + return result.getValue(); + }); +} + +kj::Promise evaluateImpl( + Calculator::Expression::Reader expression, + capnp::List::Reader params = capnp::List::Reader()) { + // Implementation of CalculatorImpl::evaluate(), also shared by + // FunctionImpl::call(). In the latter case, `params` are the parameter + // values passed to the function; in the former case, `params` is just an + // empty list. + + switch (expression.which()) { + case Calculator::Expression::LITERAL: + return expression.getLiteral(); + + case Calculator::Expression::PREVIOUS_RESULT: + return readValue(expression.getPreviousResult()); + + case Calculator::Expression::PARAMETER: { + KJ_REQUIRE(expression.getParameter() < params.size(), + "Parameter index out-of-range."); + return params[expression.getParameter()]; + } + + case Calculator::Expression::CALL: { + auto call = expression.getCall(); + auto func = call.getFunction(); + + // Evaluate each parameter. + kj::Array> paramPromises = + KJ_MAP(param, call.getParams()) { + return evaluateImpl(param, params); + }; + + // Join the array of promises into a promise for an array. + kj::Promise> joinedParams = + kj::joinPromises(kj::mv(paramPromises)); + + // When the parameters are complete, call the function. + return joinedParams.then([KJ_CPCAP(func)](kj::Array&& paramValues) mutable { + auto request = func.callRequest(); + request.setParams(paramValues); + return request.send().then( + [](capnp::Response&& result) { + return result.getValue(); + }); + }); + } + + default: + // Throw an exception. + KJ_FAIL_REQUIRE("Unknown expression type."); + } +} + +class ValueImpl final: public Calculator::Value::Server { + // Simple implementation of the Calculator.Value Cap'n Proto interface. + +public: + ValueImpl(double value): value(value) {} + + kj::Promise read(ReadContext context) { + context.getResults().setValue(value); + return kj::READY_NOW; + } + +private: + double value; +}; + +class FunctionImpl final: public Calculator::Function::Server { + // Implementation of the Calculator.Function Cap'n Proto interface, where the + // function is defined by a Calculator.Expression. + +public: + FunctionImpl(uint paramCount, Calculator::Expression::Reader body) + : paramCount(paramCount) { + this->body.setRoot(body); + } + + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == paramCount, "Wrong number of parameters."); + + return evaluateImpl(body.getRoot(), params) + .then([KJ_CPCAP(context)](double value) mutable { + context.getResults().setValue(value); + }); + } + +private: + uint paramCount; + // The function's arity. + + capnp::MallocMessageBuilder body; + // Stores a permanent copy of the function body. +}; + +class OperatorImpl final: public Calculator::Function::Server { + // Implementation of the Calculator.Function Cap'n Proto interface, wrapping + // basic binary arithmetic operators. + +public: + OperatorImpl(Calculator::Operator op): op(op) {} + + kj::Promise call(CallContext context) { + auto params = context.getParams().getParams(); + KJ_REQUIRE(params.size() == 2, "Wrong number of parameters."); + + double result; + switch (op) { + case Calculator::Operator::ADD: result = params[0] + params[1]; break; + case Calculator::Operator::SUBTRACT:result = params[0] - params[1]; break; + case Calculator::Operator::MULTIPLY:result = params[0] * params[1]; break; + case Calculator::Operator::DIVIDE: result = params[0] / params[1]; break; + default: + KJ_FAIL_REQUIRE("Unknown operator."); + } + + context.getResults().setValue(result); + return kj::READY_NOW; + } + +private: + Calculator::Operator op; +}; + +class CalculatorImpl final: public Calculator::Server { + // Implementation of the Calculator Cap'n Proto interface. + +public: + kj::Promise evaluate(EvaluateContext context) override { + return evaluateImpl(context.getParams().getExpression()) + .then([KJ_CPCAP(context)](double value) mutable { + context.getResults().setValue(kj::heap(value)); + }); + } + + kj::Promise defFunction(DefFunctionContext context) override { + auto params = context.getParams(); + context.getResults().setFunc(kj::heap( + params.getParamCount(), params.getBody())); + return kj::READY_NOW; + } + + kj::Promise getOperator(GetOperatorContext context) override { + context.getResults().setFunc(kj::heap( + context.getParams().getOp())); + return kj::READY_NOW; + } +}; + +int main(int argc, const char* argv[]) { + if (argc != 2) { + std::cerr << "usage: " << argv[0] << " ADDRESS[:PORT]\n" + "Runs the server bound to the given address/port.\n" + "ADDRESS may be '*' to bind to all local addresses.\n" + ":PORT may be omitted to choose a port automatically." << std::endl; + return 1; + } + + // Set up a server. + capnp::EzRpcServer server(kj::heap(), argv[1]); + + // Write the port number to stdout, in case it was chosen automatically. + auto& waitScope = server.getWaitScope(); + uint port = server.getPort().wait(waitScope); + if (port == 0) { + // The address format "unix:/path/to/socket" opens a unix domain socket, + // in which case the port will be zero. + std::cout << "Listening on Unix socket..." << std::endl; + } else { + std::cout << "Listening on port " << port << "..." << std::endl; + } + + // Run forever, accepting connections and handling requests. + kj::NEVER_DONE.wait(waitScope); +} diff --git a/capnproto-example/calculator.capnp b/capnproto-example/calculator.capnp new file mode 100644 index 0000000..adc8294 --- /dev/null +++ b/capnproto-example/calculator.capnp @@ -0,0 +1,118 @@ +# Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors +# Licensed under the MIT License: +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +@0x85150b117366d14b; + +interface Calculator { + # A "simple" mathematical calculator, callable via RPC. + # + # But, to show off Cap'n Proto, we add some twists: + # + # - You can use the result from one call as the input to the next + # without a network round trip. To accomplish this, evaluate() + # returns a `Value` object wrapping the actual numeric value. + # This object may be used in a subsequent expression. With + # promise pipelining, the Value can actually be used before + # the evaluate() call that creates it returns! + # + # - You can define new functions, and then call them. This again + # shows off pipelining, but it also gives the client the + # opportunity to define a function on the client side and have + # the server call back to it. + # + # - The basic arithmetic operators are exposed as Functions, and + # you have to call getOperator() to obtain them from the server. + # This again demonstrates pipelining -- using getOperator() to + # get each operator and then using them in evaluate() still + # only takes one network round trip. + + evaluate @0 (expression :Expression) -> (value :Value); + # Evaluate the given expression and return the result. The + # result is returned wrapped in a Value interface so that you + # may pass it back to the server in a pipelined request. To + # actually get the numeric value, you must call read() on the + # Value -- but again, this can be pipelined so that it incurs + # no additional latency. + + struct Expression { + # A numeric expression. + + union { + literal @0 :Float64; + # A literal numeric value. + + previousResult @1 :Value; + # A value that was (or, will be) returned by a previous + # evaluate(). + + parameter @2 :UInt32; + # A parameter to the function (only valid in function bodies; + # see defFunction). + + call :group { + # Call a function on a list of parameters. + function @3 :Function; + params @4 :List(Expression); + } + } + } + + interface Value { + # Wraps a numeric value in an RPC object. This allows the value + # to be used in subsequent evaluate() requests without the client + # waiting for the evaluate() that returns the Value to finish. + + read @0 () -> (value :Float64); + # Read back the raw numeric value. + } + + defFunction @1 (paramCount :Int32, body :Expression) + -> (func :Function); + # Define a function that takes `paramCount` parameters and returns the + # evaluation of `body` after substituting these parameters. + + interface Function { + # An algebraic function. Can be called directly, or can be used inside + # an Expression. + # + # A client can create a Function that runs on the server side using + # `defFunction()` or `getOperator()`. Alternatively, a client can + # implement a Function on the client side and the server will call back + # to it. However, a function defined on the client side will require a + # network round trip whenever the server needs to call it, whereas + # functions defined on the server and then passed back to it are called + # locally. + + call @0 (params :List(Float64)) -> (value :Float64); + # Call the function on the given parameters. + } + + getOperator @2 (op :Operator) -> (func :Function); + # Get a Function representing an arithmetic operator, which can then be + # used in Expressions. + + enum Operator { + add @0; + subtract @1; + multiply @2; + divide @3; + } +} diff --git a/capture.cpp b/capture.cpp new file mode 100644 index 0000000..ef04ad7 --- /dev/null +++ b/capture.cpp @@ -0,0 +1,220 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define MAX_EVENTS 16 +#define CONTENT_BUFFER_SIZE 32 + +constexpr std::string_view KEYBOARD = "k"; +constexpr std::string_view MOUSE = "m"; + +int dump_event(struct timespec* start, struct input_event* event, const std::string_view& type); + +int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event); +int event_content_mouse(char* buffer, int buffer_size, struct input_event* event); + +int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode); + +static inline void timespec_diff(struct timespec* a, struct timespec* b, struct timespec* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_nsec = a->tv_nsec - b->tv_nsec; + if (result->tv_nsec < 0) { + --result->tv_sec; + result->tv_nsec += 1000000000L; + } +} + +static inline void timeval_diff(struct timeval* a, struct timeval* b, struct timeval* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_usec = a->tv_usec - b->tv_usec; + if (result->tv_usec < 0) { + --result->tv_sec; + result->tv_usec += 1000000L; + } +} + +static inline void time_diff(struct timeval* a, struct timespec* b, struct timeval* result) { + result->tv_sec = a->tv_sec - b->tv_sec; + result->tv_usec = a->tv_usec - (b->tv_nsec / 1000); + if (result->tv_usec < 0) { + --result->tv_sec; + result->tv_usec += 1000000L; + } +} + +int dump_event(struct timespec* start, struct input_event* event, const std::string_view& type) { + struct timespec now; + struct timespec diff; + char content_buffer[CONTENT_BUFFER_SIZE]; + + clock_gettime(CLOCK_MONOTONIC, &now); + timespec_diff(&now, start, &diff); + // time_diff(&(event.time), start, &diff); + + if(type == MOUSE) { + if(event_content_mouse(content_buffer, CONTENT_BUFFER_SIZE, event)) { + return 1; + } + } else if(type == KEYBOARD) { + // Ignore all but EV_KEY events on keyboard, they have no useful content. + if(event->type != EV_KEY) { + return 0; + } + if(event_content_keyboard(content_buffer, CONTENT_BUFFER_SIZE, event)) { + return 1; + } + } else { + fprintf(stderr, "Unknown event type.\n"); + return 1; + } + start->tv_sec = now.tv_sec; + start->tv_nsec = now.tv_nsec; + + std::cout << + diff.tv_sec << " " << + diff.tv_nsec << "," << + type << "," << + content_buffer << std::endl; + + fflush(stdout); + return 0; +} + +int event_content_keyboard(char* buffer, int buffer_size, struct input_event* event) { + sprintf(buffer, "%d,%d,%d", + event->type, + event->code, + event->value); + return 0; +} + +int event_content_mouse(char* buffer, int buffer_size, struct input_event* event) { + unsigned char button, bLeft, bMiddle, bRight; + unsigned char *ptr = (unsigned char*)event; + int i; + char x, y; + button = ptr[0]; + bLeft = button & 0x1; + bMiddle = ( button & 0x4 ) > 0; + bRight = ( button & 0x2 ) > 0; + x=(char) ptr[1]; + y=(char) ptr[2]; + + int chars = sprintf(buffer, "l%d,m%d,r%d,x%d,y%d", + bLeft, bMiddle, bRight, x, y); + return chars < 0; +} + +int main(int argc, char* argv[]) { + int hotkey_scancode; + if(argc < 2) { + fprintf(stderr, "You must specify a mouse input to track like /dev/input/mouse1."); + exit(EXIT_FAILURE); + } + if(argc < 3) { + fprintf(stderr, "You must specify a keyboard input to track like /dev/input/event3. If you're not sure which to use read through /proc/bus/input/devices and look for 'Handlers=eventX'"); + exit(EXIT_FAILURE); + } + if(argc < 4) { + fprintf(stderr, "You must specify a character to indicate when to start and stop capture. 53 for 'z'.\n"); + exit(EXIT_FAILURE); + } + int matched = sscanf(argv[3], "%d", &hotkey_scancode); + if(matched != 1) { + fprintf(stderr, "Failed to read hotkey scancode.\n"); + exit(EXIT_FAILURE); + } + int result = stream_events(argv[1], argv[2], hotkey_scancode); + exit(result); +} + +int stream_events(char* mouse_path, char* keyboard_path, int hotkey_scancode) { + int keyboard_fd, mouse_fd; + struct timespec start; + struct epoll_event e_event, events[MAX_EVENTS]; + + int has_seen_hotkey = 0; + int running = 1; + int epoll_fd = epoll_create1(0); + if(epoll_fd < 0) { + fprintf(stderr, "Failed to initialize epoll fd: %d", errno); + return 1; + } + if((mouse_fd = open(mouse_path, O_RDONLY)) == -1) { + fprintf(stderr, "Failed to open %s. Are you running as root?\n", mouse_path); + return 1; + } + else { + fprintf(stderr, "%s open OK\n", mouse_path); + } + if((keyboard_fd = open(keyboard_path, O_RDONLY)) == -1) { + fprintf(stderr, "Failed to open %s. Are you running as root?\n", keyboard_path); + return 1; + } + else { + fprintf(stderr, "%s open OK\n", keyboard_path); + } + e_event.events = EPOLLIN; + + e_event.data.fd = keyboard_fd; + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, keyboard_fd, &e_event)) { + fprintf(stderr, "Failed to add keyboard file descriptor\n"); + close(epoll_fd); + return 1; + } + e_event.data.fd = mouse_fd; + if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, mouse_fd, &e_event)) { + fprintf(stderr, "Failed to add mouse file descriptor\n"); + close(epoll_fd); + return 1; + } + + fprintf(stderr, "Waiting for hotkey\n"); + struct input_event i_event; + while(running) { + int event_count = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); + std::string_view type; + for(int i = 0; i < event_count; i++) { + int result = read(events[i].data.fd, &i_event, sizeof(struct input_event)); + if(result < 0) { + perror("Failed to read an event"); + return 1; + } + + if(events[i].data.fd == keyboard_fd) { + type = KEYBOARD; + if(i_event.type == EV_KEY && i_event.code == hotkey_scancode && i_event.value == 1) { + if(has_seen_hotkey) { + fprintf(stderr, "Stop capture\n"); + return 0; + } else { + has_seen_hotkey = 1; + fprintf(stderr, "Start capture\n"); + clock_gettime(CLOCK_MONOTONIC, &start); + continue; + } + } else if(i_event.value == 2) { + continue; + } + } else if (events[i].data.fd == mouse_fd) { + type = MOUSE; + } else { + fprintf(stderr, "Unknown fd"); + return 1; + } + // Wait for the hotkey to start capture + if(has_seen_hotkey && dump_event(&start, &i_event, type)) { + return 1; + } + } + } + return 0; +} diff --git a/capture.py b/capture.py new file mode 100644 index 0000000..05f23fd --- /dev/null +++ b/capture.py @@ -0,0 +1,102 @@ +import argparse +import functools +import keyboard +import logging +import mouse +import pickle +import queue +import sys +import threading +import time + +LOGGER = logging.getLogger("capture") + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", "--delay", + default=0, + type=int, + help="Seconds to wait before capture." + ) + parser.add_argument( + "-t", "--trigger", + default=None, + help="The key to use to trigger start and stop of capture." + ) + parser.add_argument( + "-o", "--output", + default="dump.capture", + help="Name of the file to capture to." + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show verbose logging.", + ) + args = parser.parse_args() + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO) + if args.delay and args.trigger: + print("You cannot specify 'delay' and 'trigger'") + sys.exit(1) + + _do_delay(args.delay) + _do_trigger(args.trigger) + LOGGER.info("Capturing...") + now = time.time() + # Add dummy events to lock in the time start + with open(args.output, "w") as output: + hook = functools.partial(_on_hook, now, output) + keyhook = keyboard.hook(hook) + mousehook = mouse.hook(hook) + try: + keyboard.wait(args.trigger) + except KeyboardInterrupt: + keyboard.unhook(keyhook) + mouse.unhook(mousehook) + LOGGER.info("Wrote %s", args.output) + +def _on_hook(start, output, event): + LOGGER.debug(str(event)) + relative_time = event.time - start + if isinstance(event, keyboard.KeyboardEvent): + output.write( + f"{relative_time},k,{event.event_type},{event.scan_code},{event.name}\n") + elif isinstance(event, mouse.ButtonEvent): + output.write( + f"{relative_time},mb,{event.event_type},{event.button}\n") + elif isinstance(event, mouse.MoveEvent): + output.write( + f"{relative_time},mm,{event.x},{event.y}\n") + elif isinstance(event, mouse.WheelEvent): + output.write( + f"{relative_time},mw,{event.delta}\n") + else: + raise ValueError(f"{event} is not recognized") + +def _do_delay(delay: int) -> None: + if not delay: + return + print("\n") + for i in range(delay): + print(f"\rStarting in {delay-i} seconds") + time.sleep(1) + +def _do_trigger(trigger: str) -> None: + if not trigger: + return + print(f"Waiting for '{trigger}'") + keyboard.wait(trigger) + +def _save_events(event_q: queue.Queue, filename: str) -> None: + events = [] + while not event_q.empty(): + event = event_q.get(block=False) + events.append(event) + with open(filename, "wb") as output: + pickle.dump(events, output) + print(f"Wrote to {filename}") + +if __name__ == "__main__": + main() diff --git a/playback.cpp b/playback.cpp new file mode 100644 index 0000000..4021053 --- /dev/null +++ b/playback.cpp @@ -0,0 +1,261 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +static volatile int is_running = 1; + +int handle_udevice(char* details, int udevice_fd); +int handle_line(char* line, int udevice_fd); +int handle_mouse(char* details, int udevice_fd); +int read_file(char* filename, int udevice_fd); +int setup_udevice(); +void sigint_handler(int dummy); +void teardown_udevice(int fd); + +void emit(int fd, int type, int code, int val) { + struct input_event ie; + ie.type = type; + ie.code = code; + ie.value = val; + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + write(fd, &ie, sizeof(ie)); +} + +int handle_keyboard(char* details, int udevice_fd) { + int code, event_type, value; + int matched = sscanf(details, "%d,%d,%d", &event_type, &code, &value); + if(matched != 3) { + printf("Didn't match enough values for a keyboard event.\n"); + return 1; + } + // printf("Event type: %d, Code: %d, Value: %d\n", + // event_type, code, value); + if(event_type != 1) { + printf("Not sure what to do with event type %d", event_type); + return 1; + } + emit(udevice_fd, EV_KEY, code, value); + emit(udevice_fd, EV_SYN, SYN_REPORT, 0); + + return 0; +} + +int handle_line(char* line, int udevice_fd) { + static time_t total_seconds = 0; + static long total_nanos = 0; + + time_t seconds; + long nanos; + char type; + char details[32]; + struct timespec to_sleep; + struct timespec remaining; + + int matched = sscanf(line, "%ld %ld,%c,%s\n", &seconds, &nanos, &type, details); + if(matched == 0) { + printf("Line '%s' appears incorrect. Exiting", line); + return 1; + } else if(matched < 4) { + printf("Only matched %d", matched); + return 1; + } + + remaining.tv_sec = 0; + remaining.tv_nsec = 0; + to_sleep.tv_sec = seconds; + to_sleep.tv_nsec = nanos; + if(to_sleep.tv_nsec < 0) { + --to_sleep.tv_sec; + to_sleep.tv_nsec += 1000000000L; + } + // printf("Timer %ld %ld\n", timer_seconds, timer_nanos); + // printf("Read %ld %ld\n", seconds, nanos); + // printf("Sleep %ld %ld\n", to_sleep.tv_sec, to_sleep.tv_nsec); + while(nanosleep(&to_sleep, &remaining) == -1) { + if(!is_running) { + return 0; + } + perror("nanosleep error"); + printf("Attempted %ld.%ld sleep\n", to_sleep.tv_sec, to_sleep.tv_nsec); + printf("Need %ld.%ld more seconds for total sleep\n", remaining.tv_sec, remaining.tv_nsec); + to_sleep.tv_sec = remaining.tv_sec; + to_sleep.tv_nsec = remaining.tv_nsec; + } + if(remaining.tv_sec != 0 || remaining.tv_nsec != 0) { + printf("oops, remaining.\n"); + } + total_seconds += to_sleep.tv_sec; + total_nanos += to_sleep.tv_nsec; + if(total_nanos > 1000000000L) { + total_nanos -= 1000000000L; + total_seconds += 1; + } + + // printf("%ld %ld\tslept %ld %ld\n", + // total_seconds, total_nanos, to_sleep.tv_sec, to_sleep.tv_nsec); + if(type == 'k') { + return handle_keyboard(details, udevice_fd); + } else if(type == 'm') { + return handle_mouse(details, udevice_fd); + } else { + printf("Unexpected type %c/n", type); + return 1; + } +} + +int handle_mouse(char* details, int udevice_fd) { + static int current_left = 0; + static int current_middle = 0; + static int current_right = 0; + + int left, middle, right, x, y; + int matched = sscanf(details, "l%d,m%d,r%d,x%d,y%d", + &left, &middle, &right, &x, &y); + if(matched != 5) { + printf("Failed to match enough data for a mouse event.\n"); + return 1; + } + // printf("L: %d M: %d, R: %d, X: %d, Y: %d\n", + // left, middle, right, x, y); + + /* Move the mouse diagonally, 5 units per axis */ + if(x != 0) { + emit(udevice_fd, EV_REL, REL_X, x); + } + if(y != 0) { + emit(udevice_fd, EV_REL, REL_Y, -1 * y); + } + + if(left != current_left) { + emit(udevice_fd, EV_KEY, BTN_LEFT, left); + current_left = left; + } + if(middle != current_middle) { + emit(udevice_fd, EV_KEY, BTN_MIDDLE, middle); + current_middle = middle; + } + if(right != current_right) { + emit(udevice_fd, EV_KEY, BTN_RIGHT, right); + current_right = right; + } + emit(udevice_fd, EV_SYN, SYN_REPORT, 0); + + + return 0; +} + +int main(int argc, char* argv[]) { + int repeat = 1; + if(argc < 2) { + printf("Please provide a capture file."); + exit(EXIT_FAILURE); + } + if(argc == 3) { + int matched = sscanf(argv[2], "%d", &repeat); + if(matched != 1) { + fprintf(stderr, "Failed to read repeat value.\n"); + exit(EXIT_FAILURE); + } + printf("Repeating %d times\n", repeat); + } + + signal(SIGINT, sigint_handler); + + for(int i = 3; i > 0; i--) { + fprintf(stderr, "Playing back in %d seconds\n", i); + sleep(1); + } + + int result = 0; + int udevice_fd = setup_udevice(); + for(int i = 0; is_running && i < repeat; i++) { + fprintf(stderr, "Repeat %d/%d\n", i+1, repeat); + if(read_file(argv[1], udevice_fd)) { + result = EXIT_FAILURE; + } + } + teardown_udevice(udevice_fd); + return result; +} + +int read_file(char* filename, int udevice_fd) { + FILE* fp; + char* line = NULL; + size_t len = 0; + ssize_t read; + + fp = fopen(filename, "r"); + if (fp == NULL) { + printf("Failed to open file %s: %d\n", filename, errno); + return 1; + } + + while(is_running && (read = getline(&line, &len, fp)) != -1) { + if(handle_line(line, udevice_fd)) { + return 1; + } + } + fclose(fp); + return 0; +} + +int setup_udevice() { + struct uinput_setup usetup; + int fd = open("/dev/uinput", O_WRONLY | O_NONBLOCK); + +/* enable mouse button left and relative events */ + + ioctl(fd, UI_SET_EVBIT, EV_KEY); + + // Add keyboard keys. We could do this individually but we're super + // lazy and it appears a loop should work fine based on the linux/input-event-codes.h header + for(int i = KEY_ESC; i <= KEY_MICMUTE; i++) { + ioctl(fd, UI_SET_KEYBIT, i); + } + + // Add mouse buttons + ioctl(fd, UI_SET_KEYBIT, BTN_LEFT); + ioctl(fd, UI_SET_KEYBIT, BTN_RIGHT); + + ioctl(fd, UI_SET_EVBIT, EV_REL); + ioctl(fd, UI_SET_RELBIT, REL_X); + ioctl(fd, UI_SET_RELBIT, REL_Y); + + memset(&usetup, 0, sizeof(usetup)); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0x1234; /* sample vendor */ + usetup.id.product = 0x5678; /* sample product */ + strcpy(usetup.name, "Playback mouses"); + + ioctl(fd, UI_DEV_SETUP, &usetup); + ioctl(fd, UI_DEV_CREATE); + + /* + * On UI_DEV_CREATE the kernel will create the device node for this + * device. We are inserting a pause here so that userspace has time + * to detect, initialize the new device, and can start listening to + * the event, otherwise it will not notice the event we are about + * to send. This pause is only needed in our example code! + */ + // sleep(1); + return fd; +} + +void sigint_handler(int dummy) { + is_running = 0; +} + +void teardown_udevice(int fd) { + ioctl(fd, UI_DEV_DESTROY); + close(fd); +} diff --git a/playback.py b/playback.py new file mode 100644 index 0000000..26c6867 --- /dev/null +++ b/playback.py @@ -0,0 +1,96 @@ +import argparse +import keyboard +import logging +import mouse +import pickle +import threading +import time + +LOGGER = logging.getLogger("playback") + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument( + "-d", "--delay", + default=5, + type=int, + help="Seconds to wait before replay." + ) + parser.add_argument( + "-i", "--input", + default="dump.capture", + help="Name of the file to replay from." + ) + parser.add_argument( + "--verbose", + action="store_true", + help="Show verbose messages." + ) + args = parser.parse_args() + logging.basicConfig( + level = logging.DEBUG if args.verbose else logging.INFO + ) + _do_delay(args.delay) + + events = _load_events(args.input) + _play_events(events) + +def _play_events(events) -> None: + LOGGER.info("Playback started.") + key_state = keyboard.stash_state() + last_time = None + for event in events: + if last_time is not None: + to_sleep = event.time - last_time + if to_sleep > 0: + time.sleep(to_sleep) + last_time = event.time + if isinstance(event, keyboard.KeyboardEvent): + _play_event_keyboard(event) + elif any([ + isinstance(event, mouse.ButtonEvent), + isinstance(event, mouse.MoveEvent), + isinstance(event, mouse.WheelEvent), + ]): + _play_event_mouse(event) + else: + raise ValueError(f"Not a recognized event {event}") + + keyboard.restore_modifiers(key_state) + LOGGER.info("Done.") + +def _play_event_keyboard(event) -> None: + LOGGER.debug("Key %s", event) + key = event.scan_code or event.name + keyboard.press(key) if event.event_type == keyboard.KEY_DOWN else keyboard.release(key) + +def _play_event_mouse(event) -> None: + LOGGER.debug("Mouse %s", event) + if isinstance(event, mouse.ButtonEvent): + if event.event_type == mouse.UP: + mouse.release(event.button) + else: + mouse.press(event.button) + elif isinstance(event, mouse.MoveEvent): + mouse.move(event.x, event.y, absolute=True) + # mouse.move(event.x, event.y) + elif isinstance(event, mouse.WheelEvent): + mouse.wheel(event.delta) + +def _do_delay(delay: int) -> None: + if not delay: + return + print("\n") + for i in range(delay): + print(f"\rStarting in {delay-i} seconds") + time.sleep(1) + +def _load_events(filename: str): + with open(filename, "rb") as input_: + events = pickle.load(input_) + LOGGER.debug("Loaded %s", filename) + return events + + +if __name__ == "__main__": + main() diff --git a/show-keys b/show-keys new file mode 100755 index 0000000..84e846a --- /dev/null +++ b/show-keys @@ -0,0 +1,15 @@ +#!env python3 +import keyboard + +def main() -> None: + keyboard.hook(_on_event) + try: + keyboard.wait() + except KeyboardInterrupt: + print("End.") + +def _on_event(event: keyboard.KeyboardEvent) -> None: + print(event.to_json()) + +if __name__ == "__main__": + main() diff --git a/show-mouse b/show-mouse new file mode 100755 index 0000000..63713a9 --- /dev/null +++ b/show-mouse @@ -0,0 +1,19 @@ +#!env python3 +import keyboard +import logging +import mouse + +def main() -> None: + logging.basicConfig(level=logging.DEBUG) + mouse.hook(_on_event) + try: + mouse.wait() + except KeyboardInterrupt: + pass + print("End.") + +def _on_event(event: keyboard.KeyboardEvent) -> None: + print(event) + +if __name__ == "__main__": + main() diff --git a/show-mouse.c b/show-mouse.c new file mode 100644 index 0000000..87a944c --- /dev/null +++ b/show-mouse.c @@ -0,0 +1,61 @@ +#include +#include +#include +#include +#include + +#define MOUSEFILE "/dev/input/mouse1" +// +int main() +{ + int fd; + struct input_event ie; + // + unsigned char button,bLeft,bMiddle,bRight; + char x,y; + int absolute_x,absolute_y; + + if((fd = open(MOUSEFILE, O_RDONLY)) == -1) { + printf("Device open ERROR\n"); + exit(EXIT_FAILURE); + } + else + { + printf("Device open OK\n"); + } + // + printf("right-click to set absolute x,y coordinates origin (0,0)\n"); + while(read(fd, &ie, sizeof(struct input_event))) + { + unsigned char *ptr = (unsigned char*)&ie; + int i; + // + button=ptr[0]; + bLeft = button & 0x1; + bMiddle = ( button & 0x4 ) > 0; + bRight = ( button & 0x2 ) > 0; + x=(char) ptr[1];y=(char) ptr[2]; + // printf("bLEFT:%d, bMIDDLE: %d, bRIGHT: %d, rx: %d ry=%d\n",bLeft,bMiddle,bRight, x,y); + // + absolute_x+=x; + absolute_y-=y; + if (bRight==1) + { + absolute_x=0; + absolute_y=0; + printf("Absolute x,y coords origin recorded\n"); + } + // + printf("Absolute coords from TOP_LEFT= %i %i\n",absolute_x,absolute_y); + // + // comment to disable the display of raw event structure datas + // + for(i=0; i