From cf889dd8bd983182534457b6778892ef0634e592 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 25 Mar 2018 23:17:15 +0200 Subject: Rough rtpmidi backend skeleton --- backends/Makefile | 2 +- backends/rtpmidi.c | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++++ backends/rtpmidi.h | 94 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 1 deletion(-) create mode 100644 backends/rtpmidi.c create mode 100644 backends/rtpmidi.h diff --git a/backends/Makefile b/backends/Makefile index 446ad70..771e97e 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,6 +1,6 @@ .PHONY: all clean LINUX_BACKENDS = midi.so evdev.so -BACKENDS = artnet.so osc.so loopback.so sacn.so +BACKENDS = artnet.so osc.so loopback.so sacn.so rtpmidi.so SYSTEM := $(shell uname -s) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c new file mode 100644 index 0000000..f77f93b --- /dev/null +++ b/backends/rtpmidi.c @@ -0,0 +1,130 @@ +#include +#include + +#include "rtpmidi.h" + +#define BACKEND_NAME "rtpmidi" + +/** rtpMIDI backend w/ AppleMIDI support + * + * Global configuration + * bind = 0.0.0.0 + * apple-bind = 0.0.0.1 + * mdns-bind = 0.0.0.0 + * mdns-name = mdns-name + * + * Instance configuration + * interface = 0 + * (opt) ssrc = X + * + * apple-session = session-name + * apple-invite = invite-peer + * apple-allow = * + * or + * connect = + * reply-any = 1 + */ + +static struct /*_rtpmidi_global*/ { + int mdns_fd; + char* mdns_name; + size_t nfds; + rtpmidi_fd* fds; +} cfg = { + .mdns_fd = -1, + .mdns_name = NULL, + .nfds = 0, + .fds = NULL +}; + +int init(){ + backend rtpmidi = { + .name = BACKEND_NAME, + .conf = rtpmidi_configure, + .create = rtpmidi_instance, + .conf_instance = rtpmidi_configure_instance, + .channel = rtpmidi_channel, + .handle = rtpmidi_set, + .process = rtpmidi_handle, + .start = rtpmidi_start, + .shutdown = rtpmidi_shutdown + }; + + if(mm_backend_register(rtpmidi)){ + fprintf(stderr, "Failed to register rtpMIDI backend\n"); + return 1; + } + + return 0; +} + +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + free(cfg.mdns_name); + } + + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + //TODO this should create the mdns broadcaster and responder socket + return 0; + } + else if(!strcmp(option, "bind")){ + //TODO open listening control and data fds + } + + fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); + return 1; +} + +static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ + //TODO + return 1; +} + +static instance* rtpmidi_instance(){ + //TODO + return NULL; +} + +static channel* rtpmidi_channel(instance* inst, char* spec){ + //TODO + return NULL; +} + +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ + //TODO + return 1; +} + +static int rtpmidi_handle(size_t num, managed_fd* fds){ + //TODO + return 1; +} + +static int rtpmidi_start(){ + //TODO + return 1; +} + +static int rtpmidi_shutdown(){ + size_t u; + + free(cfg.mdns_name); + if(cfg.mdns_fd >= 0){ + close(cfg.mdns_fd); + } + + for(u = 0; u < cfg.nfds; u++){ + if(cfg.fds[u].data >= 0){ + close(cfg.fds[u].data); + } + if(cfg.fds[u].control >= 0){ + close(cfg.fds[u].control); + } + } + return 0; +} diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h new file mode 100644 index 0000000..f14357f --- /dev/null +++ b/backends/rtpmidi.h @@ -0,0 +1,94 @@ +#include +#include "midimonster.h" + +int init(); +static int rtpmidi_configure(char* option, char* value); +static int rtpmidi_configure_instance(instance* instance, char* option, char* value); +static instance* rtpmidi_instance(); +static channel* rtpmidi_channel(instance* instance, char* spec); +static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); +static int rtpmidi_handle(size_t num, managed_fd* fds); +static int rtpmidi_start(); +static int rtpmidi_shutdown(); + +#define RTPMIDI_DEFAULT_PORTBASE "9001" +#define RTPMIDI_RECV_BUF 4096 + +#define RTPMIDI_COMMAND_LEN +#define RTPMIDI_COMMAND_DATA_OFFSET +#define RTPMIDI_HAS_JOURNAL +#define RTPMIDI_IS_PHANTOM + +typedef enum /*_rtpmidi_peer_mode*/ { + peer_learned, + peer_invited, + peer_invited_by, + peer_sync, + peer_connect +} rtpmidi_peer_mode; + +typedef struct /*_rtpmidi_peer*/ { + rtpmidi_peer_mode mode; + struct sockaddr_storage dest; + socklen_t dest_len; + uint32_t ssrc; +} rtpmidi_peer; + +typedef struct /*_rtpmidi_fd*/ { + int data; + int control; +} rtpmidi_fd; + +typedef struct /*_rtmidi_instance_data*/ { + size_t fd_index; + size_t npeers; + rtpmidi_peer* peers; + uint32_t ssrc; + + //apple-midi config + char* session_name; + char* invite_peers; + char* invite_accept; + + //generic mode config + uint8_t learn_peers; +} rtpmidi_instance_data; + +#pragma pack(push, 1) +typedef struct /*_apple_session_command*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t version; + uint32_t token; + uint32_t ssrc; + //char* name +} apple_command; + +typedef struct /*_apple_session_sync*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint8_t count; + uint8_t res2[3]; + uint64_t timestamp[3]; +} apple_sync; + +typedef struct /*_apple_session_feedback*/ { + uint16_t res1; + uint8_t command[2]; + uint32_t ssrc; + uint32_t sequence; +} apple_feedback; + +typedef struct /*_rtp_midi_header*/ { + uint16_t vpxccmpt; //this is really just an amalgamated constant value + uint16_t sequence; + uint32_t timestamp; + uint32_t ssrc; +} rtpmidi_header; + +typedef struct /*_rtp_midi_command*/ { + uint8_t flags; + uint8_t additional_length; +} rtpmidi_command; +#pragma pack(pop) -- cgit v1.2.3 From 72ffbbd002034216f56dc4d00481b2feb61fdbee Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 27 Mar 2018 18:04:32 +0200 Subject: Make rtpmidi backend behave while not providing functionality --- backends/rtpmidi.c | 170 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 165 insertions(+), 5 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index f77f93b..6a8032a 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,11 @@ +#include +#include +#include #include +#include #include +#include +#include #include "rtpmidi.h" @@ -58,10 +64,118 @@ int init(){ return 0; } +static int rtpmidi_listener(char* host, char* port){ + int fd = -1, status, yes = 1, flags; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM, + .ai_flags = AI_PASSIVE + }; + struct addrinfo* info; + struct addrinfo* addr_it; + + status = getaddrinfo(host, port, &hints, &info); + if(status){ + fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); + return -1; + } + + for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ + fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); + if(fd < 0){ + continue; + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); + } + + yes = 1; + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); + } + + yes = 0; + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); + } + + status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); + if(status < 0){ + close(fd); + continue; + } + + break; + } + + freeaddrinfo(info); + + if(!addr_it){ + fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); + return -1; + } + + //set nonblocking + flags = fcntl(fd, F_GETFL, 0); + if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ + fprintf(stderr, "Failed to set rtpMIDI descriptor nonblocking\n"); + close(fd); + return -1; + } + return 0; +} + +static int rtpmidi_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ + struct addrinfo* head; + struct addrinfo hints = { + .ai_family = AF_UNSPEC, + .ai_socktype = SOCK_DGRAM + }; + + int error = getaddrinfo(host, port, &hints, &head); + if(error || !head){ + fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); + return 1; + } + + memcpy(addr, head->ai_addr, head->ai_addrlen); + *len = head->ai_addrlen; + + freeaddrinfo(head); + return 0; +} + +static int rtpmidi_separate_hostspec(char* in, char** host, char** port, char* default_port){ + size_t u; + + if(!in || !host || !port){ + return 1; + } + + for(u = 0; in[u] && !isspace(in[u]); u++){ + } + + //guess + *host = in; + + if(in[u]){ + in[u] = 0; + *port = in + u + 1; + } + else{ + //no port given + *port = default_port; + } + return 0; +} + static int rtpmidi_configure(char* option, char* value){ if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ - free(cfg.mdns_name); + fprintf(stderr, "Duplicate mdns-name assignment\n"); + return 1; } cfg.mdns_name = strdup(value); @@ -69,11 +183,22 @@ static int rtpmidi_configure(char* option, char* value){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } - //TODO this should create the mdns broadcaster and responder socket return 0; } + else if(!strcmp(option, "mdns-bind")){ + if(cfg.mdns_fd >= 0){ + fprintf(stderr, "Only one mDNS discovery bind is supported\n"); + return 1; + } + + + //TODO create mdns broadcast/responder socket + } else if(!strcmp(option, "bind")){ - //TODO open listening control and data fds + //TODO open listening data fd for raw rtpmidi instance + } + else if(!strcmp(option, "apple-bind")){ + //TODO open control and data fd for applemidi session/rtpmidi+apple-extensions instance } fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); @@ -101,13 +226,48 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v } static int rtpmidi_handle(size_t num, managed_fd* fds){ + //TODO handle discovery + + if(!num){ + return 0; + } + //TODO return 1; } static int rtpmidi_start(){ - //TODO - return 1; + size_t n, u, p; + int rv = 1; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + + //fetch all defined instances + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list\n"); + return 1; + } + + if(!n){ + free(inst); + return 0; + } + + if(!cfg.nfds){ + fprintf(stderr, "Failed to start rtpMIDI backend: no descriptors bound\n"); + goto bail; + } + + if(cfg.mdns_fd < 0){ + fprintf(stderr, "No mDNS discovery interface bound, APPLEMIDI session support disabled\n"); + } + + //TODO initialize all instances + + rv = 0; +bail: + free(inst); + return rv; } static int rtpmidi_shutdown(){ -- cgit v1.2.3 From 588f204eaa6c69a8f09a70d9188cefcb6c6075f6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 02:26:06 +0100 Subject: Partly redesign rtpmidi backend --- backends/rtpmidi.c | 166 ++++++++-------------------------------------------- backends/rtpmidi.h | 10 +--- backends/rtpmidi.md | 85 +++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 147 deletions(-) create mode 100644 backends/rtpmidi.md diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 6a8032a..5387d86 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -11,36 +11,14 @@ #define BACKEND_NAME "rtpmidi" -/** rtpMIDI backend w/ AppleMIDI support - * - * Global configuration - * bind = 0.0.0.0 - * apple-bind = 0.0.0.1 - * mdns-bind = 0.0.0.0 - * mdns-name = mdns-name - * - * Instance configuration - * interface = 0 - * (opt) ssrc = X - * - * apple-session = session-name - * apple-invite = invite-peer - * apple-allow = * - * or - * connect = - * reply-any = 1 - */ - static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; - size_t nfds; - rtpmidi_fd* fds; + uint8_t detect; } cfg = { .mdns_fd = -1, .mdns_name = NULL, - .nfds = 0, - .fds = NULL + .detect = 0 }; int init(){ @@ -57,121 +35,17 @@ int init(){ }; if(mm_backend_register(rtpmidi)){ - fprintf(stderr, "Failed to register rtpMIDI backend\n"); - return 1; - } - - return 0; -} - -static int rtpmidi_listener(char* host, char* port){ - int fd = -1, status, yes = 1, flags; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM, - .ai_flags = AI_PASSIVE - }; - struct addrinfo* info; - struct addrinfo* addr_it; - - status = getaddrinfo(host, port, &hints, &info); - if(status){ - fprintf(stderr, "Failed to get socket info for %s port %s: %s\n", host, port, gai_strerror(status)); - return -1; - } - - for(addr_it = info; addr_it != NULL; addr_it = addr_it->ai_next){ - fd = socket(addr_it->ai_family, addr_it->ai_socktype, addr_it->ai_protocol); - if(fd < 0){ - continue; - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_REUSEADDR on socket\n"); - } - - yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to set SO_BROADCAST on socket\n"); - } - - yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - fprintf(stderr, "Failed to unset IP_MULTICAST_LOOP option: %s\n", strerror(errno)); - } - - status = bind(fd, addr_it->ai_addr, addr_it->ai_addrlen); - if(status < 0){ - close(fd); - continue; - } - - break; - } - - freeaddrinfo(info); - - if(!addr_it){ - fprintf(stderr, "Failed to create listening socket for %s port %s\n", host, port); - return -1; - } - - //set nonblocking - flags = fcntl(fd, F_GETFL, 0); - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - fprintf(stderr, "Failed to set rtpMIDI descriptor nonblocking\n"); - close(fd); - return -1; - } - return 0; -} - -static int rtpmidi_parse_addr(char* host, char* port, struct sockaddr_storage* addr, socklen_t* len){ - struct addrinfo* head; - struct addrinfo hints = { - .ai_family = AF_UNSPEC, - .ai_socktype = SOCK_DGRAM - }; - - int error = getaddrinfo(host, port, &hints, &head); - if(error || !head){ - fprintf(stderr, "Failed to parse address %s port %s: %s\n", host, port, gai_strerror(error)); - return 1; - } - - memcpy(addr, head->ai_addr, head->ai_addrlen); - *len = head->ai_addrlen; - - freeaddrinfo(head); - return 0; -} - -static int rtpmidi_separate_hostspec(char* in, char** host, char** port, char* default_port){ - size_t u; - - if(!in || !host || !port){ + fprintf(stderr, "Failed to register rtpmidi backend\n"); return 1; } - for(u = 0; in[u] && !isspace(in[u]); u++){ - } - - //guess - *host = in; - - if(in[u]){ - in[u] = 0; - *port = in + u + 1; - } - else{ - //no port given - *port = default_port; - } return 0; } static int rtpmidi_configure(char* option, char* value){ + char* host = NULL, *port = NULL; + char next_port[10]; + if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ fprintf(stderr, "Duplicate mdns-name assignment\n"); @@ -191,17 +65,27 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } + if(mmbackend_parse_hostspec(value, &host, &port)){ + fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); + return 1; + } - //TODO create mdns broadcast/responder socket - } - else if(!strcmp(option, "bind")){ - //TODO open listening data fd for raw rtpmidi instance + cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); + return 1; + } + return 0; } - else if(!strcmp(option, "apple-bind")){ - //TODO open control and data fd for applemidi session/rtpmidi+apple-extensions instance + else if(!strcmp(option, "detect")){ + cfg.detect = 0; + if(!strcmp(value, "on")){ + cfg.detect = 1; + } + return 0; } - fprintf(stderr, "Unknown rtpMIDI backend option %s\n", option); + fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); return 1; } @@ -215,7 +99,7 @@ static instance* rtpmidi_instance(){ return NULL; } -static channel* rtpmidi_channel(instance* inst, char* spec){ +static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ //TODO return NULL; } @@ -242,6 +126,8 @@ static int rtpmidi_start(){ instance** inst = NULL; rtpmidi_instance_data* data = NULL; + //TODO if mdns name defined and no socket, bind default values + //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f14357f..6985ede 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -5,7 +5,7 @@ int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static instance* rtpmidi_instance(); -static channel* rtpmidi_channel(instance* instance, char* spec); +static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(); @@ -13,11 +13,7 @@ static int rtpmidi_shutdown(); #define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 - -#define RTPMIDI_COMMAND_LEN -#define RTPMIDI_COMMAND_DATA_OFFSET -#define RTPMIDI_HAS_JOURNAL -#define RTPMIDI_IS_PHANTOM +#define RTPMIDI_MDNS_PORT "5353" typedef enum /*_rtpmidi_peer_mode*/ { peer_learned, @@ -40,7 +36,7 @@ typedef struct /*_rtpmidi_fd*/ { } rtpmidi_fd; typedef struct /*_rtmidi_instance_data*/ { - size_t fd_index; + int fd; size_t npeers; rtpmidi_peer* peers; uint32_t ssrc; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md new file mode 100644 index 0000000..49c6baf --- /dev/null +++ b/backends/rtpmidi.md @@ -0,0 +1,85 @@ +### The `rtpmidi` backend + +This backend provides read-write access to RTP MIDI streams, which transfer MIDI data +over the network. + +As the specification for RTP MIDI does not normatively indicate any method +for session management, most vendors define their own standards for this. +The MIDIMonster supports the following session management methods, which are +selectable per-instance, with some methods requiring additional global configuration: + +* Direct connection: The instance will send and receive data from peers configured in the + instance configuration +* Direct connection with peer learning: The instance will send and receive data from peers + configured in the instance configuration as well as previously unknown peers that + voluntarily send data to the instance. +* AppleMIDI session management: + +Note that instances that receive data from multiple peers will combine all inputs into one +stream, which may lead to inconsistencies during playback. + +#### Global configuration + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | +| `mdns-bind` | `10.1.2.1 5353` | `0.0.0.0 5353` | Bind host for the mDNS discovery server | +| `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | + +#### Instance configuration + +Common instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | +| `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | + +`direct` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to | +| `learn` | `true` | `false` | Accept new peers for data exchange at runtime | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | + +`apple` mode instance configuration parameters + +| Option | Example value | Default value | Description | +|---------------|-----------------------|-----------------------|-----------------------| +| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | +| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers) | + +Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. + +#### Channel specification + +The `rtpmidi` backend supports mapping different MIDI events to MIDIMonster channels. The currently supported event types are + +* `cc` - Control Changes +* `note` - Note On/Off messages +* `pressure` - Note pressure/aftertouch messages +* `aftertouch` - Channel-wide aftertouch messages +* `pitch` - Channel pitchbend messages + +A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be +used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). + +The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. + +MIDI channels range from `0` to `15`. Each MIDI channel consists of 128 notes (numbered `0` through `127`), which +additionally each have a pressure control, 128 CC's (numbered likewise), a channel pressure control (also called +'channel aftertouch') and a pitch control which may all be mapped to individual MIDIMonster channels. + +Example mappings: + +``` +rmidi1.ch0.note9 > rmidi2.channel1.cc4 +rmidi1.channel15.pressure1 > rmidi1.channel0.note0 +rmidi1.ch1.aftertouch > rmidi2.ch2.cc0 +rmidi1.ch0.pitch > rmidi2.ch1.pitch +``` + +#### Known bugs / problems -- cgit v1.2.3 From 99d31eabf4a3afa4fd54782cc1d7cd92fbdae084 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 02:27:04 +0100 Subject: Cleanup rtmidi data structures --- backends/rtpmidi.c | 13 ++++--------- backends/rtpmidi.h | 28 ++++++++++++---------------- backends/rtpmidi.md | 5 +++-- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 5387d86..ea97565 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -7,6 +7,7 @@ #include #include +#include "libmmbackend.h" #include "rtpmidi.h" #define BACKEND_NAME "rtpmidi" @@ -65,7 +66,9 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } - if(mmbackend_parse_hostspec(value, &host, &port)){ + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); return 1; } @@ -164,13 +167,5 @@ static int rtpmidi_shutdown(){ close(cfg.mdns_fd); } - for(u = 0; u < cfg.nfds; u++){ - if(cfg.fds[u].data >= 0){ - close(cfg.fds[u].data); - } - if(cfg.fds[u].control >= 0){ - close(cfg.fds[u].control); - } - } return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 6985ede..af6a189 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -14,31 +14,27 @@ static int rtpmidi_shutdown(); #define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 #define RTPMIDI_MDNS_PORT "5353" +#define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) -typedef enum /*_rtpmidi_peer_mode*/ { - peer_learned, - peer_invited, - peer_invited_by, - peer_sync, - peer_connect -} rtpmidi_peer_mode; +typedef enum /*_rtpmidi_instance_mode*/ { + direct, + apple +} rtpmidi_instance_mode; typedef struct /*_rtpmidi_peer*/ { - rtpmidi_peer_mode mode; struct sockaddr_storage dest; socklen_t dest_len; uint32_t ssrc; } rtpmidi_peer; -typedef struct /*_rtpmidi_fd*/ { - int data; - int control; -} rtpmidi_fd; - typedef struct /*_rtmidi_instance_data*/ { + rtpmidi_instance_mode mode; + int fd; - size_t npeers; - rtpmidi_peer* peers; + int control_fd; + + size_t peers; + rtpmidi_peer* peer; uint32_t ssrc; //apple-midi config @@ -46,7 +42,7 @@ typedef struct /*_rtmidi_instance_data*/ { char* invite_peers; char* invite_accept; - //generic mode config + //direct mode config uint8_t learn_peers; } rtpmidi_instance_data; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 49c6baf..cea69e2 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -49,10 +49,11 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers) | +| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | +| `joint` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter -(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. +(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. #### Channel specification -- cgit v1.2.3 From aa092469e21674bb99275dda08a695f0d426f6de Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 8 Dec 2019 15:43:05 +0100 Subject: rtpmidi channel spec parsing --- backends/rtpmidi.c | 199 ++++++++++++++++++++++++++++++++++++++++++++++------ backends/rtpmidi.h | 20 ++++++ backends/rtpmidi.md | 2 +- 3 files changed, 200 insertions(+), 21 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index ea97565..4fb0ce0 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -35,6 +35,11 @@ int init(){ .shutdown = rtpmidi_shutdown }; + if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ + fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); + return 1; + } + if(mm_backend_register(rtpmidi)){ fprintf(stderr, "Failed to register rtpmidi backend\n"); return 1; @@ -45,7 +50,6 @@ int init(){ static int rtpmidi_configure(char* option, char* value){ char* host = NULL, *port = NULL; - char next_port[10]; if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ @@ -93,17 +97,170 @@ static int rtpmidi_configure(char* option, char* value){ } static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ - //TODO + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + + if(!strcmp(option, "mode")){ + if(!strcmp(value, "direct")){ + data->mode = direct; + return 0; + } + else if(!strcmp(value, "apple")){ + data->mode = apple; + return 0; + } + fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); + return 1; + } + else if(!strcmp(option, "ssrc")){ + data->ssrc = strtoul(value, NULL, 0); + if(!data->ssrc){ + fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); + } + return 0; + } + else if(!strcmp(option, "bind")){ + //TODO set the bind host + } + else if(!strcmp(option, "learn")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); + return 1; + } + data->learn_peers = 0; + if(!strcmp(value, "true")){ + data->learn_peers = 1; + } + return 0; + } + else if(!strcmp(option, "peer")){ + if(data->mode != direct){ + fprintf(stderr, "The rtpmidi 'peer' option is only valid for direct mode instances\n"); + return 1; + } + + //TODO add peer + return 0; + } + else if(!strcmp(option, "session")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); + return 1; + } + free(data->session_name); + data->session_name = strdup(value); + if(!data->session_name){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "invite")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_peers); + data->invite_peers = strdup(value); + if(!data->invite_peers){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + else if(!strcmp(option, "join")){ + if(data->mode != apple){ + fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); + return 1; + } + free(data->invite_accept); + data->invite_accept = strdup(value); + if(!data->invite_accept){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + return 0; + } + + fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); return 1; } static instance* rtpmidi_instance(){ - //TODO - return NULL; + rtpmidi_instance_data* data = NULL; + instance* inst = mm_instance(); + + if(!inst){ + return NULL; + } + + data = calloc(1, sizeof(rtpmidi_instance_data)); + if(!data){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + + inst->impl = data; + return inst; } static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ - //TODO + char* next_token = spec; + rtpmidi_channel_ident ident = { + .label = 0 + }; + + if(!strncmp(spec, "ch", 2)){ + next_token += 2; + if(!strncmp(spec, "channel", 7)){ + next_token = spec + 7; + } + } + else{ + fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); + return NULL; + } + + ident.fields.channel = strtoul(next_token, &next_token, 10); + if(ident.fields.channel > 15){ + fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); + return NULL; + } + + if(*next_token != '.'){ + fprintf(stderr, "rtpmidi channel specification %s does not conform to channel.\n", spec); + return NULL; + } + + next_token++; + + if(!strncmp(next_token, "cc", 2)){ + ident.fields.type = cc; + next_token += 2; + } + else if(!strncmp(next_token, "note", 4)){ + ident.fields.type = note; + next_token += 4; + } + else if(!strncmp(next_token, "pressure", 8)){ + ident.fields.type = pressure; + next_token += 8; + } + else if(!strncmp(next_token, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(next_token, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); + return NULL; + } + + ident.fields.control = strtoul(next_token, NULL, 10); + + if(ident.label){ + return mm_channel(inst, ident.label, 1); + } return NULL; } @@ -124,35 +281,37 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(){ - size_t n, u, p; + size_t n, u; int rv = 1; instance** inst = NULL; rtpmidi_instance_data* data = NULL; //TODO if mdns name defined and no socket, bind default values + if(cfg.mdns_fd < 0){ + fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session support disabled\n"); + } + //fetch all defined instances if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ fprintf(stderr, "Failed to fetch instance list\n"); return 1; } - if(!n){ - free(inst); - return 0; - } - - if(!cfg.nfds){ - fprintf(stderr, "Failed to start rtpMIDI backend: no descriptors bound\n"); - goto bail; - } + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + //check whether instances are explicitly configured to a mode + if(data->mode == unconfigured){ + fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); + goto bail; + } - if(cfg.mdns_fd < 0){ - fprintf(stderr, "No mDNS discovery interface bound, APPLEMIDI session support disabled\n"); + //generate random ssrc's + if(!data->ssrc){ + data->ssrc = rand() << 16 | rand(); + } } - //TODO initialize all instances - rv = 0; bail: free(inst); @@ -160,7 +319,7 @@ bail: } static int rtpmidi_shutdown(){ - size_t u; + //TODO cleanup instance data free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index af6a189..c076fd0 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,11 +16,31 @@ static int rtpmidi_shutdown(); #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) +enum /*_rtpmidi_channel_type*/ { + none = 0, + note = 0x90, + cc = 0xB0, + pressure = 0xA0, + aftertouch = 0xD0, + pitchbend = 0xE0 +}; + typedef enum /*_rtpmidi_instance_mode*/ { + unconfigured = 0, direct, apple } rtpmidi_instance_mode; +typedef union { + struct { + uint8_t pad[5]; + uint8_t type; + uint8_t channel; + uint8_t control; + } fields; + uint64_t label; +} rtpmidi_channel_ident; + typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index cea69e2..c84c5b3 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -50,7 +50,7 @@ Common instance configuration parameters | `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | -| `joint` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From 48e12201f5c57cda581bc0c713d99da6524c49a8 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 9 Dec 2019 23:14:23 +0100 Subject: rtpmidi socket binding --- backends/rtpmidi.c | 87 ++++++++++++++++++++++++++++++++++++++++++++++++----- backends/rtpmidi.h | 2 ++ backends/rtpmidi.md | 8 ++--- 3 files changed, 85 insertions(+), 12 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 4fb0ce0..38cc9c1 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,6 +1,3 @@ -#include -#include -#include #include #include #include @@ -96,8 +93,40 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } +static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ + struct sockaddr_storage sock_addr = { + 0 + }; + socklen_t sock_len = sizeof(sock_addr); + char control_port[32]; + + //bind to random port if none supplied + data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); + if(data->fd < 0){ + return 1; + } + + //bind control port + if(data->mode == apple){ + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); + return 1; + } + + snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); + data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); + if(data->control_fd < 0){ + fprintf(stderr, "Failed to bind control port %s\n", control_port); + return 1; + } + } + + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + char* host = NULL, *port = NULL; if(!strcmp(option, "mode")){ if(!strcmp(value, "direct")){ @@ -119,7 +148,19 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "bind")){ - //TODO set the bind host + if(data->mode == unconfigured){ + fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); + return 1; + } + + mmbackend_parse_hostspec(value, &host, &port); + + if(!host){ + fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); + return 1; + } + + return rtpmidi_bind_instance(data, host, port); } else if(!strcmp(option, "learn")){ if(data->mode != direct){ @@ -198,6 +239,8 @@ static instance* rtpmidi_instance(){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } + data->fd = -1; + data->control_fd = -1; inst->impl = data; return inst; @@ -281,15 +324,29 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(){ - size_t n, u; + size_t n, u, fds = 0; int rv = 1; instance** inst = NULL; rtpmidi_instance_data* data = NULL; - //TODO if mdns name defined and no socket, bind default values + //if mdns name defined and no socket, bind default values + if(cfg.mdns_name && cfg.mdns_fd < 0){ + cfg.mdns_fd = mmbackend_socket("::", RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + return 1; + } + } - if(cfg.mdns_fd < 0){ - fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session support disabled\n"); + //register mdns fd to core + if(cfg.mdns_fd >= 0){ + if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ + fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); + goto bail; + } + fds++; + } + else{ + fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); } //fetch all defined instances @@ -310,8 +367,22 @@ static int rtpmidi_start(){ if(!data->ssrc){ data->ssrc = rand() << 16 | rand(); } + + //if not bound, bind to default + if(data->fd < 0 && rtpmidi_bind_instance(data, "::", NULL)){ + fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); + goto bail; + } + + //register fds to core + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, NULL) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, NULL))){ + fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); + goto bail; + } + fds += (data->control_fd >= 0) ? 2 : 1; } + fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); rv = 0; bail: free(inst); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index c076fd0..6cab225 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -1,4 +1,6 @@ +#ifndef _WIN32 #include +#endif #include "midimonster.h" int init(); diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c84c5b3..c208bf7 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -23,7 +23,7 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-bind` | `10.1.2.1 5353` | `0.0.0.0 5353` | Bind host for the mDNS discovery server | +| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | | `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | #### Instance configuration @@ -39,7 +39,7 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to | +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to | | `learn` | `true` | `false` | Accept new peers for data exchange at runtime | | `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | @@ -47,12 +47,12 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `0.0.0.0 ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | | `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | -Note that AppleMIDI session establishment requires mDNS functionality, thus the `mdns-name` global parameter +Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. #### Channel specification -- cgit v1.2.3 From 1bde6249ab6b44764ff2d7ef79e2f22ddf9a7082 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Dec 2019 23:30:46 +0100 Subject: rtpmidi shutdown procedure --- backends/rtpmidi.c | 44 ++++++++++++++++++++++++++++++++------------ backends/rtpmidi.h | 5 ++--- 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 38cc9c1..77819ee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -323,10 +323,9 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ return 1; } -static int rtpmidi_start(){ - size_t n, u, fds = 0; +static int rtpmidi_start(size_t n, instance** inst){ + size_t u, fds = 0; int rv = 1; - instance** inst = NULL; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values @@ -349,12 +348,6 @@ static int rtpmidi_start(){ fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); } - //fetch all defined instances - if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list\n"); - return 1; - } - for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode @@ -385,12 +378,39 @@ static int rtpmidi_start(){ fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); rv = 0; bail: - free(inst); return rv; } -static int rtpmidi_shutdown(){ - //TODO cleanup instance data +static int rtpmidi_shutdown(size_t n, instance** inst){ + rtpmidi_instance_data* data = NULL; + size_t u; + + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + if(data->fd >= 0){ + close(data->fd); + } + + if(data->control_fd >= 0){ + close(data->control_fd); + } + + free(data->session_name); + data->session_name = NULL; + + free(data->invite_peers); + data->invite_peers = NULL; + + free(data->invite_accept); + data->invite_accept = NULL; + + free(data->peer); + data->peer = NULL; + data->peers = 0; + + free(inst[u]->impl); + inst[u]->impl = NULL; + } free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 6cab225..78675bc 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -10,10 +10,9 @@ static instance* rtpmidi_instance(); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); -static int rtpmidi_start(); -static int rtpmidi_shutdown(); +static int rtpmidi_start(size_t n, instance** inst); +static int rtpmidi_shutdown(size_t n, instance** inst); -#define RTPMIDI_DEFAULT_PORTBASE "9001" #define RTPMIDI_RECV_BUF 4096 #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) -- cgit v1.2.3 From 60adf2c4fe53e935e6de359ef1c01d0a91ab7480 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Dec 2019 23:49:11 +0100 Subject: Implement rtpmidi peer handling --- backends/rtpmidi.c | 76 +++++++++++++++++++++++++++++++++++++++-------------- backends/rtpmidi.h | 3 ++- backends/rtpmidi.md | 1 + 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 77819ee..612ac6f 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,7 +19,7 @@ static struct /*_rtpmidi_global*/ { .detect = 0 }; -int init(){ +MM_PLUGIN_API int init(){ backend rtpmidi = { .name = BACKEND_NAME, .conf = rtpmidi_configure, @@ -124,9 +124,34 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 0; } +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ + size_t u; + + for(u = 0; u < data->peers; u++){ + //check whether the peer is already in the list + if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + return 0; + } + } + + data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); + if(!data->peer){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + data->peer[data->peers].dest = sock_addr; + data->peer[data->peers].dest_len = sock_len; + + data->peers++; + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); if(!strcmp(option, "mode")){ if(!strcmp(value, "direct")){ @@ -174,13 +199,18 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ - if(data->mode != direct){ - fprintf(stderr, "The rtpmidi 'peer' option is only valid for direct mode instances\n"); + mmbackend_parse_hostspec(value, &host, &port); + if(!host || !port){ + fprintf(stderr, "Invalid peer %s configured on rtpmidi instance %s\n", value, inst->name); return 1; } - //TODO add peer - return 0; + if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ + fprintf(stderr, "Failed to resolve peer %s configured on rtpmidi instance %s\n", value, inst->name); + return 1; + } + + return rtpmidi_push_peer(data, sock_addr, sock_len); } else if(!strcmp(option, "session")){ if(data->mode != apple){ @@ -313,24 +343,34 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v } static int rtpmidi_handle(size_t num, managed_fd* fds){ - //TODO handle discovery + size_t u; + int rv = 0; + + //TODO handle mDNS discovery frames if(!num){ return 0; } - //TODO - return 1; + for(u = 0; u < num; u++){ + if(!fds[u].impl){ + //TODO handle mDNS discovery input + } + else{ + //TODO handle rtp/control input + } + } + + return rv; } static int rtpmidi_start(size_t n, instance** inst){ size_t u, fds = 0; - int rv = 1; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values if(cfg.mdns_name && cfg.mdns_fd < 0){ - cfg.mdns_fd = mmbackend_socket("::", RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); if(cfg.mdns_fd < 0){ return 1; } @@ -340,7 +380,7 @@ static int rtpmidi_start(size_t n, instance** inst){ if(cfg.mdns_fd >= 0){ if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); - goto bail; + return 1; } fds++; } @@ -353,7 +393,7 @@ static int rtpmidi_start(size_t n, instance** inst){ //check whether instances are explicitly configured to a mode if(data->mode == unconfigured){ fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); - goto bail; + return 1; } //generate random ssrc's @@ -362,23 +402,21 @@ static int rtpmidi_start(size_t n, instance** inst){ } //if not bound, bind to default - if(data->fd < 0 && rtpmidi_bind_instance(data, "::", NULL)){ + if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); - goto bail; + return 1; } //register fds to core - if(mm_manage_fd(data->fd, BACKEND_NAME, 1, NULL) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, NULL))){ + if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); - goto bail; + return 1; } fds += (data->control_fd >= 0) ? 2 : 1; } fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); - rv = 0; -bail: - return rv; + return 0; } static int rtpmidi_shutdown(size_t n, instance** inst){ diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 78675bc..ddb7bed 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -3,7 +3,7 @@ #endif #include "midimonster.h" -int init(); +MM_PLUGIN_API int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static instance* rtpmidi_instance(); @@ -14,6 +14,7 @@ static int rtpmidi_start(size_t n, instance** inst); static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_RECV_BUF 4096 +#define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c208bf7..d8e3b63 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -51,6 +51,7 @@ Common instance configuration parameters | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | | `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From d17023d77e90a8665965da50685dcf4a30f3eb37 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 13 Dec 2019 22:43:22 +0100 Subject: Basic rtpmidi output --- backends/rtpmidi.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/rtpmidi.h | 7 ++-- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 612ac6f..40e2cb2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -338,13 +338,108 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ } static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v){ - //TODO - return 1; + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + rtpmidi_header* rtp_header = (rtpmidi_header*) frame; + rtpmidi_command_header* command_header = (rtpmidi_command_header*) (frame + sizeof(rtpmidi_header)); + size_t offset = sizeof(rtpmidi_header) + sizeof(rtpmidi_command_header), u = 0; + uint8_t* payload = frame + offset; + rtpmidi_channel_ident ident; + + rtp_header->vpxccmpt = RTPMIDI_HEADER_MAGIC; + rtp_header->sequence = htobe16(data->sequence++); + rtp_header->timestamp = 0; //TODO calculate appropriate timestamps + rtp_header->ssrc = htobe32(data->ssrc); + + //midi command section header + //TODO enable the journal bit here + command_header->flags = 0xA0; //extended length header, first entry in list has dtime + + //midi list + for(u = 0; u < num; u++){ + ident.label = c[u]->ident; + + //encode timestamp + payload[0] = 0; + + //encode midi command + payload[1] = ident.fields.type | ident.fields.channel; + payload[2] = ident.fields.control; + payload[3] = v[u].normalised * 127.0; + + if(ident.fields.type == pitchbend){ + payload[2] = ((int)(v[u].normalised * 16384.0)) & 0x7F; + payload[3] = (((int)(v[u].normalised * 16384.0)) >> 7) & 0x7F; + } + //channel-wide aftertouch is only 2 bytes + else if(ident.fields.type == aftertouch){ + payload[2] = payload[3]; + payload -= 1; + offset -= 1; + } + + payload += 4; + offset += 4; + } + + //update command section length + //FIXME this might overrun, might check the number of events at some point + command_header->flags |= (((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0x0F00) >> 8); + command_header->length = ((offset - sizeof(rtpmidi_header) - sizeof(rtpmidi_command_header)) & 0xFF); + + //TODO journal section + + for(u = 0; u < data->peers; u++){ + sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + } + + return 0; +} + +static int rtpmidi_handle_data(instance* inst){ + size_t u; + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + rtpmidi_header* rtp_header = (rtpmidi_header*) frame; + ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + + //TODO receive until EAGAIN + //FIXME might want to filter data input from sources that are not registered peers + if(rtp_header->vpxccmpt != RTPMIDI_HEADER_MAGIC){ + fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); + return 0; + } + + //try to learn peers + if(data->learn_peers){ + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == sock_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + break; + } + } + + if(u == data->peers){ + fprintf(stderr, "rtpmidi instance %s learned new peer\n", inst->name); + return rtpmidi_push_peer(data, sock_addr, sock_len); + } + } + return 0; +} + +static int rtpmidi_handle_control(instance* inst){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + + return 0; } static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; + instance* inst = NULL; + rtpmidi_instance_data* data = NULL; //TODO handle mDNS discovery frames @@ -357,7 +452,18 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //TODO handle mDNS discovery input } else{ - //TODO handle rtp/control input + //handle rtp/control input + inst = (instance*) fds[u].impl; + data = (rtpmidi_instance_data*) inst->impl; + if(fds[u].fd == data->fd){ + rv |= rtpmidi_handle_data(inst); + } + else if(fds[u].fd == data->control_fd){ + rv |= rtpmidi_handle_control(inst); + } + else{ + fprintf(stderr, "rtpmidi signaled descriptor not recognized\n"); + } } } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index ddb7bed..b56000a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -13,7 +13,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(size_t n, instance** inst); static int rtpmidi_shutdown(size_t n, instance** inst); -#define RTPMIDI_RECV_BUF 4096 +#define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) @@ -58,6 +58,7 @@ typedef struct /*_rtmidi_instance_data*/ { size_t peers; rtpmidi_peer* peer; uint32_t ssrc; + uint16_t sequence; //apple-midi config char* session_name; @@ -103,6 +104,6 @@ typedef struct /*_rtp_midi_header*/ { typedef struct /*_rtp_midi_command*/ { uint8_t flags; - uint8_t additional_length; -} rtpmidi_command; + uint8_t length; +} rtpmidi_command_header; #pragma pack(pop) -- cgit v1.2.3 From 81c6af48f9b63171d1f407f8203f742cd64c3688 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Dec 2019 22:30:06 +0100 Subject: Initial AppleMIDI session support --- backends/rtpmidi.c | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- backends/rtpmidi.h | 14 ++++++++-- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 40e2cb2..e78b1f2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -346,7 +346,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v uint8_t* payload = frame + offset; rtpmidi_channel_ident ident; - rtp_header->vpxccmpt = RTPMIDI_HEADER_MAGIC; + rtp_header->vpxcc = RTPMIDI_HEADER_MAGIC; + //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly + rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; rtp_header->sequence = htobe16(data->sequence++); rtp_header->timestamp = 0; //TODO calculate appropriate timestamps rtp_header->ssrc = htobe32(data->ssrc); @@ -396,6 +398,44 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* data, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + apple_command* command = (apple_command*) data; + size_t u; + + //find peer if already in list + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == peer_len + && !memcmp(&data->peer[u].dest, peer, peer_len)){ + break; + } + } + + if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ + //TODO check whether the session is in the accept list + } + else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ + //TODO mark peer as in-session, start timesync + } + else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ + //TODO mark peer as rejected (or retry invitation) + } + else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ + //TODO mark peer as disconnected, retry invitation + } + else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ + //TODO respond with sync answer + } + else if(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ + //ignore + } + else{ + fprintf(stderr, "Unknown AppleMIDI session command %02X %02X\n", command->command[0], command->command[1]); + } + + return 0; +} + static int rtpmidi_handle_data(instance* inst){ size_t u; rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; @@ -406,12 +446,27 @@ static int rtpmidi_handle_data(instance* inst){ ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); //TODO receive until EAGAIN + if(bytes_recv < 0){ + fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + return 1; + } + + if(bytes_recv < sizeof(rtpmidi_header)){ + fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + return 0; + } + //FIXME might want to filter data input from sources that are not registered peers - if(rtp_header->vpxccmpt != RTPMIDI_HEADER_MAGIC){ + if(data->mode == apple && rtp_header->vpxcc == 0xFF && rtp_header->mpt == 0xFF){ + return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); + } + else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); return 0; } + //TODO parse data + //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ @@ -431,7 +486,27 @@ static int rtpmidi_handle_data(instance* inst){ static int rtpmidi_handle_control(instance* inst){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage sock_addr; + socklen_t sock_len = sizeof(sock_addr); + ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + + if(bytes_recv < 0){ + fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + return 1; + } + + //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar + if(bytes_recv < sizeof(rtpmidi_header)){ + fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + return 0; + } + + if(data->mode == apple && frame[0] == 0xFF && frame[1] == 0xFF){ + return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len); + } + fprintf(stderr, "Unknown session protocol frame received on rtpmidi instance %s\n", inst->name); return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index b56000a..d4ca044 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,7 +16,16 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" #define RTPMIDI_MDNS_PORT "5353" -#define RTPMIDI_HEADER_MAGIC htobe16(0x80E1) +#define RTPMIDI_HEADER_MAGIC 0x80 +#define RTPMIDI_HEADER_TYPE 0x61 +#define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) + +#define APPLEMIDI_INVITE "IN" +#define APPLEMIDI_ACCEPT "OK" +#define APPLEMIDI_REJECT "NO" +#define APPLEMIDI_LEAVE "BY" +#define APPLEMIDI_SYNC "CK" +#define APPLEMIDI_FEEDBACK "RS" enum /*_rtpmidi_channel_type*/ { none = 0, @@ -96,7 +105,8 @@ typedef struct /*_apple_session_feedback*/ { } apple_feedback; typedef struct /*_rtp_midi_header*/ { - uint16_t vpxccmpt; //this is really just an amalgamated constant value + uint8_t vpxcc; + uint8_t mpt; uint16_t sequence; uint32_t timestamp; uint32_t ssrc; -- cgit v1.2.3 From 86ca5c25b1c5f71c99edbd59f179c4e5f88e16e5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Dec 2019 22:40:06 +0100 Subject: Fix overloading --- backends/rtpmidi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index e78b1f2..780e517 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -398,9 +398,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v return 0; } -static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* data, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ +static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; - apple_command* command = (apple_command*) data; + apple_command* command = (apple_command*) frame; size_t u; //find peer if already in list -- cgit v1.2.3 From b5d5f26835ea8840fc3aedd38780f3025d2959b3 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 16 Dec 2019 22:47:52 +0100 Subject: Move active invitations to global scope --- backends/rtpmidi.c | 85 ++++++++++++++++++++++++++++++++++++++++++----------- backends/rtpmidi.h | 9 ++++-- backends/rtpmidi.md | 6 ++-- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 780e517..719f823 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -13,10 +13,15 @@ static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; uint8_t detect; + + size_t announces; + rtpmidi_announce* announce; } cfg = { .mdns_fd = -1, .mdns_name = NULL, - .detect = 0 + .detect = 0, + .announces = 0, + .announce = NULL }; MM_PLUGIN_API int init(){ @@ -137,6 +142,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); if(!data->peer){ fprintf(stderr, "Failed to allocate memory\n"); + data->peers = 0; return 1; } @@ -147,6 +153,58 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag return 0; } +static int rtpmidi_push_invite(instance* inst, char* peer){ + size_t u, p; + + //check whether the instance is already in the announce list + for(u = 0; u < cfg.announces; u++){ + if(cfg.announce[u].inst == inst){ + break; + } + } + + //add to the announce list + if(u == cfg.announces){ + cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); + if(!cfg.announce){ + fprintf(stderr, "Failed to allocate memory\n"); + cfg.announces = 0; + return 1; + } + + cfg.announce[u].inst = inst; + cfg.announce[u].invites = 0; + cfg.announce[u].invite = NULL; + + cfg.announces++; + } + + //check whether the peer is already in the invite list + for(p = 0; p < cfg.announce[u].invites; p++){ + if(!strcmp(cfg.announce[u].invite[p], peer)){ + return 0; + } + } + + //extend the invite list + cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); + if(!cfg.announce[u].invite){ + fprintf(stderr, "Failed to allocate memory\n"); + cfg.announce[u].invites = 0; + return 1; + } + + //append the new invitee + cfg.announce[u].invite[p] = strdup(peer); + if(!cfg.announce[u].invite[p]){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + cfg.announce[u].invites++; + return 0; +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; @@ -230,22 +288,17 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); return 1; } - free(data->invite_peers); - data->invite_peers = strdup(value); - if(!data->invite_peers){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } - return 0; + + return rtpmidi_push_invite(inst, value); } else if(!strcmp(option, "join")){ if(data->mode != apple){ fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); return 1; } - free(data->invite_accept); - data->invite_accept = strdup(value); - if(!data->invite_accept){ + free(data->accept); + data->accept = strdup(value); + if(!data->accept){ fprintf(stderr, "Failed to allocate memory\n"); return 1; } @@ -350,7 +403,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //some receivers seem to have problems reading rfcs and interpreting the marker bit correctly rtp_header->mpt = (data->mode == apple ? 0 : 0x80) | RTPMIDI_HEADER_TYPE; rtp_header->sequence = htobe16(data->sequence++); - rtp_header->timestamp = 0; //TODO calculate appropriate timestamps + rtp_header->timestamp = mm_timestamp() * 10; //just assume 100msec resolution because rfc4695 handwaves it rtp_header->ssrc = htobe32(data->ssrc); //midi command section header @@ -617,11 +670,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ free(data->session_name); data->session_name = NULL; - free(data->invite_peers); - data->invite_peers = NULL; - - free(data->invite_accept); - data->invite_accept = NULL; + free(data->accept); + data->accept = NULL; free(data->peer); data->peer = NULL; @@ -631,6 +681,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } + //TODO free announces free(cfg.mdns_name); if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index d4ca044..2652db7 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -71,13 +71,18 @@ typedef struct /*_rtmidi_instance_data*/ { //apple-midi config char* session_name; - char* invite_peers; - char* invite_accept; + char* accept; //direct mode config uint8_t learn_peers; } rtpmidi_instance_data; +typedef struct /*rtpmidi_announced_instance*/ { + instance* inst; + size_t invites; + char** invite; +} rtpmidi_announce; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d8e3b63..d42df6f 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -34,6 +34,7 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | | `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery protocols | `direct` mode instance configuration parameters @@ -41,7 +42,6 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to | | `learn` | `true` | `false` | Accept new peers for data exchange at runtime | -| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times | `apple` mode instance configuration parameters @@ -49,8 +49,8 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad,piano` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator | -| `join` | `Just Jamming` | none | Sessions for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | | `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -- cgit v1.2.3 From 2ba935f9a435624d57bfc57cf6262898987c4a08 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 22 Dec 2019 13:52:37 +0100 Subject: Update rtpmidi to conform to master changes --- backends/rtpmidi.c | 105 +++++++++++++++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 52 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 719f823..e842a84 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,3 +1,5 @@ +#define BACKEND_NAME "rtpmidi" + #include #include #include @@ -7,8 +9,6 @@ #include "libmmbackend.h" #include "rtpmidi.h" -#define BACKEND_NAME "rtpmidi" - static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -38,12 +38,12 @@ MM_PLUGIN_API int init(){ }; if(sizeof(rtpmidi_channel_ident) != sizeof(uint64_t)){ - fprintf(stderr, "rtpmidi channel identification union out of bounds\n"); + LOG("Channel identification union out of bounds"); return 1; } if(mm_backend_register(rtpmidi)){ - fprintf(stderr, "Failed to register rtpmidi backend\n"); + LOG("Failed to register backend"); return 1; } @@ -55,33 +55,33 @@ static int rtpmidi_configure(char* option, char* value){ if(!strcmp(option, "mdns-name")){ if(cfg.mdns_name){ - fprintf(stderr, "Duplicate mdns-name assignment\n"); + LOG("Duplicate mdns-name assignment"); return 1; } cfg.mdns_name = strdup(value); if(!cfg.mdns_name){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } else if(!strcmp(option, "mdns-bind")){ if(cfg.mdns_fd >= 0){ - fprintf(stderr, "Only one mDNS discovery bind is supported\n"); + LOG( "Only one mDNS discovery bind is supported"); return 1; } - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host){ - fprintf(stderr, "Not a valid mDNS bind address: %s\n", value); + LOGPF("Not a valid mDNS bind address: %s", value); return 1; } cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); if(cfg.mdns_fd < 0){ - fprintf(stderr, "Failed to bind mDNS interface: %s\n", value); + LOGPF("Failed to bind mDNS interface: %s", value); return 1; } return 0; @@ -94,7 +94,7 @@ static int rtpmidi_configure(char* option, char* value){ return 0; } - fprintf(stderr, "Unknown rtpmidi backend option %s\n", option); + LOGPF("Unknown backend configuration option %s", option); return 1; } @@ -114,14 +114,14 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* //bind control port if(data->mode == apple){ if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - fprintf(stderr, "Failed to fetch data port information: %s\n", strerror(errno)); + LOGPF("Failed to fetch data port information: %s", strerror(errno)); return 1; } snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); if(data->control_fd < 0){ - fprintf(stderr, "Failed to bind control port %s\n", control_port); + LOGPF("Failed to bind control port %s", control_port); return 1; } } @@ -141,7 +141,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); if(!data->peer){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); data->peers = 0; return 1; } @@ -167,7 +167,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ if(u == cfg.announces){ cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); if(!cfg.announce){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); cfg.announces = 0; return 1; } @@ -189,7 +189,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ //extend the invite list cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); if(!cfg.announce[u].invite){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); cfg.announce[u].invites = 0; return 1; } @@ -197,7 +197,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ //append the new invitee cfg.announce[u].invite[p] = strdup(peer); if(!cfg.announce[u].invite[p]){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } @@ -220,26 +220,26 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) data->mode = apple; return 0; } - fprintf(stderr, "Unknown rtpmidi instance mode %s for instance %s\n", value, inst->name); + LOGPF("Unknown instance mode %s for instance %s", value, inst->name); return 1; } else if(!strcmp(option, "ssrc")){ data->ssrc = strtoul(value, NULL, 0); if(!data->ssrc){ - fprintf(stderr, "Random SSRC will be generated for rtpmidi instance %s\n", inst->name); + LOGPF("Random SSRC will be generated for instance %s", inst->name); } return 0; } else if(!strcmp(option, "bind")){ if(data->mode == unconfigured){ - fprintf(stderr, "Please specify mode for instance %s before setting bind host\n", inst->name); + LOGPF("Please specify mode for instance %s before setting bind host", inst->name); return 1; } - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host){ - fprintf(stderr, "Could not parse bind host specification %s for instance %s\n", value, inst->name); + LOGPF("Could not parse bind host specification %s for instance %s", value, inst->name); return 1; } @@ -247,7 +247,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "learn")){ if(data->mode != direct){ - fprintf(stderr, "The rtpmidi 'learn' option is only valid for direct mode instances\n"); + LOG("'learn' option is only valid for direct mode instances"); return 1; } data->learn_peers = 0; @@ -257,14 +257,14 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ - mmbackend_parse_hostspec(value, &host, &port); + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host || !port){ - fprintf(stderr, "Invalid peer %s configured on rtpmidi instance %s\n", value, inst->name); + LOGPF("Invalid peer %s configured on instance %s", value, inst->name); return 1; } if(mmbackend_parse_sockaddr(host, port, &sock_addr, &sock_len)){ - fprintf(stderr, "Failed to resolve peer %s configured on rtpmidi instance %s\n", value, inst->name); + LOGPF("Failed to resolve peer %s on instance %s", value, inst->name); return 1; } @@ -272,20 +272,20 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "session")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'session' option is only valid for apple mode instances\n"); + LOG("'session' option is only valid for apple mode instances"); return 1; } free(data->session_name); data->session_name = strdup(value); if(!data->session_name){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } else if(!strcmp(option, "invite")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'invite' option is only valid for apple mode instances\n"); + LOG("'invite' option is only valid for apple mode instances"); return 1; } @@ -293,19 +293,19 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) } else if(!strcmp(option, "join")){ if(data->mode != apple){ - fprintf(stderr, "The rtpmidi 'join' option is only valid for apple mode instances\n"); + LOG("'join' option is only valid for apple mode instances"); return 1; } free(data->accept); data->accept = strdup(value); if(!data->accept){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return 1; } return 0; } - fprintf(stderr, "Unknown rtpmidi instance option %s\n", option); + LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); return 1; } @@ -319,7 +319,7 @@ static instance* rtpmidi_instance(){ data = calloc(1, sizeof(rtpmidi_instance_data)); if(!data){ - fprintf(stderr, "Failed to allocate memory\n"); + LOG("Failed to allocate memory"); return NULL; } data->fd = -1; @@ -342,18 +342,18 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ } } else{ - fprintf(stderr, "Invalid rtpmidi channel specification %s\n", spec); + LOGPF("Invalid channel specification %s", spec); return NULL; } ident.fields.channel = strtoul(next_token, &next_token, 10); if(ident.fields.channel > 15){ - fprintf(stderr, "rtpmidi channel out of range in channel spec %s\n", spec); + LOGPF("Channel out of range in channel spec %s", spec); return NULL; } if(*next_token != '.'){ - fprintf(stderr, "rtpmidi channel specification %s does not conform to channel.\n", spec); + LOGPF("Channel specification %s does not conform to channel.", spec); return NULL; } @@ -378,7 +378,7 @@ static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = aftertouch; } else{ - fprintf(stderr, "Unknown rtpmidi channel control type in spec %s\n", spec); + LOGPF("Unknown control type in spec %s", spec); return NULL; } @@ -483,7 +483,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //ignore } else{ - fprintf(stderr, "Unknown AppleMIDI session command %02X %02X\n", command->command[0], command->command[1]); + LOGPF("Unknown AppleMIDI session command %02X %02X", command->command[0], command->command[1]); } return 0; @@ -500,12 +500,12 @@ static int rtpmidi_handle_data(instance* inst){ //TODO receive until EAGAIN if(bytes_recv < 0){ - fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + LOGPF("Failed to receive for instance %s", inst->name); return 1; } if(bytes_recv < sizeof(rtpmidi_header)){ - fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + LOGPF("Skipping short packet on instance %s", inst->name); return 0; } @@ -514,7 +514,7 @@ static int rtpmidi_handle_data(instance* inst){ return rtpmidi_handle_applemidi(inst, data->fd, frame, bytes_recv, &sock_addr, sock_len); } else if(rtp_header->vpxcc != RTPMIDI_HEADER_MAGIC || RTPMIDI_GET_TYPE(rtp_header->mpt) != RTPMIDI_HEADER_TYPE){ - fprintf(stderr, "rtpmidi instance %s received frame with invalid header magic\n", inst->name); + LOGPF("Frame with invalid header magic on %s", inst->name); return 0; } @@ -530,7 +530,7 @@ static int rtpmidi_handle_data(instance* inst){ } if(u == data->peers){ - fprintf(stderr, "rtpmidi instance %s learned new peer\n", inst->name); + LOGPF("Learned new peer on %s", inst->name); return rtpmidi_push_peer(data, sock_addr, sock_len); } } @@ -545,13 +545,13 @@ static int rtpmidi_handle_control(instance* inst){ ssize_t bytes_recv = recvfrom(data->control_fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); if(bytes_recv < 0){ - fprintf(stderr, "rtpmidi failed to receive for instance %s\n", inst->name); + LOGPF("Failed to receive on control socket for instance %s", inst->name); return 1; } //the shortest applemidi packet is still larger than the rtpmidi header, so use that as bar if(bytes_recv < sizeof(rtpmidi_header)){ - fprintf(stderr, "Skipping short packet on rtpmidi instance %s\n", inst->name); + LOGPF("Skipping short packet on control socket of instance %s", inst->name); return 0; } @@ -559,7 +559,7 @@ static int rtpmidi_handle_control(instance* inst){ return rtpmidi_handle_applemidi(inst, data->control_fd, frame, bytes_recv, &sock_addr, sock_len); } - fprintf(stderr, "Unknown session protocol frame received on rtpmidi instance %s\n", inst->name); + LOGPF("Unknown session protocol frame received on instance %s", inst->name); return 0; } @@ -590,7 +590,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ rv |= rtpmidi_handle_control(inst); } else{ - fprintf(stderr, "rtpmidi signaled descriptor not recognized\n"); + LOG("Signaled for unknown descriptor"); } } } @@ -613,20 +613,20 @@ static int rtpmidi_start(size_t n, instance** inst){ //register mdns fd to core if(cfg.mdns_fd >= 0){ if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ - fprintf(stderr, "rtpmidi failed to register mDNS socket with core\n"); + LOG("Failed to register mDNS socket with core"); return 1; } fds++; } else{ - fprintf(stderr, "No mDNS discovery interface bound, AppleMIDI session discovery disabled\n"); + LOG("No mDNS discovery interface bound, AppleMIDI session discovery disabled"); } for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode if(data->mode == unconfigured){ - fprintf(stderr, "rtpmidi instance %s is missing a mode configuration\n", inst[u]->name); + LOGPF("Instance %s is missing a mode configuration", inst[u]->name); return 1; } @@ -637,19 +637,19 @@ static int rtpmidi_start(size_t n, instance** inst){ //if not bound, bind to default if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ - fprintf(stderr, "Failed to bind default sockets for rtpmidi instance %s\n", inst[u]->name); + LOGPF("Failed to bind default sockets for instance %s", inst[u]->name); return 1; } //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ - fprintf(stderr, "rtpmidi failed to register instance socket with core\n"); + LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name); return 1; } fds += (data->control_fd >= 0) ? 2 : 1; } - fprintf(stderr, "rtpmidi backend registered %" PRIsize_t " descriptors to core\n", fds); + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } @@ -687,5 +687,6 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(cfg.mdns_fd); } + LOG("Backend shut down"); return 0; } -- cgit v1.2.3 From 474077a1ad13f945f2fd3dc6199e67eeeb86f517 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 24 Dec 2019 00:48:00 +0100 Subject: Implement AppleMIDI responder mode --- backends/rtpmidi.c | 180 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 32 ++++++---- backends/rtpmidi.md | 6 +- 3 files changed, 172 insertions(+), 46 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index e842a84..fc7af26 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "rtpmidi" +#define DEBUG #include #include @@ -9,6 +10,9 @@ #include "libmmbackend.h" #include "rtpmidi.h" +//TODO learn peer ssrcs +//TODO participants need to initiate clock sync at some point + static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -130,26 +134,35 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* } static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ - size_t u; + size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list - if(sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + if(!data->peer[u].inactive + && sock_len == data->peer[u].dest_len + && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ return 0; } - } - data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); - if(!data->peer){ - LOG("Failed to allocate memory"); - data->peers = 0; - return 1; + if(data->peer[u].inactive){ + p = u; + } } - data->peer[data->peers].dest = sock_addr; - data->peer[data->peers].dest_len = sock_len; + if(p == data->peers){ + data->peer = realloc(data->peer, (data->peers + 1) * sizeof(rtpmidi_peer)); + if(!data->peer){ + LOG("Failed to allocate memory"); + data->peers = 0; + return 1; + } + data->peers++; + DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers); + } - data->peers++; + data->peer[p].inactive = 0; + data->peer[p].dest = sock_addr; + data->peer[p].dest_len = sock_len; return 0; } @@ -445,7 +458,9 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //TODO journal section for(u = 0; u < data->peers; u++){ - sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + if(!data->peer[u].inactive){ + sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); + } } return 0; @@ -453,37 +468,141 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size_t bytes, struct sockaddr_storage* peer, socklen_t peer_len){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t response[RTPMIDI_PACKET_BUFFER] = ""; apple_command* command = (apple_command*) frame; - size_t u; + char* session_name = (char*) frame + sizeof(apple_command); + size_t u, n; + + command->command = be16toh(command->command); + + //check command version (except for clock sync and receiver feedback) + if(command->command != apple_sync && command->command != apple_feedback + && be32toh(command->version) != 2){ + LOGPF("Invalid AppleMIDI command version %" PRIu32 " on instance %s", be32toh(command->version), inst->name); + return 0; + } //find peer if already in list for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == peer_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == peer_len && !memcmp(&data->peer[u].dest, peer, peer_len)){ break; } } - if(!strncmp((char*) command->command, APPLEMIDI_INVITE, 2)){ - //TODO check whether the session is in the accept list - } - else if(!strncmp((char*) command->command, APPLEMIDI_ACCEPT, 2)){ - //TODO mark peer as in-session, start timesync + if(command->command == apple_invite){ + //check session name + for(n = sizeof(apple_command); n < bytes; n++){ + if(!frame[n]){ + break; + } + + if(!isprint(frame[n])){ + session_name = NULL; + break; + } + } + + //unterminated string + if(n == bytes){ + session_name = NULL; + } + + //FIXME if already in session, reject the invitation + if(data->accept && + (!strcmp(data->accept, "*") || (session_name && !strcmp(session_name, data->accept)))){ + //accept the invitation + LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); + //send accept message + apple_command* accept = (apple_command*) response; + accept->res1 = 0xFFFF; + accept->command = htobe16(apple_accept); + accept->version = htobe32(2); + accept->token = command->token; + accept->ssrc = htobe32(data->ssrc); + //add local name to response + //FIXME might want to use the session name in case it is set + if(cfg.mdns_name){ + memcpy(response + sizeof(apple_command), cfg.mdns_name, strlen(cfg.mdns_name) + 1); + } + else{ + memcpy(response + sizeof(apple_command), RTPMIDI_DEFAULT_NAME, strlen(RTPMIDI_DEFAULT_NAME) + 1); + } + sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + + //push peer + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + } + return 0; + } + else{ + //send reject message + LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); + apple_command reject = { + .res1 = 0xFFFF, + .command = htobe16(apple_reject), + .version = htobe32(2), + .token = command->token, + .ssrc = htobe32(data->ssrc) + }; + sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); + } + } + else if(command->command == apple_accept){ + if(fd != data->control_fd){ + return rtpmidi_push_peer(data, *peer, peer_len); + //FIXME store ssrc, start timesync + } + else{ + //TODO send invite on data fd + + } } - else if(!strncmp((char*) command->command, APPLEMIDI_REJECT, 2)){ - //TODO mark peer as rejected (or retry invitation) + else if(command->command == apple_reject){ + //just ignore this for now and retry the invitation } - else if(!strncmp((char*) command->command, APPLEMIDI_LEAVE, 2)){ - //TODO mark peer as disconnected, retry invitation + else if(command->command == apple_leave){ + //remove peer from list + if(u != data->peers){ + data->peer[u].inactive = 1; + } } - else if(!strncmp((char*) command->command, APPLEMIDI_SYNC, 2)){ - //TODO respond with sync answer + else if(command->command == apple_sync){ + //respond with sync answer + memcpy(response, frame, bytes); + apple_sync_frame* sync = (apple_sync_frame*) response; + DBGPF("Incoming sync on instance %s (%d)", inst->name, sync->count); + sync->command = htobe16(apple_sync); + sync->ssrc = htobe32(data->ssrc); + switch(sync->count){ + case 0: + //this happens if we're a participant + sync->count++; + sync->timestamp[1] = htobe64(mm_timestamp() * 10); + break; + case 1: + //this happens if we're an initiator + sync->count++; + sync->timestamp[2] = htobe64(mm_timestamp() * 10); + break; + default: + //ignore this one + return 0; + } + + sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len); + return 0; } - else if(!strncmp((char*) command->command, APPLEMIDI_FEEDBACK, 2)){ - //ignore + else if(command->command == apple_feedback){ + if(u != data->peers){ + //TODO store this somewhere to properly update the recovery journal + LOGPF("Feedback on instance %s", inst->name); + } } else{ - LOGPF("Unknown AppleMIDI session command %02X %02X", command->command[0], command->command[1]); + LOGPF("Unknown AppleMIDI session command %04X", command->command); } return 0; @@ -523,7 +642,8 @@ static int rtpmidi_handle_data(instance* inst){ //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ - if(data->peer[u].dest_len == sock_len + if(!data->peer[u].inactive + && data->peer[u].dest_len == sock_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ break; } @@ -632,7 +752,7 @@ static int rtpmidi_start(size_t n, instance** inst){ //generate random ssrc's if(!data->ssrc){ - data->ssrc = rand() << 16 | rand(); + data->ssrc = ((uint32_t) rand()) << 16 | rand(); } //if not bound, bind to default diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 2652db7..a9effd8 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -19,13 +19,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_HEADER_MAGIC 0x80 #define RTPMIDI_HEADER_TYPE 0x61 #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) - -#define APPLEMIDI_INVITE "IN" -#define APPLEMIDI_ACCEPT "OK" -#define APPLEMIDI_REJECT "NO" -#define APPLEMIDI_LEAVE "BY" -#define APPLEMIDI_SYNC "CK" -#define APPLEMIDI_FEEDBACK "RS" +#define RTPMIDI_DEFAULT_NAME "MIDIMonster" enum /*_rtpmidi_channel_type*/ { none = 0, @@ -55,7 +49,8 @@ typedef union { typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; - uint32_t ssrc; + //uint32_t ssrc; + uint8_t inactive; } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { @@ -70,8 +65,8 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* session_name; - char* accept; + char* session_name; /* initiator only */ + char* accept; /* participant only */ //direct mode config uint8_t learn_peers; @@ -83,10 +78,19 @@ typedef struct /*rtpmidi_announced_instance*/ { char** invite; } rtpmidi_announce; +enum applemidi_command { + apple_invite = 0x494E, //IN + apple_accept = 0x4F4B, //OK + apple_reject = 0x4E4F, //NO + apple_leave = 0x4259, //BY + apple_sync = 0x434B, //CK + apple_feedback = 0x5253 //RS +}; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t version; uint32_t token; uint32_t ssrc; @@ -95,19 +99,19 @@ typedef struct /*_apple_session_command*/ { typedef struct /*_apple_session_sync*/ { uint16_t res1; - uint8_t command[2]; + uint16_t command; uint32_t ssrc; uint8_t count; uint8_t res2[3]; uint64_t timestamp[3]; -} apple_sync; +} apple_sync_frame; typedef struct /*_apple_session_feedback*/ { uint16_t res1; uint8_t command[2]; uint32_t ssrc; uint32_t sequence; -} apple_feedback; +} apple_journal_feedback; typedef struct /*_rtp_midi_header*/ { uint8_t vpxcc; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d42df6f..93811c6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -13,7 +13,9 @@ selectable per-instance, with some methods requiring additional global configura * Direct connection with peer learning: The instance will send and receive data from peers configured in the instance configuration as well as previously unknown peers that voluntarily send data to the instance. -* AppleMIDI session management: +* AppleMIDI session management: The instance will be able to communicate (either as participant + or initiator) in an AppleMIDI session, which can optionally be announced via mDNS (better + known as "Bonjour" to Apple users). Note that instances that receive data from multiple peers will combine all inputs into one stream, which may lead to inconsistencies during playback. @@ -50,7 +52,7 @@ Common instance configuration parameters | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | | `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | -| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts all invitations). Setting this option makes the instance a session participant | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | | `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -- cgit v1.2.3 From 0a7858e17d8c60c5c768d310b1c7a511e91565ab Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 26 Dec 2019 09:44:17 +0100 Subject: Invite peer on data port --- backends/rtpmidi.c | 38 ++++++++++++++++++++++++++++---------- backends/rtpmidi.md | 2 +- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index fc7af26..4966a79 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -12,6 +12,7 @@ //TODO learn peer ssrcs //TODO participants need to initiate clock sync at some point +//TODO applemode peers still need session negotiation (peer option should only bypass discovery) static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -523,12 +524,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size accept->ssrc = htobe32(data->ssrc); //add local name to response //FIXME might want to use the session name in case it is set - if(cfg.mdns_name){ - memcpy(response + sizeof(apple_command), cfg.mdns_name, strlen(cfg.mdns_name) + 1); - } - else{ - memcpy(response + sizeof(apple_command), RTPMIDI_DEFAULT_NAME, strlen(RTPMIDI_DEFAULT_NAME) + 1); - } + memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); //push peer @@ -552,12 +548,24 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_accept){ if(fd != data->control_fd){ + LOGPF("Instance %s negotiated new peer\n", inst->name); return rtpmidi_push_peer(data, *peer, peer_len); //FIXME store ssrc, start timesync } else{ - //TODO send invite on data fd - + //send invite on data fd + LOGPF("Instance %s peer accepted on control port, inviting data port\n", inst->name); + //FIXME limit max length of session name + apple_command* invite = (apple_command*) response; + invite->res1 = 0xFFFF; + invite->command = htobe16(apple_invite); + invite->version = htobe32(2); + invite->token = command->token; + invite->ssrc = htobe32(data->ssrc); + memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + //calculate data port + ((struct sockaddr_in*) peer)->sin_port++; + sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } } else if(command->command == apple_reject){ @@ -775,7 +783,7 @@ static int rtpmidi_start(size_t n, instance** inst){ static int rtpmidi_shutdown(size_t n, instance** inst){ rtpmidi_instance_data* data = NULL; - size_t u; + size_t u, p; for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; @@ -801,8 +809,18 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } - //TODO free announces + for(u = 0; u < cfg.announces; u++){ + for(p = 0; p < cfg.announce[u].invites; p++){ + free(cfg.announce[u].invite[p]); + } + free(cfg.announce[u].invite); + } + free(cfg.announce); + cfg.announce = NULL; + cfg.announces = 0; + free(cfg.mdns_name); + cfg.mdns_name = NULL; if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 93811c6..e857a5a 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -36,7 +36,7 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `ssrc` | `0xDEADBEEF` | Randomly generated | 32-bit synchronization source identifier | | `mode` | `direct` | none | Instance session management mode (`direct` or `apple`) | -| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery protocols | +| `peer` | `10.1.2.3 9001` | none | MIDI session peer, may be specified multiple times. Bypasses session discovery (but still performs session negotiation) | `direct` mode instance configuration parameters -- cgit v1.2.3 From 4275aa8d0209768e45b43ef7e3aefcb808001926 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 27 Dec 2019 17:46:37 +0100 Subject: Implement MIDI command parsing --- backends/rtpmidi.c | 155 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 153 insertions(+), 2 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 4966a79..611d99b 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -134,6 +134,22 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 0; } +static char* rtpmidi_type_name(uint8_t type){ + switch(type){ + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ size_t u, p = data->peers; @@ -616,14 +632,145 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } +static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ + uint16_t length = 0; + size_t offset = 1, decode_time = 0, command_bytes = 0; + uint8_t midi_status = 0; + rtpmidi_channel_ident ident; + channel_value val; + channel* chan = NULL; + + if(!bytes){ + LOGPF("No command section in data on instance %s", inst->name); + return 1; + } + + //calculate midi command section length + length = frame[0] & 0x0F; + if(frame[0] & 0x80){ + //extended header + if(bytes < 2){ + LOGPF("Short command section (%" PRIsize_t " bytes) on %s, missing extended header", bytes, inst->name); + return 1; + } + length <<= 8; + length |= frame[1]; + offset = 2; + } + + command_bytes = offset + length; + DBGPF("%u/%" PRIsize_t " bytes of command section on %s, %s header, %s initial dtime", + length, bytes, inst->name, + (frame[0] & 0x80) ? "extended" : "normal", + (frame[0] & 0x20) ? "has" : "no"); + + if(command_bytes > bytes){ + LOGPF("Short command section on %s, indicated %" PRIsize_t ", had %" PRIsize_t, inst->name, command_bytes, bytes); + return 1; + } + + if(frame[0] & 0x20){ + decode_time = 1; + } + + do{ + //decode delta-time + if(decode_time){ + for(; offset < command_bytes && frame[offset] & 0x80; offset++){ + } + offset++; + } + + //section 3 of rfc6295 states that the first dtime as well as the last command may be omitted + //this may make sense on a low-speed serial line, but on a network... come on. + if(offset >= command_bytes){ + break; + } + + //check for a status byte + //TODO filter sysex + if(frame[offset] & 0x80){ + midi_status = frame[offset]; + offset++; + } + + //having variable encoding in each and every component is super annoying to check for... + if(offset >= command_bytes){ + break; + } + + ident.label = 0; + ident.fields.type = midi_status & 0xF0; + ident.fields.channel = midi_status & 0x0F; + + //single byte command + if(ident.fields.type == aftertouch){ + ident.fields.control = 0; + val.normalised = (double) frame[offset] / 127.0; + offset++; + } + //two-byte command + else{ + offset++; + if(offset >= command_bytes){ + break; + } + + if(ident.fields.type == pitchbend){ + ident.fields.control = 0; + val.normalised = (double)((frame[offset] << 7) | frame[offset - 1]) / 16384.0; + } + else{ + ident.fields.control = frame[offset - 1]; + val.normalised = (double) frame[offset] / 127.0; + } + + //fix-up note off events + if(ident.fields.type == 0x80){ + ident.fields.type = note; + val.normalised = 0; + } + + offset++; + } + + DBGPF("Decoded command type %02X channel %d control %d value %f", + ident.fields.type, ident.fields.channel, ident.fields.control, val.normalised); + + if(cfg.detect){ + if(ident.fields.type == pitchbend || ident.fields.type == aftertouch){ + LOGPF("Incoming data on channel %s.ch%d.%s, value %f", + inst->name, ident.fields.channel, + rtpmidi_type_name(ident.fields.type), val.normalised); + } + else{ + LOGPF("Incoming data on channel %s.ch%d.%s%d, value %f", + inst->name, ident.fields.channel, + rtpmidi_type_name(ident.fields.type), + ident.fields.control, val.normalised); + } + } + + //find channel + chan = mm_channel(inst, ident.label, 0); + if(chan){ + mm_channel_event(chan, val); + } + + decode_time = 1; + } while(offset < command_bytes); + + return 0; +} + static int rtpmidi_handle_data(instance* inst){ - size_t u; rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; struct sockaddr_storage sock_addr; socklen_t sock_len = sizeof(sock_addr); rtpmidi_header* rtp_header = (rtpmidi_header*) frame; ssize_t bytes_recv = recvfrom(data->fd, frame, sizeof(frame), 0, (struct sockaddr*) &sock_addr, &sock_len); + size_t u; //TODO receive until EAGAIN if(bytes_recv < 0){ @@ -645,7 +792,11 @@ static int rtpmidi_handle_data(instance* inst){ return 0; } - //TODO parse data + //parse data + if(rtpmidi_parse(inst, frame + sizeof(rtpmidi_header), bytes_recv - sizeof(rtpmidi_header))){ + //returning errors here fails the core loop, so just return 0 to have some logging + return 0; + } //try to learn peers if(data->learn_peers){ -- cgit v1.2.3 From 9cb41bd9e07a80885fafe30c9d230615f8b5cc67 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Dec 2019 14:48:22 +0100 Subject: Extend peer handling --- backends/rtpmidi.c | 111 +++++++++++++++++++++++++++++++++++++---------------- backends/rtpmidi.h | 5 ++- 2 files changed, 82 insertions(+), 34 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 611d99b..0a83fee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -13,11 +13,15 @@ //TODO learn peer ssrcs //TODO participants need to initiate clock sync at some point //TODO applemode peers still need session negotiation (peer option should only bypass discovery) +//TODO default session join? +//TODO default mode? +//TODO internal loop mode static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; uint8_t detect; + uint64_t last_service; size_t announces; rtpmidi_announce* announce; @@ -25,6 +29,8 @@ static struct /*_rtpmidi_global*/ { .mdns_fd = -1, .mdns_name = NULL, .detect = 0, + .last_service = 0, + .announces = 0, .announce = NULL }; @@ -150,18 +156,18 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list - if(!data->peer[u].inactive + if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ return 0; } - if(data->peer[u].inactive){ + if(!data->peer[u].active){ p = u; } } @@ -177,7 +183,9 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag DBGPF("Extending peer registry to %" PRIsize_t " entries", data->peers); } - data->peer[p].inactive = 0; + data->peer[p].active = 1; + data->peer[p].learned = learned; + data->peer[p].connected = connected; data->peer[p].dest = sock_addr; data->peer[p].dest_len = sock_len; return 0; @@ -287,6 +295,11 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 0; } else if(!strcmp(option, "peer")){ + if(data->mode == unconfigured){ + LOGPF("Please specify mode for instance %s before configuring peers", inst->name); + return 1; + } + mmbackend_parse_hostspec(value, &host, &port, NULL); if(!host || !port){ LOGPF("Invalid peer %s configured on instance %s", value, inst->name); @@ -298,7 +311,12 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } - return rtpmidi_push_peer(data, sock_addr, sock_len); + //apple peers are specified using the control port, but we want to store the data port as peer + if(data->mode == apple){ + ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); + } + + return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); } else if(!strcmp(option, "session")){ if(data->mode != apple){ @@ -475,7 +493,7 @@ static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v //TODO journal section for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive){ + if(data->peer[u].active && data->peer[u].connected){ sendto(data->fd, frame, offset, 0, (struct sockaddr*) &data->peer[u].dest, data->peer[u].dest_len); } } @@ -488,7 +506,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size uint8_t response[RTPMIDI_PACKET_BUFFER] = ""; apple_command* command = (apple_command*) frame; char* session_name = (char*) frame + sizeof(apple_command); - size_t u, n; + size_t n, u; command->command = be16toh(command->command); @@ -499,15 +517,6 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } - //find peer if already in list - for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive - && data->peer[u].dest_len == peer_len - && !memcmp(&data->peer[u].dest, peer, peer_len)){ - break; - } - } - if(command->command == apple_invite){ //check session name for(n = sizeof(apple_command); n < bytes; n++){ @@ -545,7 +554,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); } return 0; } @@ -561,11 +570,12 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size }; sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); } + return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer\n", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); //FIXME store ssrc, start timesync } else{ @@ -580,18 +590,31 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size invite->ssrc = htobe32(data->ssrc); memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port - ((struct sockaddr_in*) peer)->sin_port++; + ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } + return 0; } else if(command->command == apple_reject){ //just ignore this for now and retry the invitation } else if(command->command == apple_leave){ - //remove peer from list - if(u != data->peers){ - data->peer[u].inactive = 1; + //remove peer from list - this comes in on the control port, but we need to remove the data port... + ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + for(u = 0; u < data->peers; u++){ + if(data->peer[u].dest_len == peer_len + && !memcmp(&data->peer[u].dest, peer, peer_len)){ + LOGPF("Instance %s removed peer", inst->name); + //learned peers are marked inactive, configured peers are marked unconnected + if(data->peer[u].learned){ + data->peer[u].active = 0; + } + else{ + data->peer[u].connected = 0; + } + } } + return 0; } else if(command->command == apple_sync){ //respond with sync answer @@ -620,10 +643,9 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } else if(command->command == apple_feedback){ - if(u != data->peers){ - //TODO store this somewhere to properly update the recovery journal - LOGPF("Feedback on instance %s", inst->name); - } + //TODO store this somewhere to properly update the recovery journal + LOGPF("Feedback on instance %s", inst->name); + return 0; } else{ LOGPF("Unknown AppleMIDI session command %04X", command->command); @@ -674,7 +696,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ } do{ - //decode delta-time + //decode (and ignore) delta-time if(decode_time){ for(; offset < command_bytes && frame[offset] & 0x80; offset++){ } @@ -751,7 +773,7 @@ static int rtpmidi_parse(instance* inst, uint8_t* frame, size_t bytes){ } } - //find channel + //push event chan = mm_channel(inst, ident.label, 0); if(chan){ mm_channel_event(chan, val); @@ -801,7 +823,7 @@ static int rtpmidi_handle_data(instance* inst){ //try to learn peers if(data->learn_peers){ for(u = 0; u < data->peers; u++){ - if(!data->peer[u].inactive + if(data->peer[u].active && data->peer[u].dest_len == sock_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ break; @@ -810,7 +832,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len); + return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1); } } return 0; @@ -842,13 +864,28 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } +static int rtpmidi_service(){ + + if(cfg.mdns_fd >= 0){ + //TODO send applemidi discovery packets + } + //TODO send sync packets for all connected applemidi peers + //TODO try to invite pre-defined unconnected applemidi peers + return 0; +} + static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; instance* inst = NULL; rtpmidi_instance_data* data = NULL; - //TODO handle mDNS discovery frames + //handle service tasks (mdns, clock sync, peer connections) + if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ + DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); + rtpmidi_service(); + cfg.last_service = mm_timestamp(); + } if(!num){ return 0; @@ -866,7 +903,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ rv |= rtpmidi_handle_data(inst); } else if(fds[u].fd == data->control_fd){ - rv |= rtpmidi_handle_control(inst); + rv |= rtpmidi_handle_control(inst); } else{ LOG("Signaled for unknown descriptor"); @@ -878,7 +915,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start(size_t n, instance** inst){ - size_t u, fds = 0; + size_t u, p, fds = 0; rtpmidi_instance_data* data = NULL; //if mdns name defined and no socket, bind default values @@ -920,6 +957,14 @@ static int rtpmidi_start(size_t n, instance** inst){ return 1; } + //mark configured peers on direct instances as connected so output is sent + //apple mode instances go through the session negotiation before marking peers as active + if(data->mode == direct){ + for(p = 0; p < data->peers; p++){ + data->peer[p].connected = 1; + } + } + //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ LOGPF("Failed to register descriptor for instance %s with core", inst[u]->name); diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a9effd8..db6237b 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -20,6 +20,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_HEADER_TYPE 0x61 #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" +#define RTPMIDI_SERVICE_INTERVAL 1000 enum /*_rtpmidi_channel_type*/ { none = 0, @@ -50,7 +51,9 @@ typedef struct /*_rtpmidi_peer*/ { struct sockaddr_storage dest; socklen_t dest_len; //uint32_t ssrc; - uint8_t inactive; + uint8_t active; //marked for reuse + uint8_t learned; //learned / configured peer + uint8_t connected; //currently in active session } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { -- cgit v1.2.3 From 0c21a3d8d6a6b3448d0341fbc7239f90ba888a80 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Dec 2019 12:56:26 +0100 Subject: rtpmidi service handling --- backends/rtpmidi.c | 74 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 0a83fee..8e2b208 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -164,6 +164,8 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + //if yes, update connection flag (but not learned flag because that doesnt change) + data->peer[u].connected = connected; return 0; } @@ -574,13 +576,13 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_accept){ if(fd != data->control_fd){ - LOGPF("Instance %s negotiated new peer\n", inst->name); + LOGPF("Instance %s negotiated new peer", inst->name); return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); //FIXME store ssrc, start timesync } else{ //send invite on data fd - LOGPF("Instance %s peer accepted on control port, inviting data port\n", inst->name); + LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); //FIXME limit max length of session name apple_command* invite = (apple_command*) response; invite->res1 = 0xFFFF; @@ -865,12 +867,70 @@ static int rtpmidi_handle_control(instance* inst){ } static int rtpmidi_service(){ + size_t n, u, p; + instance** inst = NULL; + rtpmidi_instance_data* data = NULL; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + struct sockaddr_storage control_peer; + //prepare commands + apple_sync_frame sync = { + .res1 = 0xFFFF, + .command = htobe16(apple_sync), + .ssrc = 0, + .count = 0, + .timestamp = { + mm_timestamp() * 10 + } + }; + apple_command* invite = (apple_command*) &frame; + invite->res1 = 0xFFFF; + invite->command = htobe16(apple_invite); + invite->version = htobe32(2); + invite->token = ((uint32_t) rand()) << 16 | rand(); + + if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ + LOG("Failed to fetch instances"); + return 1; + } + + //mdns discovery if(cfg.mdns_fd >= 0){ //TODO send applemidi discovery packets } - //TODO send sync packets for all connected applemidi peers - //TODO try to invite pre-defined unconnected applemidi peers + + for(u = 0; u < n; u++){ + data = (rtpmidi_instance_data*) inst[u]->impl; + + if(data->mode == apple){ + for(p = 0; p < data->peers; p++){ + if(data->peer[p].active && data->peer[p].connected){ + //apple sync + DBGPF("Instance %s initializing sync on peer %" PRIsize_t, inst[u]->name, p); + sync.ssrc = htobe32(data->ssrc); + //calculate remote control port from data port + memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); + ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + + sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + } + else if(data->peer[p].active && !data->peer[p].learned){ + //try to invite pre-defined unconnected applemidi peers + DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); + invite->ssrc = htobe32(data->ssrc); + //calculate remote control port from data port + memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); + ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + //append session name to packet + memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + + sendto(data->control_fd, invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + } + } + } + } + + free(inst); return 0; } @@ -883,7 +943,9 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //handle service tasks (mdns, clock sync, peer connections) if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); - rtpmidi_service(); + if(rtpmidi_service()){ + return 1; + } cfg.last_service = mm_timestamp(); } -- cgit v1.2.3 From 666aec036f9bf0de82c435bd7eace271613cee1e Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 3 Jan 2020 00:25:50 +0100 Subject: Limit connection attempts to one in 10sec --- backends/rtpmidi.c | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 8e2b208..c39139a 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -//#define DEBUG +#define DEBUG #include #include @@ -11,8 +11,6 @@ #include "rtpmidi.h" //TODO learn peer ssrcs -//TODO participants need to initiate clock sync at some point -//TODO applemode peers still need session negotiation (peer option should only bypass discovery) //TODO default session join? //TODO default mode? //TODO internal loop mode @@ -164,7 +162,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ - //if yes, update connection flag (but not learned flag because that doesnt change) + //if yes, update connection flag (but not learned flag because that doesn't change) data->peer[u].connected = connected; return 0; } @@ -914,7 +912,7 @@ static int rtpmidi_service(){ sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } - else if(data->peer[p].active && !data->peer[p].learned){ + else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); invite->ssrc = htobe32(data->ssrc); -- cgit v1.2.3 From 26d661fa01df5cb2569432413ad91df891d088f2 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Sun, 8 Mar 2020 21:08:42 +0100 Subject: Implement argument parsing. Implement forceupdate argument (Closes #49). --- installer.sh | 160 ++++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 119 insertions(+), 41 deletions(-) diff --git a/installer.sh b/installer.sh index 15ad203..7cf389d 100755 --- a/installer.sh +++ b/installer.sh @@ -12,25 +12,81 @@ latest_version=$(curl --silent "https://api.github.com/repos/cbdevnet/midimonste makeargs=all # Build args +normal=$(tput sgr0) +bold=$(tput bold) +bold2=$(tput smso) +c_red=$(tput setaf 1) +c_green=$(tput setaf 2) + VAR_DESTDIR="" # Unused VAR_PREFIX="/usr" VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" VAR_DEFAULT_CFG="/etc/midimonster/midimonster.cfg" VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" -bold=$(tput bold) -normal=$(tput sgr0) ################################################ SETUP ################################################ ############################################## FUNCTIONS ############################################## -INSTALL-DEPS () { ##Install deps from array "$deps" +ARGS () { + for i in "$@" + do + case $i in + --prefix=*) + VAR_PREFIX="${i#*=}" + VAR_PREFIX_I="1" + ;; + --plugins=*) + VAR_PLUGINS="${i#*=}" + VAR_PLUGINS_I="1" + ;; + --defcfg=*) + VAR_DEFAULT_CFG="${i#*=}" + VAR_DEFAULT_CFG_I="1" + ;; + --examples=*) + VAR_EXAMPLE_CFGS="${i#*=}" + VAR_EXAMPLE_CFGS_I="1" + ;; + --dev) + NIGHTLY=1 + NIGHTLY_I="1" + ;; + -d|--default) + VAR_PREFIX_I="1" + VAR_PLUGINS_I="1" + VAR_DEFAULT_CFG_I="1" + VAR_EXAMPLE_CFGS_I="1" + NIGHTLY_I="1" + NIGHTLY=1 + ;; + -fu|--forceupdate) + UPDATER_FORCE="1" + ;; + -h|--help|*) # [WIP messages] + printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" + printf "\n ${c_green}--prefix${normal} ${c_red}${normal} Sets the prefix" + printf "\n ${c_green}--plugins${normal} ${c_red}${normal} Sets the plugin path" + printf "\n ${c_green}--defcfg${normal} ${c_red}${normal} Sets the config path" + printf "\n ${c_green}--examples${normal} ${c_red}${normal} Sets the example configs path\n" + printf "\n ${c_green}--dev${normal} Install nightly version (combine with --forceupdate to upgrade to nightly version)" + printf "\n ${c_green}-d, --default${normal} Use default values to install" + printf "\n ${c_green}-fu, --forceupdate${normal} Force the updater to update without a version check (Can be used to upgrade to the nightly version)" + printf "\n ${c_green}-h, --help${normal} Show this message\n" + exit 1 + ;; + esac + shift + done +} + +INSTALL-DEPS () { # Install deps from array "$deps" for t in ${deps[@]}; do if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then printf "Installing %s\n" "$t" apt-get install $t; - printf "Done\n"; + printf "Done.\n"; else printf "%s already installed!\n" "$t" fi @@ -38,26 +94,28 @@ INSTALL-DEPS () { ##Install deps from array "$deps" printf "\n" } -NIGHTLY_CHECK () { - #Asks for nightly version - read -p "Do you want to install the latest development version? (y/n)? " magic - case "$magic" in - y|Y) - printf "OK! You´re a risky person ;D\n" - NIGHTLY=1 - ;; - n|N) - printf "That´s OK - installing the latest stable version for you ;-)\n" - NIGHTLY=0 - ;; - *) - printf "Invalid input\n" - ERROR - ;; - esac +NIGHTLY_CHECK () { # Asks for nightly version + if [ -z "$NIGHTLY_I" ]; then + read -p "Do you want to install the latest development version? (y/n)? " magic + case "$magic" in + y|Y) + printf "OK! You´re a risky person ;D\n" + NIGHTLY=1 + ;; + n|N) + printf "That´s OK - installing the latest stable version for you ;-)\n" + NIGHTLY=0 + ;; + *) + printf "${bold}Invalid input -- INSTALLING LATEST STABLE VERSION!${normal}\n" + NIGHTLY=0 + ;; + esac + fi # Roll back to last tag if we're not on a nightly build if [ "$NIGHTLY" != 1 ]; then + cd "$tmp_path" printf "Finding latest stable version...\n" Iversion=$(git describe --abbrev=0) printf "Starting Git checkout to %s...\n" "$Iversion" @@ -75,20 +133,27 @@ INSTALL-PREP () { printf "\n" ) NIGHTLY_CHECK - printf "Preparation successful\n\n" - printf "${bold}If you don't know what you're doing, just hit enter 4 times.${normal}\n" - - read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX - VAR_PREFIX="${input:-$VAR_PREFIX}" + printf "Preparation done.\n\n" + printf "${bold}If you don't know what you're doing, just hit enter.${normal}\n\n" + if [ -z "$VAR_PREFIX_I" ]; then + read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX + VAR_PREFIX="${input:-$VAR_PREFIX}" + fi - read -e -i "$VAR_PLUGINS" -p "PLUGINS (Plugin directory): " input # Reads VAR_PLUGINS - VAR_PLUGINS="${input:-$VAR_PLUGINS}" + if [ -z "$VAR_PLUGINS_I" ]; then + read -e -i "$VAR_PLUGINS" -p "PLUGINS (Plugin directory): " input # Reads VAR_PLUGINS + VAR_PLUGINS="${input:-$VAR_PLUGINS}" + fi - read -e -i "$VAR_DEFAULT_CFG" -p "Default config path: " input # Reads VAR_DEFAULT_CFG - VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" + if [ -z "$VAR_DEFAULT_CFG_I" ]; then + read -e -i "$VAR_DEFAULT_CFG" -p "Default config path: " input # Reads VAR_DEFAULT_CFG + VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" + fi - read -e -i "$VAR_EXAMPLE_CFGS" -p "Example config directory: " input # Reads VAR_EXAMPLE_CFGS - VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" + if [ -z "$VAR_EXAMPLE_CFGS_I" ]; then + read -e -i "$VAR_EXAMPLE_CFGS" -p "Example config directory: " input # Reads VAR_EXAMPLE_CFGS + VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" + fi UPDATER_SAVE @@ -106,12 +171,9 @@ UPDATER-PREP () { printf "\nInitializing repository...\n" cd $tmp_path git init $tmp_path - printf "Successfully imported settings from %s\n" "$updater_file" ) NIGHTLY_CHECK - printf "Preparation successful\n\n" - - rm -f "$VAR_PREFIX/bin/midimonster" + printf "Preparation done.\n\n" rm -rf "$VAR_PLUGINS/" UPDATER_SAVE @@ -125,12 +187,12 @@ UPDATER-PREP () { UPDATER () { installed_version="$(midimonster --version)" - #installed_version="MIDIMonster v0.3-40-gafed325" # FOR TESTING ONLY! (or bypassing updater version check) + #installed_version="MIDIMonster v0.3-40-gafed325" # FOR TESTING ONLY! if [[ "$installed_version" =~ "$latest_version" ]]; then printf "Newest Version is already installed! ${bold}($installed_version)${normal}\n\n" ERROR else - printf "The installed Version ${bold}´$installed_version´${normal} equals not the newest stable version ${bold}´$latest_version´${normal} (Maybe you are running a development version?)\n\n" + printf "The installed Version ${bold}´$installed_version´${normal} equals not the newest stable version ${bold}´$latest_version´${normal} (Maybe you are running a development(NIGHTLY) version?)\n\n" fi UPDATER-PREP @@ -180,6 +242,7 @@ CLEAN () { ################################################ Main ################################################# trap ERROR SIGINT SIGTERM SIGKILL +ARGS "$@" # Parse arguments clear # Check if $user = root! @@ -189,16 +252,31 @@ if [ "$user" != "root" ]; then fi if [ $(wget -q --spider http://github.com) $? -eq 1 ]; then - printf "You need connection to the internet\n" + printf "You need a connection to the internet\n" ERROR fi +# Forceupdate # Now only with default config because source imports all and overwrites the args.. [WIP!] +if [ "$UPDATER_FORCE" -eq "1" ]; then + printf "Forcing the updater to start...\n\n" + if [ -f $updater_file ]; then + . $updater_file + ARGS "$@" # Parse arguments again to compensate overwrite from source /\ + fi + + UPDATER-PREP + INSTALL-RUN + DONE +fi + # Check if updater config file exist and import it (overwrites default values!) if [ -f $updater_file ]; then printf "Starting updater...\n\n" . $updater_file + ARGS "$@" # Parse arguments again to compensate overwrite from source /\ + printf "Successfully imported settings from %s\n" "$updater_file" - # Check if binary $updater/bin/midimonster exist. If yes start updater else skip. + # Check if binary $VAR_PREFIX/bin/midimonster exist. If yes start updater else skip. if [ -x "$VAR_PREFIX/bin/midimonster" ]; then UPDATER else @@ -206,8 +284,8 @@ if [ -f $updater_file ]; then fi fi +#Normal install INSTALL-DEPS INSTALL-PREP -printf "\n" INSTALL-RUN DONE -- cgit v1.2.3 From f66a8cf114c113cabd81bc9d8900f22c3e19da3c Mon Sep 17 00:00:00 2001 From: Spacelord Date: Sun, 8 Mar 2020 21:21:59 +0100 Subject: Updater: fix if --- installer.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/installer.sh b/installer.sh index 7cf389d..30b03e5 100755 --- a/installer.sh +++ b/installer.sh @@ -257,11 +257,12 @@ if [ $(wget -q --spider http://github.com) $? -eq 1 ]; then fi # Forceupdate # Now only with default config because source imports all and overwrites the args.. [WIP!] -if [ "$UPDATER_FORCE" -eq "1" ]; then +if [ "$UPDATER_FORCE" = "1" ]; then printf "Forcing the updater to start...\n\n" if [ -f $updater_file ]; then . $updater_file ARGS "$@" # Parse arguments again to compensate overwrite from source /\ + printf "Successfully imported settings from %s\n" "$updater_file" fi UPDATER-PREP @@ -280,7 +281,7 @@ if [ -f $updater_file ]; then if [ -x "$VAR_PREFIX/bin/midimonster" ]; then UPDATER else - printf "midimonster binary not found, skipping updater.\n" + printf "MIDIMonster binary not found, skipping updater.\nYou can force an update with --forceupdate\n" fi fi -- cgit v1.2.3 From 7085f3511cd41cbb9f05d2ab46f3bcb2b93a6c71 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Sun, 8 Mar 2020 22:00:37 +0100 Subject: Installer: Update prefix-containing variables when the prefix argument is set. --- installer.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/installer.sh b/installer.sh index 30b03e5..ed6b2aa 100755 --- a/installer.sh +++ b/installer.sh @@ -20,9 +20,9 @@ c_green=$(tput setaf 2) VAR_DESTDIR="" # Unused VAR_PREFIX="/usr" -VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" +VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" # Reassigned in func. ARGS to update "$VAR_PREFIX" when an argument is set. VAR_DEFAULT_CFG="/etc/midimonster/midimonster.cfg" -VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" +VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" # Reassigned in func. ARGS to update "$VAR_PREFIX" when an argument is set. ################################################ SETUP ################################################ @@ -36,6 +36,8 @@ ARGS () { --prefix=*) VAR_PREFIX="${i#*=}" VAR_PREFIX_I="1" + VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" + VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" ;; --plugins=*) VAR_PLUGINS="${i#*=}" @@ -134,7 +136,7 @@ INSTALL-PREP () { ) NIGHTLY_CHECK printf "Preparation done.\n\n" - printf "${bold}If you don't know what you're doing, just hit enter.${normal}\n\n" + printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" if [ -z "$VAR_PREFIX_I" ]; then read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX VAR_PREFIX="${input:-$VAR_PREFIX}" -- cgit v1.2.3 From c656294f14c3b31c408d2e24fa8a062144ceb9ab Mon Sep 17 00:00:00 2001 From: Spacelord Date: Mon, 9 Mar 2020 16:40:43 +0100 Subject: Installer: Update prefix-containing variables. --- installer.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/installer.sh b/installer.sh index ed6b2aa..049d7c7 100755 --- a/installer.sh +++ b/installer.sh @@ -38,6 +38,7 @@ ARGS () { VAR_PREFIX_I="1" VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" + VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" ;; --plugins=*) VAR_PLUGINS="${i#*=}" @@ -138,8 +139,10 @@ INSTALL-PREP () { printf "Preparation done.\n\n" printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" if [ -z "$VAR_PREFIX_I" ]; then - read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX + read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX then update containing vars VAR_PREFIX="${input:-$VAR_PREFIX}" + VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" # Update prefix-containing variables. + VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" # Update prefix-containing variables. fi if [ -z "$VAR_PLUGINS_I" ]; then -- cgit v1.2.3 From 62afd5d7fb2c6d764d0756136041d0162cb022db Mon Sep 17 00:00:00 2001 From: Spacelord Date: Mon, 9 Mar 2020 19:07:50 +0100 Subject: Installer: Update help --- installer.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/installer.sh b/installer.sh index 049d7c7..f8fad18 100755 --- a/installer.sh +++ b/installer.sh @@ -13,10 +13,12 @@ latest_version=$(curl --silent "https://api.github.com/repos/cbdevnet/midimonste makeargs=all # Build args normal=$(tput sgr0) +dim=$(tput dim) bold=$(tput bold) -bold2=$(tput smso) -c_red=$(tput setaf 1) -c_green=$(tput setaf 2) +uline=$(tput smul) +c_red=$(tput setaf 1) +c_green=$(tput setaf 2) +c_mag=$(tput setaf 5) VAR_DESTDIR="" # Unused VAR_PREFIX="/usr" @@ -67,16 +69,17 @@ ARGS () { -fu|--forceupdate) UPDATER_FORCE="1" ;; - -h|--help|*) # [WIP messages] + -h|--help|*) # Help messages printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" - printf "\n ${c_green}--prefix${normal} ${c_red}${normal} Sets the prefix" - printf "\n ${c_green}--plugins${normal} ${c_red}${normal} Sets the plugin path" - printf "\n ${c_green}--defcfg${normal} ${c_red}${normal} Sets the config path" - printf "\n ${c_green}--examples${normal} ${c_red}${normal} Sets the example configs path\n" - printf "\n ${c_green}--dev${normal} Install nightly version (combine with --forceupdate to upgrade to nightly version)" + printf "\n ${c_green}--prefix${normal} ${c_red}${normal} Sets the prefix ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_PREFIX" + printf "\n ${c_green}--plugins${normal} ${c_red}${normal} Sets the plugin path ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_PLUGINS" + printf "\n ${c_green}--defcfg${normal} ${c_red}${normal} Sets the config path ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_DEFAULT_CFG" + printf "\n ${c_green}--examples${normal} ${c_red}${normal} Sets the example configs path ${c_mag}Default:${normal} ${dim}%s ${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n ${c_green}--dev${normal} Install nightly version" printf "\n ${c_green}-d, --default${normal} Use default values to install" - printf "\n ${c_green}-fu, --forceupdate${normal} Force the updater to update without a version check (Can be used to upgrade to the nightly version)" - printf "\n ${c_green}-h, --help${normal} Show this message\n" + printf "\n ${c_green}-fu, --forceupdate${normal} Force the updater to update without a version check" + printf "\n ${c_green}-h, --help${normal} Show this message" + printf "\n ${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" exit 1 ;; esac -- cgit v1.2.3 From 878988c9d5a8e106af16d55702176a0ce8f0ae71 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 9 Mar 2020 21:12:48 +0100 Subject: Remove deprecated MIDI channel syntax --- backends/midi.c | 79 +++++++++++++++++++++----------------------------------- backends/midi.md | 2 -- 2 files changed, 30 insertions(+), 51 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index f73ebb4..f378f1e 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -111,39 +111,22 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ .label = 0 }; - //support deprecated syntax for a transition period... - uint8_t old_syntax = 0; - char* channel; - + char* channel = NULL; if(!strncmp(spec, "ch", 2)){ channel = spec + 2; if(!strncmp(spec, "channel", 7)){ channel = spec + 7; } } - else if(!strncmp(spec, "cc", 2)){ - ident.fields.type = cc; - channel = spec + 2; - old_syntax = 1; - } - else if(!strncmp(spec, "note", 4)){ - ident.fields.type = note; - channel = spec + 4; - old_syntax = 1; - } - else if(!strncmp(spec, "nrpn", 4)){ - ident.fields.type = nrpn; - channel = spec + 4; - old_syntax = 1; - } - else{ - LOGPF("Unknown control type in %s", spec); + + if(!channel){ + LOGPF("Invalid channel specification %s", spec); return NULL; } ident.fields.channel = strtoul(channel, &channel, 10); if(ident.fields.channel > 15){ - LOGPF("Channel out of range in spec %s", spec); + LOGPF("MIDI channel out of range in spec %s", spec); return NULL; } @@ -154,33 +137,31 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ //skip the period channel++; - if(!old_syntax){ - if(!strncmp(channel, "cc", 2)){ - ident.fields.type = cc; - channel += 2; - } - else if(!strncmp(channel, "note", 4)){ - ident.fields.type = note; - channel += 4; - } - else if(!strncmp(channel, "nrpn", 4)){ - ident.fields.type = nrpn; - channel += 4; - } - else if(!strncmp(channel, "pressure", 8)){ - ident.fields.type = pressure; - channel += 8; - } - else if(!strncmp(channel, "pitch", 5)){ - ident.fields.type = pitchbend; - } - else if(!strncmp(channel, "aftertouch", 10)){ - ident.fields.type = aftertouch; - } - else{ - LOGPF("Unknown control type in %s", spec); - return NULL; - } + if(!strncmp(channel, "cc", 2)){ + ident.fields.type = cc; + channel += 2; + } + else if(!strncmp(channel, "note", 4)){ + ident.fields.type = note; + channel += 4; + } + else if(!strncmp(channel, "nrpn", 4)){ + ident.fields.type = nrpn; + channel += 4; + } + else if(!strncmp(channel, "pressure", 8)){ + ident.fields.type = pressure; + channel += 8; + } + else if(!strncmp(channel, "pitch", 5)){ + ident.fields.type = pitchbend; + } + else if(!strncmp(channel, "aftertouch", 10)){ + ident.fields.type = aftertouch; + } + else{ + LOGPF("Unknown control type in %s", spec); + return NULL; } ident.fields.control = strtoul(channel, NULL, 10); diff --git a/backends/midi.md b/backends/midi.md index 108860e..8188444 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -34,8 +34,6 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). -The earlier syntax of `.` is officially deprecated but still supported for compatibility -reasons. This support may be removed at some future time. The `pitch` and `aftertouch` events are channel-wide, thus they can be specified as `channel.`. -- cgit v1.2.3 From 4ebcda3a1b0d90c44e91a5dfe455ad59fe694cbe Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 9 Mar 2020 21:31:54 +0100 Subject: Update README and example config --- README.md | 9 +++++++-- configs/osc-artnet.cfg | 2 +- midimonster.h | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a71b3a..aeaeb2a 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,10 @@ chmod +x ./installer.sh ./installer.sh ``` +The installer can also be used for automating installations by specifying additional +command line arguments. To see a list of valid arguments, run the installer with the +`--help` argument. + The installer script can also update MIDIMonster to the latest version automatically, using a configuration file generated during the installation. To do so, run `midimonster-updater` as root on your system after using the installer. @@ -187,8 +191,9 @@ dpkg -i .deb ### Building from source To build the MIDIMonster directly from the sources, you'll need some libraries that provide -support for the protocols to translate. When building from source, you can also to exclude -backends (for example, if you don't need them or don't want to install their prerequisites). +support for the protocols to translate. When building from source, you can also choose to +exclude backends (for example, if you don't need them or don't want to install their +prerequisites). * `libasound2-dev` (for the ALSA MIDI backend) * `libevdev-dev` (for the evdev backend) diff --git a/configs/osc-artnet.cfg b/configs/osc-artnet.cfg index ab1d767..35b2111 100644 --- a/configs/osc-artnet.cfg +++ b/configs/osc-artnet.cfg @@ -5,7 +5,7 @@ bind = 0.0.0.0 [osc touch] -bind = * 8000 +bind = 0.0.0.0 8000 dest = learn@8001 [artnet out] diff --git a/midimonster.h b/midimonster.h index 2c29956..bad83c7 100644 --- a/midimonster.h +++ b/midimonster.h @@ -7,7 +7,7 @@ /* Core version unless set by the build process */ #ifndef MIDIMONSTER_VERSION - #define MIDIMONSTER_VERSION "v0.4-dist" + #define MIDIMONSTER_VERSION "v0.5-dist" #endif /* Set backend name if unset */ -- cgit v1.2.3 From d757a9c63371fcf5f9aa832d58ed4b8bdf634909 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:42:55 +0100 Subject: Restructure core loop, add check for zero-descriptor case --- TODO | 1 + backend.c | 3 + midimonster.c | 197 ++++++++++++++++++++++++++++++++++++---------------------- 3 files changed, 125 insertions(+), 76 deletions(-) diff --git a/TODO b/TODO index 900cc1b..1ea91f4 100644 --- a/TODO +++ b/TODO @@ -7,3 +7,4 @@ make event collectors threadsafe to stop marshalling data... collect & check backend API version windows strerror move all connection establishment to _start to be able to hot-stop/start all backends +only call _handle and _interval on backends with instances diff --git a/backend.c b/backend.c index e9c3829..ef8f66a 100644 --- a/backend.c +++ b/backend.c @@ -4,6 +4,7 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core/be" #include "midimonster.h" #include "backend.h" @@ -227,10 +228,12 @@ struct timeval backend_timeout(){ if(backends[u].interval){ res = backends[u].interval(); if((res / 1000) < secs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, backends[u].name); secs = res / 1000; msecs = res % 1000; } else if(res / 1000 == secs && (res % 1000) < msecs){ + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, backends[u].name); msecs = res % 1000; } } diff --git a/midimonster.c b/midimonster.c index b8594b4..3212f7f 100644 --- a/midimonster.c +++ b/midimonster.c @@ -9,6 +9,7 @@ #else #define MM_API __attribute__((dllexport)) #endif +#define BACKEND_NAME "core" #include "midimonster.h" #include "config.h" #include "backend.h" @@ -325,18 +326,126 @@ static int args_parse(int argc, char** argv, char** cfg_file){ return 0; } -int main(int argc, char** argv){ - fd_set all_fds, read_fds; +static int core_process(size_t nfds, managed_fd* signaled_fds){ event_collection* secondary = NULL; - struct timeval tv; - size_t u, n; + size_t u; + + //run backend processing, collect events + DBGPF("%lu backend FDs signaled\n", nfds); + if(backends_handle(nfds, signaled_fds)){ + return 1; + } + + while(primary->n){ + //swap primary and secondary event collectors + DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); + for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ + if(primary != event_pool + u){ + secondary = primary; + primary = event_pool + u; + break; + } + } + + //push collected events to target backends + if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ + fprintf(stderr, "Backends failed to handle output\n"); + return 1; + } + + //reset the event count + secondary->n = 0; + } + + return 0; +} + +static int core_loop(){ + fd_set all_fds, read_fds; managed_fd* signaled_fds = NULL; - int rv = EXIT_FAILURE, error, maxfd = -1; - char* cfg_file = DEFAULT_CFG; + struct timeval tv; + int error, maxfd = -1; + size_t n, u; #ifdef _WIN32 char* error_message = NULL; + #else + struct timespec ts; #endif + FD_ZERO(&all_fds); + + //process events + while(!shutdown_requested){ + //rebuild fd set if necessary + if(fd_set_dirty){ + all_fds = fds_collect(&maxfd); + signaled_fds = realloc(signaled_fds, fds * sizeof(managed_fd)); + if(!signaled_fds){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + fd_set_dirty = 0; + } + + //wait for & translate events + read_fds = all_fds; + tv = backend_timeout(); + + //check whether there are any fds active, windows does not like select() without descriptors + if(maxfd >= 0){ + error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); + if(error < 0){ + #ifndef _WIN32 + fprintf(stderr, "select failed: %s\n", strerror(errno)); + #else + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); + fprintf(stderr, "select failed: %s\n", error_message); + LocalFree(error_message); + error_message = NULL; + #endif + free(signaled_fds); + return 1; + } + } + else{ + DBGPF("No descriptors, sleeping for %zu msec", tv.tv_sec * 1000 + tv.tv_usec / 1000); + #ifdef _WIN32 + Sleep(tv.tv_sec * 1000 + tv.tv_usec / 1000); + #else + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; + error = nanosleep(&ts, NULL); + #endif + } + + //update this iteration's timestamp + update_timestamp(); + + //find all signaled fds + n = 0; + for(u = 0; u < fds; u++){ + if(fd[u].fd >= 0 && FD_ISSET(fd[u].fd, &read_fds)){ + signaled_fds[n] = fd[u]; + n++; + } + } + + //fetch and process events + if(core_process(n, signaled_fds)){ + free(signaled_fds); + return 1; + } + } + + free(signaled_fds); + return 0; +} + +int main(int argc, char** argv){ + int rv = EXIT_FAILURE; + char* cfg_file = DEFAULT_CFG; + //parse commandline arguments if(args_parse(argc, argv, &cfg_file)){ return EXIT_FAILURE; @@ -347,7 +456,6 @@ int main(int argc, char** argv){ return EXIT_FAILURE; } - FD_ZERO(&all_fds); //initialize backends if(plugins_load(PLUGINS)){ fprintf(stderr, "Failed to initialize a backend\n"); @@ -377,80 +485,17 @@ int main(int argc, char** argv){ signal(SIGINT, signal_handler); - //process events - while(!shutdown_requested){ - //rebuild fd set if necessary - if(fd_set_dirty){ - all_fds = fds_collect(&maxfd); - signaled_fds = realloc(signaled_fds, fds * sizeof(managed_fd)); - if(!signaled_fds){ - fprintf(stderr, "Failed to allocate memory\n"); - goto bail; - } - fd_set_dirty = 0; - } - - //wait for & translate events - read_fds = all_fds; - tv = backend_timeout(); - error = select(maxfd + 1, &read_fds, NULL, NULL, &tv); - if(error < 0){ - #ifndef _WIN32 - fprintf(stderr, "select failed: %s\n", strerror(errno)); - #else - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); - fprintf(stderr, "select failed: %s\n", error_message); - LocalFree(error_message); - error_message = NULL; - #endif - break; - } - - //find all signaled fds - n = 0; - for(u = 0; u < fds; u++){ - if(fd[u].fd >= 0 && FD_ISSET(fd[u].fd, &read_fds)){ - signaled_fds[n] = fd[u]; - n++; - } - } - - //update this iteration's timestamp - update_timestamp(); - - //run backend processing, collect events - DBGPF("%lu backend FDs signaled\n", n); - if(backends_handle(n, signaled_fds)){ - goto bail; - } - - while(primary->n){ - //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - if(primary != event_pool + u){ - secondary = primary; - primary = event_pool + u; - break; - } - } - - //push collected events to target backends - if(secondary->n && backends_notify(secondary->n, secondary->channel, secondary->value)){ - fprintf(stderr, "Backends failed to handle output\n"); - goto bail; - } + if(!fds){ + fprintf(stderr, "No descriptors registered for multiplexing\n"); + } - //reset the event count - secondary->n = 0; - } + //run the core loop + if(!core_loop()){ + rv = EXIT_SUCCESS; } - rv = EXIT_SUCCESS; bail: //free all data - free(signaled_fds); backends_stop(); channels_free(); instances_free(); -- cgit v1.2.3 From 377556e3407b107d6033be246ba6f41e0681f690 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:43:48 +0100 Subject: Simplify MIDI backend --- backends/midi.c | 61 ++++++++++++++++++++++++++------------------------------ backends/midi.md | 3 --- 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/backends/midi.c b/backends/midi.c index f378f1e..1f0f2d5 100644 --- a/backends/midi.c +++ b/backends/midi.c @@ -13,9 +13,7 @@ enum /*_midi_channel_type*/ { cc, pressure, aftertouch, - pitchbend, - nrpn, - sysmsg + pitchbend }; static struct { @@ -145,10 +143,6 @@ static channel* midi_channel(instance* inst, char* spec, uint8_t flags){ ident.fields.type = note; channel += 4; } - else if(!strncmp(channel, "nrpn", 4)){ - ident.fields.type = nrpn; - channel += 4; - } else if(!strncmp(channel, "pressure", 8)){ ident.fields.type = pressure; channel += 8; @@ -205,9 +199,6 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ case aftertouch: snd_seq_ev_set_chanpress(&ev, ident.fields.channel, v[u].normalised * 127.0); break; - case nrpn: - //FIXME set to nrpn output - break; } snd_seq_event_output(sequencer, &ev); @@ -217,6 +208,24 @@ static int midi_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } +static char* midi_type_name(uint8_t type){ + switch(type){ + case none: + return "none"; + case note: + return "note"; + case cc: + return "cc"; + case pressure: + return "pressure"; + case aftertouch: + return "aftertouch"; + case pitchbend: + return "pitch"; + } + return "unknown"; +} + static int midi_handle(size_t num, managed_fd* fds){ snd_seq_event_t* ev = NULL; instance* inst = NULL; @@ -234,59 +243,45 @@ static int midi_handle(size_t num, managed_fd* fds){ while(snd_seq_event_input(sequencer, &ev) > 0){ event_type = NULL; ident.label = 0; + + ident.fields.channel = ev->data.note.channel; + ident.fields.control = ev->data.note.note; + val.normalised = (double) ev->data.note.velocity / 127.0; + switch(ev->type){ case SND_SEQ_EVENT_NOTEON: case SND_SEQ_EVENT_NOTEOFF: case SND_SEQ_EVENT_NOTE: ident.fields.type = note; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; if(ev->type == SND_SEQ_EVENT_NOTEOFF){ val.normalised = 0; } - event_type = "note"; break; case SND_SEQ_EVENT_KEYPRESS: ident.fields.type = pressure; - ident.fields.channel = ev->data.note.channel; - ident.fields.control = ev->data.note.note; - val.normalised = (double)ev->data.note.velocity / 127.0; - event_type = "pressure"; break; case SND_SEQ_EVENT_CHANPRESS: ident.fields.type = aftertouch; ident.fields.channel = ev->data.control.channel; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "aftertouch"; + val.normalised = (double) ev->data.control.value / 127.0; break; case SND_SEQ_EVENT_PITCHBEND: ident.fields.type = pitchbend; ident.fields.channel = ev->data.control.channel; - val.normalised = ((double)ev->data.control.value + 8192) / 16383.0; - event_type = "pitch"; + val.normalised = ((double) ev->data.control.value + 8192) / 16383.0; break; case SND_SEQ_EVENT_CONTROLLER: ident.fields.type = cc; ident.fields.channel = ev->data.control.channel; ident.fields.control = ev->data.control.param; - val.raw.u64 = ev->data.control.value; - val.normalised = (double)ev->data.control.value / 127.0; - event_type = "cc"; - break; - case SND_SEQ_EVENT_CONTROL14: - case SND_SEQ_EVENT_NONREGPARAM: - case SND_SEQ_EVENT_REGPARAM: - //FIXME value calculation - ident.fields.type = nrpn; - ident.fields.channel = ev->data.control.channel; - ident.fields.control = ev->data.control.param; + val.normalised = (double) ev->data.control.value / 127.0; break; default: LOG("Ignored event of unsupported type"); continue; } + event_type = midi_type_name(ident.fields.type); inst = mm_instance_find(BACKEND_NAME, ev->dest.port); if(!inst){ //FIXME might want to return failure diff --git a/backends/midi.md b/backends/midi.md index 8188444..d3d6e33 100644 --- a/backends/midi.md +++ b/backends/midi.md @@ -30,7 +30,6 @@ The MIDI backend supports mapping different MIDI events to MIDIMonster channels. * `pressure` - Note pressure/aftertouch messages * `aftertouch` - Channel-wide aftertouch messages * `pitch` - Channel pitchbend messages -* `nrpn` - NRPNs (not yet implemented) A MIDIMonster channel is specified using the syntax `channel.`. The shorthand `ch` may be used instead of the word `channel` (Note that `channel` here refers to the MIDI channel number). @@ -57,7 +56,5 @@ Currently, no Note Off messages are sent (instead, Note On messages with a veloc generated, which amount to the same thing according to the spec). This may be implemented as a configuration option at a later time. -NRPNs are not yet fully implemented, though rudimentary support is in the codebase. - To see which events your MIDI devices output, ALSA provides the `aseqdump` utility. You can list all incoming events using `aseqdump -p `. -- cgit v1.2.3 From e48583db0e02336381a1626af814549bf12b1214 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:44:38 +0100 Subject: Check ArtNet/sACN channel direction at map time --- backends/artnet.c | 5 +++++ backends/artnet.h | 2 +- backends/sacn.c | 5 +++++ backends/sacn.h | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 9fac332..0a1ed11 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -164,6 +164,11 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->dest_len){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (missing destination)", inst->name, spec); + } + //secondary channel setup if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); diff --git a/backends/artnet.h b/backends/artnet.h index d83999d..ac69b3d 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -20,7 +20,7 @@ static int artnet_shutdown(size_t n, instance** inst); #define ARTNET_KEEPALIVE_INTERVAL 1000 //limit transmit rate to at most 44 packets per second (1000/44 ~= 22) -#define ARTNET_FRAME_TIMEOUT 15 +#define ARTNET_FRAME_TIMEOUT 20 #define ARTNET_SYNTHESIZE_MARGIN 10 #define MAP_COARSE 0x0200 diff --git a/backends/sacn.c b/backends/sacn.c index 79ffb46..9c7f890 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -231,6 +231,11 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } chan_a--; + //check output capabilities + if((flags & mmchannel_output) && !data->xmit_prio){ + LOGPF("Channel %s.%s mapped for output, but instance is not configured for output (no priority set)", inst->name, spec); + } + //if wide channel, mark fine if(*spec_next == '+'){ chan_b = strtoul(spec_next + 1, NULL, 10); diff --git a/backends/sacn.h b/backends/sacn.h index 4642e59..f2cd49f 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -16,7 +16,7 @@ static int sacn_shutdown(size_t n, instance** inst); //spec 6.6.2.1 #define SACN_KEEPALIVE_INTERVAL 1000 //spec 6.6.1 -#define SACN_FRAME_TIMEOUT 15 +#define SACN_FRAME_TIMEOUT 20 #define SACN_SYNTHESIZE_MARGIN 10 #define SACN_DISCOVERY_TIMEOUT 9000 #define SACN_PDU_MAGIC "ASC-E1.17\0\0\0" -- cgit v1.2.3 From b09f93ea146812c9a3a3a15cc7654868f9c5a468 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 20:45:37 +0100 Subject: Restructure & simplify winmidi --- backends/winmidi.c | 57 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/backends/winmidi.c b/backends/winmidi.c index ad9b02d..d9b3047 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -117,7 +117,7 @@ static channel* winmidi_channel(instance* inst, char* spec, uint8_t flags){ next_token = spec + 7; } } - + if(!next_token){ LOGPF("Invalid channel specification %s", spec); return NULL; @@ -213,7 +213,7 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v midiOutShortMsg(data->device_out, output.dword); } - + return 0; } @@ -310,7 +310,7 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW ident.fields.type = input.components.status & 0xF0; ident.fields.control = input.components.data1; val.normalised = (double) input.components.data2 / 127.0; - + if(ident.fields.type == 0x80){ ident.fields.type = note; val.normalised = 0; @@ -335,7 +335,6 @@ static void CALLBACK winmidi_input_callback(HMIDIIN device, unsigned message, DW case MIM_CLOSE: //device opened/closed return; - } DBGPF("Incoming message type %d channel %d control %d value %f", @@ -435,32 +434,22 @@ static int winmidi_match_output(char* prefix){ return -1; } -static int winmidi_start(size_t n, instance** inst){ - size_t p; - int device, rv = -1; - winmidi_instance_data* data = NULL; +static int winmidi_socket_pair(int* fds){ + //this really should be a size_t but getsockname specifies int* for some reason + int sockadd_len = sizeof(struct sockaddr_storage); + char* error = NULL; struct sockaddr_storage sockadd = { 0 }; - //this really should be a size_t but getsockname specifies int* for some reason - int sockadd_len = sizeof(sockadd); - char* error = NULL; - DBGPF("Main thread ID is %ld", GetCurrentThreadId()); - - //output device list if requested - if(backend_config.list_devices){ - winmidi_match_input(NULL); - winmidi_match_output(NULL); - } - //open the feedback sockets //for some reason the feedback connection fails to work on 'real' windows with ipv6 - backend_config.socket_pair[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); - if(backend_config.socket_pair[0] < 0){ + fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); + if(fds[0] < 0){ LOG("Failed to open feedback socket"); return 1; } - if(getsockname(backend_config.socket_pair[0], (struct sockaddr*) &sockadd, &sockadd_len)){ + + if(getsockname(fds[0], (struct sockaddr*) &sockadd, &sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to query feedback socket information: %s", error); @@ -483,8 +472,8 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } DBGPF("Feedback socket family %d port %d", sockadd.ss_family, be16toh(((struct sockaddr_in*)&sockadd)->sin_port)); - backend_config.socket_pair[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); - if(backend_config.socket_pair[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ + fds[1] = socket(sockadd.ss_family, SOCK_DGRAM, IPPROTO_UDP); + if(fds[1] < 0 || connect(backend_config.socket_pair[1], (struct sockaddr*) &sockadd, sockadd_len)){ FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); LOGPF("Failed to connect to feedback socket: %s", error); @@ -492,6 +481,26 @@ static int winmidi_start(size_t n, instance** inst){ return 1; } + return 0; +} + +static int winmidi_start(size_t n, instance** inst){ + size_t p; + int device, rv = -1; + winmidi_instance_data* data = NULL; + DBGPF("Main thread ID is %ld", GetCurrentThreadId()); + + //output device list if requested + if(backend_config.list_devices){ + winmidi_match_input(NULL); + winmidi_match_output(NULL); + } + + //open the feedback sockets + if(winmidi_socket_pair(backend_config.socket_pair)){ + return 1; + } + //set up instances and start input for(p = 0; p < n; p++){ data = (winmidi_instance_data*) inst[p]->impl; -- cgit v1.2.3 From 04494e4543150567a461dd478ce9ccdc067131b2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 21:57:47 +0100 Subject: Move ArtNet and sACN to local channel stores --- backends/artnet.c | 29 ++++++++++++++++++----------- backends/artnet.h | 1 + backends/sacn.c | 18 +++++++++++++----- backends/sacn.h | 1 + 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 0a1ed11..5d26b31 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -104,12 +104,18 @@ static int artnet_configure(char* option, char* value){ static int artnet_instance(instance* inst){ artnet_instance_data* data = calloc(1, sizeof(artnet_instance_data)); + size_t u; + if(!data){ LOG("Failed to allocate memory"); return 1; } data->net = default_net; + for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ + data->data.channel[u].ident = u; + data->data.channel[u].instance = inst; + } inst->impl = data; return 0; @@ -197,7 +203,7 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int artnet_transmit(instance* inst){ @@ -233,7 +239,7 @@ static int artnet_transmit(instance* inst){ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v){ uint32_t frame_delta = 0; - size_t u, mark = 0; + size_t u, mark = 0, channel_offset = 0; artnet_instance_data* data = (artnet_instance_data*) inst->impl; if(!data->dest_len){ @@ -242,22 +248,23 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } for(u = 0; u < num; u++){ - if(IS_WIDE(data->data.map[c[u]->ident])){ + channel_offset = c[u]->ident; + if(IS_WIDE(data->data.map[channel_offset])){ uint32_t val = v[u].normalised * ((double) 0xFFFF); //the primary (coarse) channel is the one registered to the core, so we don't have to check for that - if(data->data.out[c[u]->ident] != ((val >> 8) & 0xFF)){ + if(data->data.out[channel_offset] != ((val >> 8) & 0xFF)){ mark = 1; - data->data.out[c[u]->ident] = (val >> 8) & 0xFF; + data->data.out[channel_offset] = (val >> 8) & 0xFF; } - if(data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] != (val & 0xFF)){ + if(data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] != (val & 0xFF)){ mark = 1; - data->data.out[MAPPED_CHANNEL(data->data.map[c[u]->ident])] = val & 0xFF; + data->data.out[MAPPED_CHANNEL(data->data.map[channel_offset])] = val & 0xFF; } } - else if(data->data.out[c[u]->ident] != (v[u].normalised * 255.0)){ + else if(data->data.out[channel_offset] != (v[u].normalised * 255.0)){ mark = 1; - data->data.out[c[u]->ident] = v[u].normalised * 255.0; + data->data.out[channel_offset] = v[u].normalised * 255.0; } } @@ -310,10 +317,10 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ if(data->data.map[p] & MAP_MARK){ data->data.map[p] &= ~MAP_MARK; if(data->data.map[p] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(data->data.map[p]), 0); + chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } else{ - chan = mm_channel(inst, p, 0); + chan = data->data.channel + p; } if(!chan){ diff --git a/backends/artnet.h b/backends/artnet.h index ac69b3d..a517aa0 100644 --- a/backends/artnet.h +++ b/backends/artnet.h @@ -37,6 +37,7 @@ typedef struct /*_artnet_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } artnet_universe; typedef struct /*_artnet_instance_model*/ { diff --git a/backends/sacn.c b/backends/sacn.c index 9c7f890..dd05dc7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -209,12 +209,20 @@ static int sacn_configure_instance(instance* inst, char* option, char* value){ } static int sacn_instance(instance* inst){ - inst->impl = calloc(1, sizeof(sacn_instance_data)); - if(!inst->impl){ + sacn_instance_data* data = calloc(1, sizeof(sacn_instance_data)); + size_t u; + + if(!data){ LOG("Failed to allocate memory"); return 1; } + for(u = 0; u < sizeof(data->data.channel) / sizeof(channel); u++){ + data->data.channel[u].ident = u; + data->data.channel[u].instance = inst; + } + + inst->impl = data; return 0; } @@ -264,7 +272,7 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ } data->data.map[chan_a] = (*spec_next == '+') ? (MAP_COARSE | chan_b) : (MAP_SINGLE | chan_a); - return mm_channel(inst, chan_a, 1); + return data->data.channel + chan_a; } static int sacn_transmit(instance* inst){ @@ -424,10 +432,10 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame //unmark and get channel inst_data->data.map[u] &= ~MAP_MARK; if(inst_data->data.map[u] & MAP_FINE){ - chan = mm_channel(inst, MAPPED_CHANNEL(inst_data->data.map[u]), 0); + chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } else{ - chan = mm_channel(inst, u, 0); + chan = inst_data->data.channel + u; } if(!chan){ diff --git a/backends/sacn.h b/backends/sacn.h index f2cd49f..4138f45 100644 --- a/backends/sacn.h +++ b/backends/sacn.h @@ -36,6 +36,7 @@ typedef struct /*_sacn_universe_model*/ { uint8_t in[512]; uint8_t out[512]; uint16_t map[512]; + channel channel[512]; } sacn_universe; typedef struct /*_sacn_instance_model*/ { -- cgit v1.2.3 From c15ac89a33b6f6f51ac0c3da7a86dc43c8a9321a Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 10 Mar 2020 22:31:17 +0100 Subject: Fix Coverity CID 354930 & CID 354929 --- midimonster.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/midimonster.c b/midimonster.c index 3212f7f..2b0b7a0 100644 --- a/midimonster.c +++ b/midimonster.c @@ -377,7 +377,7 @@ static int core_loop(){ //process events while(!shutdown_requested){ //rebuild fd set if necessary - if(fd_set_dirty){ + if(fd_set_dirty || !signaled_fds){ all_fds = fds_collect(&maxfd); signaled_fds = realloc(signaled_fds, fds * sizeof(managed_fd)); if(!signaled_fds){ @@ -415,7 +415,7 @@ static int core_loop(){ #else ts.tv_sec = tv.tv_sec; ts.tv_nsec = tv.tv_usec * 1000; - error = nanosleep(&ts, NULL); + nanosleep(&ts, NULL); #endif } -- cgit v1.2.3 From 20803bd7d7578706ad55b56cf1a4d8865b5c1285 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Mar 2020 20:44:24 +0100 Subject: Restructure CI spellchecking --- .travis-ci.sh | 80 +++++++++++++++++++++-------------------------------------- .travis.yml | 46 +++++++++++----------------------- 2 files changed, 43 insertions(+), 83 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 8008026..e9e3df3 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -1,66 +1,42 @@ #!/bin/bash -# This script is triggered from the script section of .travis.yml -# It runs the appropriate commands depending on the task requested. +if [ "$TASK" = "spellcheck" ]; then + result=0 + # Create list of files to be spellchecked + spellcheck_files=$(find -type f | grep -v ".git/") -set -e + # Run spellintian to find spelling errors + sl_results=$(spellintian $spellcheck_files 2>&1) -SPELLINGBLACKLIST=$(cat <<-BLACKLIST --wholename "./.git/*" -BLACKLIST -) + sl_errors=$(wc -l <<< "$sl_results") + sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") + sl_errors_nodups=$((grep -v "\(duplicate word\)" | wc -l) <<< "$sl_results") -if [[ $TASK = 'spellintian' ]]; then - # run spellintian only if it is the requested task, ignoring duplicate words - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of spellintian errors, ignoring duplicate words - spellingerrors=$(zrun spellintian $spellingfiles 2>&1 | grep -v "\(duplicate word\)" | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun spellintian $spellingfiles | grep -v "\(duplicate word\)" - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - exit 1; + if [ "$sl_errors" -ne 0 ]; then + printf "Spellintian found %s errors (%s spelling, %s duplicate words):\n\n" "$sl_errors" "$sl_errors_nodups" "$sl_errors_dups" + printf "%s\n\n" "$sl_results" + result=1 else - echo "Found $spellingerrors spelling errors via spellintian, ignoring duplicates" - fi; -elif [[ $TASK = 'spellintian-duplicates' ]]; then - # run spellintian only if it is the requested task - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of spellintian errors - spellingerrors=$(zrun spellintian $spellingfiles 2>&1 | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun spellintian $spellingfiles - echo "Found $spellingerrors spelling errors via spellintian" - exit 1; - else - echo "Found $spellingerrors spelling errors via spellintian" - fi; -elif [[ $TASK = 'codespell' ]]; then - # run codespell only if it is the requested task - spellingfiles=$(eval "find ./ -type f -and ! \( \ - $SPELLINGBLACKLIST \ - \) | xargs") - # count the number of codespell errors - spellingerrors=$(zrun codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellingfiles 2>&1 | wc -l) - if [[ $spellingerrors -ne 0 ]]; then - # print the output for info - zrun codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellingfiles - echo "Found $spellingerrors spelling errors via codespell" - exit 1; + printf "Spellintian reports no errors\n" + fi + + # Run codespell to find some more + cs_results=$(codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellcheck_files 2>&1) + cs_errors=$(wc -l <<< "$cs_results") + if [ "$cs_errors" -ne 0 ]; then + printf "Codespell found %s errors:\n\n" "$cs_errors" + printf "%s\n\n" "$cs_results" + result=1 else - echo "Found $spellingerrors spelling errors via codespell" - fi; -elif [[ $TASK = 'sanitize' ]]; then + printf "Codespell reports no errors\n" + fi + exit "$result" +elif [ "$TASK" = 'sanitize' ]; then # Run sanitized compile travis_fold start "make_sanitize" make sanitize; travis_fold end "make_sanitize" -elif [[ $TASK = 'windows' ]]; then +elif [ "$TASK" = 'windows' ]; then travis_fold start "make_windows" make windows; make -C backends lua.dll diff --git a/.travis.yml b/.travis.yml index b9b6969..48b4b71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,8 +12,6 @@ script: addons: apt: packages: &base_build - # This is the absolute minimum for configure to pass - # Non C++ based tasks use it so they can run make builtfiles - ccache packages: &core_build # This is all the bits we need to enable all options @@ -36,6 +34,11 @@ addons: packages: &core_build_windows - *core_build - mingw-w64 + packages: &linters + - lintian + - codespell + - shellcheck + - cloc jobs: fast_finish: true @@ -74,30 +77,6 @@ jobs: apt: packages: - *core_build_clang_latest - - os: linux - dist: bionic - env: TASK='spellintian' - addons: - apt: - packages: - - *core_build - - moreutils - - os: linux - dist: bionic - env: TASK='spellintian-duplicates' - addons: - apt: - packages: - - *core_build - - moreutils - - os: linux - dist: bionic - env: TASK='codespell' - addons: - apt: - packages: - - *core_build - - moreutils - os: osx osx_image: xcode10.2 compiler: clang @@ -116,10 +95,18 @@ jobs: allow_failures: - os: linux dist: bionic - env: TASK='spellintian-duplicates' + env: TASK='codesmell' + addons: + apt: + packages: + - *linters - os: linux dist: bionic - env: TASK='codespell' + env: TASK='spellcheck' + addons: + apt: + packages: + - *linters env: global: @@ -136,9 +123,6 @@ cache: before_cache: - ccache -s # see how many hits ccache got -install: - - if [ "$TASK" = "codespell" ]; then pip install --user git+https://github.com/codespell-project/codespell.git; fi - before_install: # Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them - git pull --tags -- cgit v1.2.3 From 678cc465124ad81dcec47c44cc30827e5246bd3b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Mar 2020 20:50:29 +0100 Subject: Simplify ArtNet & sACN --- backends/artnet.c | 4 +--- backends/artnet.md | 2 ++ backends/sacn.c | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 5d26b31..7d5d9ee 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -316,12 +316,10 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ for(p = 0; p <= max_mark; p++){ if(data->data.map[p] & MAP_MARK){ data->data.map[p] &= ~MAP_MARK; + chan = data->data.channel + p; if(data->data.map[p] & MAP_FINE){ chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } - else{ - chan = data->data.channel + p; - } if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); diff --git a/backends/artnet.md b/backends/artnet.md index 7e1ecff..383203d 100644 --- a/backends/artnet.md +++ b/backends/artnet.md @@ -3,6 +3,8 @@ The ArtNet backend provides read-write access to the UDP-based ArtNet protocol for lighting fixture control. +Art-Net™ Designed by and Copyright Artistic Licence Holdings Ltd. + #### Global configuration | Option | Example value | Default value | Description | diff --git a/backends/sacn.c b/backends/sacn.c index dd05dc7..c9be8ff 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -431,12 +431,10 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame if(inst_data->data.map[u] & MAP_MARK){ //unmark and get channel inst_data->data.map[u] &= ~MAP_MARK; + chan = inst_data->data.channel + u; if(inst_data->data.map[u] & MAP_FINE){ chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } - else{ - chan = inst_data->data.channel + u; - } if(!chan){ LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); -- cgit v1.2.3 From abb1ffd367f4046d77ac62b0b017b407997bc43b Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Mar 2020 21:53:14 +0100 Subject: Add codesmell statistics to CI --- .travis-ci.sh | 34 +++++++++++++++++++++++++++++++--- .travis.yml | 12 ++++++++++-- midimonster.h | 6 ++++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index e9e3df3..763e558 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -21,7 +21,7 @@ if [ "$TASK" = "spellcheck" ]; then fi # Run codespell to find some more - cs_results=$(codespell --check-filenames --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellcheck_files 2>&1) + cs_results=$(codespell --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellcheck_files 2>&1) cs_errors=$(wc -l <<< "$cs_results") if [ "$cs_errors" -ne 0 ]; then printf "Codespell found %s errors:\n\n" "$cs_errors" @@ -31,12 +31,40 @@ if [ "$TASK" = "spellcheck" ]; then printf "Codespell reports no errors\n" fi exit "$result" -elif [ "$TASK" = 'sanitize' ]; then +elif [ "$TASK" = "codesmell" ]; then + result=0 + + if [ -z "$(which lizard)" ]; then + printf "Installing lizard...\n" + pip3 install lizard + fi + + # Run shellcheck for all shell scripts + printf "Running shellcheck...\n" + shell_files="$(find . -type f -iname \*.sh)" + xargs shellcheck -Cnever -s bash <<< "$shell_files" + if [ "$?" -ne "0" ]; then + result=1 + fi + + # Run cloc for some stats + printf "Code statistics:\n\n" + cloc ./ + + # Run lizard for the project + printf "Running lizard for code complexity analysis\n" + lizard ./ + if [ "$?" -ne "0" ]; then + result=1 + fi + + exit "$result" +elif [ "$TASK" = "sanitize" ]; then # Run sanitized compile travis_fold start "make_sanitize" make sanitize; travis_fold end "make_sanitize" -elif [ "$TASK" = 'windows' ]; then +elif [ "$TASK" = "windows" ]; then travis_fold start "make_windows" make windows; make -C backends lua.dll diff --git a/.travis.yml b/.travis.yml index 48b4b71..a5de2f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ before_script: - export -f travis_fold script: - - "bash -ex .travis-ci.sh" + - "bash .travis-ci.sh" addons: apt: @@ -35,6 +35,8 @@ addons: - *core_build - mingw-w64 packages: &linters + - python3 + - python3-pip - lintian - codespell - shellcheck @@ -92,7 +94,6 @@ jobs: compiler: clang env: - TASK='sanitize' - allow_failures: - os: linux dist: bionic env: TASK='codesmell' @@ -107,6 +108,13 @@ jobs: apt: packages: - *linters + allow_failures: + - os: linux + dist: bionic + env: TASK='codesmell' + - os: linux + dist: bionic + env: TASK='spellcheck' env: global: diff --git a/midimonster.h b/midimonster.h index bad83c7..dc0e255 100644 --- a/midimonster.h +++ b/midimonster.h @@ -234,10 +234,12 @@ MM_API instance* mm_instance_find(char* backend, uint64_t ident); * matching `ident`, a pointer to it is returned. * This API is just a convenience function. The array of channels is * only used for mapping internally, creating and managing your own - * channel store is possible. + * channel store is possible. When returning pointers from a + * backend-local channel store, the returned pointers must stay + * valid over the lifetime of the instance. * For each channel with a non-NULL `impl` field registered using * this function, the backend will receive a call to its channel_free - * function. + * function (if it exists). */ MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); -- cgit v1.2.3 From 9ec65119e11941d81a641f6387cdb542b4e8070e Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 11 Mar 2020 22:23:55 +0100 Subject: Refactor installer --- .travis-ci.sh | 8 +- installer.sh | 356 +++++++++++++++++++++++++--------------------------------- 2 files changed, 160 insertions(+), 204 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 763e558..3b7d7f5 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -3,10 +3,10 @@ if [ "$TASK" = "spellcheck" ]; then result=0 # Create list of files to be spellchecked - spellcheck_files=$(find -type f | grep -v ".git/") + spellcheck_files=$(find . -type f | grep -v ".git/") # Run spellintian to find spelling errors - sl_results=$(spellintian $spellcheck_files 2>&1) + sl_results=$(xargs spellintian 2>&1 <<< "$spellcheck_files") sl_errors=$(wc -l <<< "$sl_results") sl_errors_dups=$((grep "\(duplicate word\)" | wc -l) <<< "$sl_results") @@ -21,7 +21,7 @@ if [ "$TASK" = "spellcheck" ]; then fi # Run codespell to find some more - cs_results=$(codespell --check-hidden --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" $spellcheck_files 2>&1) + cs_results=$(xargs codespell --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" <<< "$spellcheck_files" 2>&1) cs_errors=$(wc -l <<< "$cs_results") if [ "$cs_errors" -ne 0 ]; then printf "Codespell found %s errors:\n\n" "$cs_errors" @@ -100,7 +100,7 @@ else cp ./backends/*.md ./deployment/docs/ cp -r ./configs ./deployment/ cd ./deployment - tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" * + tar czf "midimonster-$(git describe)-$TRAVIS_OS_NAME.tgz" ./ find . ! -iname '*.tgz' -delete travis_fold end "deploy_unix" fi diff --git a/installer.sh b/installer.sh index f8fad18..b2ca958 100755 --- a/installer.sh +++ b/installer.sh @@ -1,16 +1,30 @@ #!/bin/bash ################################################ SETUP ################################################ -deps=(libasound2-dev libevdev-dev liblua5.3-dev libjack-jackd2-dev pkg-config libssl-dev gcc make wget git) -user=$(whoami) # for bypassing user check replace "$(whoami)" with "root". - -tmp_path=$(mktemp -d) # Repo download path -updater_dir=/etc/midimonster-updater # Updater download + config path -updater_file=$updater_dir/updater.conf - -latest_version=$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') - -makeargs=all # Build args +deps=( + libasound2-dev + libevdev-dev + liblua5.3-dev + libjack-jackd2-dev + pkg-config + libssl-dev + python3-dev + gcc + make + wget + git +) +# Replace this with 'root' to bypass the user check +user="$(whoami)" +# Temporary directory used for repository clone +tmp_path="$(mktemp -d)" +# Installer/updater install directory +updater_dir="/etc/midimonster-updater" + +latest_version="$(curl --silent "https://api.github.com/repos/cbdevnet/midimonster/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')" + +# make invocation arguments +makeargs="all" normal=$(tput sgr0) dim=$(tput dim) @@ -20,66 +34,56 @@ c_red=$(tput setaf 1) c_green=$(tput setaf 2) c_mag=$(tput setaf 5) -VAR_DESTDIR="" # Unused -VAR_PREFIX="/usr" -VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" # Reassigned in func. ARGS to update "$VAR_PREFIX" when an argument is set. -VAR_DEFAULT_CFG="/etc/midimonster/midimonster.cfg" -VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" # Reassigned in func. ARGS to update "$VAR_PREFIX" when an argument is set. - - -################################################ SETUP ################################################ +DEFAULT_PREFIX="/usr" +DEFAULT_PLUGINPATH="/lib/midimonster" +DEFAULT_CFGPATH="/etc/midimonster/midimonster.cfg" +DEFAULT_EXAMPLES="/share/midimonster" ############################################## FUNCTIONS ############################################## +assign_defaults(){ + VAR_PREFIX="${VAR_PREFIX:-$DEFAULT_PREFIX}" + VAR_PLUGINS="${VAR_PLUGINS:-$VAR_PREFIX$DEFAULT_PLUGINPATH}" + VAR_DEFAULT_CFG="${VAR_DEFAULT_CFG:-$DEFAULT_CFGPATH}" + VAR_EXAMPLE_CFGS="${VAR_EXAMPLE_CFGS:-$VAR_PREFIX$DEFAULT_EXAMPLES}" +} -ARGS () { - for i in "$@" - do +ARGS(){ + for i in "$@"; do case $i in --prefix=*) VAR_PREFIX="${i#*=}" - VAR_PREFIX_I="1" - VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" - VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" - VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" ;; --plugins=*) VAR_PLUGINS="${i#*=}" - VAR_PLUGINS_I="1" ;; --defcfg=*) VAR_DEFAULT_CFG="${i#*=}" - VAR_DEFAULT_CFG_I="1" ;; --examples=*) VAR_EXAMPLE_CFGS="${i#*=}" - VAR_EXAMPLE_CFGS_I="1" ;; --dev) NIGHTLY=1 - NIGHTLY_I="1" ;; -d|--default) - VAR_PREFIX_I="1" - VAR_PLUGINS_I="1" - VAR_DEFAULT_CFG_I="1" - VAR_EXAMPLE_CFGS_I="1" - NIGHTLY_I="1" - NIGHTLY=1 + assign_defaults ;; -fu|--forceupdate) UPDATER_FORCE="1" - ;; - -h|--help|*) # Help messages + ;; + -h|--help|*) + assign_defaults printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" - printf "\n ${c_green}--prefix${normal} ${c_red}${normal} Sets the prefix ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_PREFIX" - printf "\n ${c_green}--plugins${normal} ${c_red}${normal} Sets the plugin path ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_PLUGINS" - printf "\n ${c_green}--defcfg${normal} ${c_red}${normal} Sets the config path ${c_mag}Default:${normal} ${dim}%s ${normal}" "$VAR_DEFAULT_CFG" - printf "\n ${c_green}--examples${normal} ${c_red}${normal} Sets the example configs path ${c_mag}Default:${normal} ${dim}%s ${normal}\n" "$VAR_EXAMPLE_CFGS" - printf "\n ${c_green}--dev${normal} Install nightly version" - printf "\n ${c_green}-d, --default${normal} Use default values to install" - printf "\n ${c_green}-fu, --forceupdate${normal} Force the updater to update without a version check" - printf "\n ${c_green}-h, --help${normal} Show this message" - printf "\n ${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" + printf "\n\t${c_green}--prefix${normal} ${c_red}${normal}\t\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green}--plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green}--defcfg${normal} ${c_red}${normal}\t\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green}--examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green}--dev${normal}\t\t\tInstall nightly version" + printf "\n\t${c_green}-d, --default${normal}\t\tUse default values to install" + printf "\n\t${c_green}-h, --help${normal}\t\tShow this message" + printf "\n\t${c_green}-fu, --forceupdate${normal}\tForce the updater to update without a version check" + printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" + rmdir "$tmp_path" exit 1 ;; esac @@ -87,21 +91,49 @@ ARGS () { done } -INSTALL-DEPS () { # Install deps from array "$deps" - for t in ${deps[@]}; do - if [ $(dpkg-query -W -f='${Status}' $t 2>/dev/null | grep -c "ok installed") -eq 0 ]; then - printf "Installing %s\n" "$t" - apt-get install $t; - printf "Done.\n"; - else - printf "%s already installed!\n" "$t" - fi +# Install unmatched dependencies +install_dependencies(){ + for dependency in ${deps[@]}; do + if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then + printf "Installing %s\n" "$dependency" + apt-get install $dependency + else + printf "%s already installed!\n" "$dependency" + fi done printf "\n" } -NIGHTLY_CHECK () { # Asks for nightly version - if [ -z "$NIGHTLY_I" ]; then +ask_questions(){ + printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" + if [ -z "$VAR_PREFIX" ]; then + read -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input + VAR_PREFIX="${input:-$VAR_PREFIX}" + fi + + if [ -z "$VAR_PLUGINS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_PLUGINPATH" -p "PLUGINS (Plugin directory): " input + VAR_PLUGINS="${input:-$VAR_PLUGINS}" + fi + + if [ -z "$VAR_DEFAULT_CFG" ]; then + read -e -i "$DEFAULT_CFGPATH" -p "Default config path: " input + VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" + fi + + if [ -z "$VAR_EXAMPLE_CFGS" ]; then + read -e -i "$VAR_PREFIX$DEFAULT_EXAMPLES" -p "Example config directory: " input + VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" + fi +} + +# Clone the repository and select the correct version +prepare_repo(){ + printf "Cloning the repository\n" + git clone "https://github.com/cbdevnet/midimonster.git" "$tmp_path" + + # If not set via argument, ask whether to install development build + if [ -z "$NIGHTLY" ]; then read -p "Do you want to install the latest development version? (y/n)? " magic case "$magic" in y|Y) @@ -119,182 +151,106 @@ NIGHTLY_CHECK () { # Asks for nightly version esac fi - # Roll back to last tag if we're not on a nightly build + # Roll back to last tag if a stable version was requested if [ "$NIGHTLY" != 1 ]; then cd "$tmp_path" printf "Finding latest stable version...\n" - Iversion=$(git describe --abbrev=0) - printf "Starting Git checkout to %s...\n" "$Iversion" - git checkout -f -q $Iversion + last_tag=$(git describe --abbrev=0) + printf "Checking out %s...\n" "$last_tag" + git checkout -f -q $last_tag fi } -INSTALL-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - printf "\n" - ) - NIGHTLY_CHECK - printf "Preparation done.\n\n" - printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" - if [ -z "$VAR_PREFIX_I" ]; then - read -e -i "$VAR_PREFIX" -p "PREFIX (Install root directory): " input # Reads VAR_PREFIX then update containing vars - VAR_PREFIX="${input:-$VAR_PREFIX}" - VAR_PLUGINS="$VAR_PREFIX/lib/midimonster" # Update prefix-containing variables. - VAR_EXAMPLE_CFGS="$VAR_PREFIX/share/midimonster" # Update prefix-containing variables. - fi +# Build and install the software +build(){ + # Export variables for make + export PREFIX="$VAR_PREFIX" + export PLUGINS="$VAR_PLUGINS" + export DEFAULT_CFG="$VAR_DEFAULT_CFG" + export EXAMPLES="$VAR_EXAMPLE_CFGS" - if [ -z "$VAR_PLUGINS_I" ]; then - read -e -i "$VAR_PLUGINS" -p "PLUGINS (Plugin directory): " input # Reads VAR_PLUGINS - VAR_PLUGINS="${input:-$VAR_PLUGINS}" - fi - - if [ -z "$VAR_DEFAULT_CFG_I" ]; then - read -e -i "$VAR_DEFAULT_CFG" -p "Default config path: " input # Reads VAR_DEFAULT_CFG - VAR_DEFAULT_CFG="${input:-$VAR_DEFAULT_CFG}" - fi - - if [ -z "$VAR_EXAMPLE_CFGS_I" ]; then - read -e -i "$VAR_EXAMPLE_CFGS" -p "Example config directory: " input # Reads VAR_EXAMPLE_CFGS - VAR_EXAMPLE_CFGS="${input:-$VAR_EXAMPLE_CFGS}" - fi - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS -} - -UPDATER-PREP () { - ( - printf "Starting download...\n" - git clone https://github.com/cbdevnet/midimonster.git "$tmp_path" # Gets Midimonster - printf "\nInitializing repository...\n" - cd $tmp_path - git init $tmp_path - ) - NIGHTLY_CHECK - printf "Preparation done.\n\n" - rm -rf "$VAR_PLUGINS/" - - UPDATER_SAVE - - export PREFIX=$VAR_PREFIX - export PLUGINS=$VAR_PLUGINS - export DEFAULT_CFG=$VAR_DEFAULT_CFG - export DESTDIR=$VAR_DESTDIR - export EXAMPLES=$VAR_EXAMPLE_CFGS -} - -UPDATER () { - installed_version="$(midimonster --version)" - #installed_version="MIDIMonster v0.3-40-gafed325" # FOR TESTING ONLY! - if [[ "$installed_version" =~ "$latest_version" ]]; then - printf "Newest Version is already installed! ${bold}($installed_version)${normal}\n\n" - ERROR - else - printf "The installed Version ${bold}´$installed_version´${normal} equals not the newest stable version ${bold}´$latest_version´${normal} (Maybe you are running a development(NIGHTLY) version?)\n\n" - fi - - UPDATER-PREP - INSTALL-RUN - DONE -} - -INSTALL-RUN () { # Build cd "$tmp_path" make clean - make $makeargs + make "$makeargs" make install } -UPDATER_SAVE () { # Saves file for the auto updater in this script +# Save data for the updater +save_config(){ rm -rf $updater_dir - printf "Saving updater to %s/updater.sh\n" "$update_dir" + printf "Copying updater to %s/updater.sh\n" "$updater_dir" mkdir -p "$updater_dir" - wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O $updater_dir/updater.sh - printf "Creating symlink to updater in /usr/bin/midimonster-updater\n" + cp "$0" "$updater_dir/updater.sh" + printf "Creating symlin /usr/bin/midimonster-updater\n" ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" chmod +x "$updater_dir/updater.sh" - printf "Exporting updater config to %s\n" "$updater_file" - printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > $updater_file + printf "Exporting updater config\n" + printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > "$updater_dir/updater.conf" } -ERROR () { - printf "\nAborting...\n" - CLEAN - printf "Exiting...\n" +error_handler(){ + printf "\nAborting\n" exit 1 } -DONE () { - printf "\nDone.\n" - CLEAN - exit 0 -} - -CLEAN () { - printf "\nCleaning...\n" - rm -rf $tmp_path +cleanup() { + if [ -d "$tmp_path" ]; then + printf "Cleaning up temporary files...\n" + rm -rf "$tmp_path" + fi } -############################################## FUNCTIONS ############################################## - - ################################################ Main ################################################# -trap ERROR SIGINT SIGTERM SIGKILL -ARGS "$@" # Parse arguments +trap error_handler SIGINT SIGTERM +trap cleanup EXIT + +# Parse arguments +ARGS "$@" clear -# Check if $user = root! +# Check whether we have the privileges to install stuff if [ "$user" != "root" ]; then - printf "Installer must be run as root\n" - ERROR + printf "The installer requires root privileges to install the midimonster system-wide\n" + exit 1 fi -if [ $(wget -q --spider http://github.com) $? -eq 1 ]; then - printf "You need a connection to the internet\n" - ERROR +# Check if we can download the sources +if [ "$(wget -q --spider http://github.com)" ]; then + printf "The installer requires internet connectivity to download the midimonster sources\n" + exit 1 fi -# Forceupdate # Now only with default config because source imports all and overwrites the args.. [WIP!] -if [ "$UPDATER_FORCE" = "1" ]; then - printf "Forcing the updater to start...\n\n" - if [ -f $updater_file ]; then - . $updater_file - ARGS "$@" # Parse arguments again to compensate overwrite from source /\ - printf "Successfully imported settings from %s\n" "$updater_file" +# Check whether the updater needs to run +if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then + if [ -f "$updater_dir/updater.conf" ]; then + . "$updater_dir/updater.conf" + # Parse arguments again to compensate overwrite from source + ARGS "$@" + printf "Imported settings from %s/updater.conf\n" "$updater_dir" fi - UPDATER-PREP - INSTALL-RUN - DONE -fi - -# Check if updater config file exist and import it (overwrites default values!) -if [ -f $updater_file ]; then - printf "Starting updater...\n\n" - . $updater_file - ARGS "$@" # Parse arguments again to compensate overwrite from source /\ - printf "Successfully imported settings from %s\n" "$updater_file" - - # Check if binary $VAR_PREFIX/bin/midimonster exist. If yes start updater else skip. - if [ -x "$VAR_PREFIX/bin/midimonster" ]; then - UPDATER - else - printf "MIDIMonster binary not found, skipping updater.\nYou can force an update with --forceupdate\n" + if [ -n "$UPDATER_FORCE" ]; then + printf "Forcing the updater to start...\n\n" + elif [ -x "$VAR_PREFIX/bin/midimonster" ]; then + installed_version="$(midimonster --version)" + if [[ "$installed_version" =~ "$latest_version" ]]; then + printf "The installed version ${bold}$installed_version${normal} seems to be up to date\nDoing nothing\n\n" + exit 0 + else + printf "The installed version ${bold}$installed_version${normal} does not match the latest version ${bold}$latest_version${normal}\nMaybe you are running a development version?\n\n" + fi fi + + # Run updater steps + prepare_repo + build +else + # Run installer steps + install_dependencies + prepare_repo + ask_questions + save_config + build fi +exit 0 -#Normal install -INSTALL-DEPS -INSTALL-PREP -INSTALL-RUN -DONE -- cgit v1.2.3 From 9286bf43da5f6e87dfb20db50cf7c5ceaea925ba Mon Sep 17 00:00:00 2001 From: Spacelord Date: Wed, 11 Mar 2020 23:35:01 +0100 Subject: Installer: Fix name --- installer.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/installer.sh b/installer.sh index b2ca958..843e6bc 100755 --- a/installer.sh +++ b/installer.sh @@ -210,13 +210,13 @@ clear # Check whether we have the privileges to install stuff if [ "$user" != "root" ]; then - printf "The installer requires root privileges to install the midimonster system-wide\n" + printf "The installer/updater requires root privileges to install the midimonster system-wide\n" exit 1 fi # Check if we can download the sources if [ "$(wget -q --spider http://github.com)" ]; then - printf "The installer requires internet connectivity to download the midimonster sources\n" + printf "The installer/updater requires internet connectivity to download the midimonster sources\n" exit 1 fi -- cgit v1.2.3 From 813c0587d6add1df302a9b0c88fdff4e111e391e Mon Sep 17 00:00:00 2001 From: Spacelord Date: Thu, 12 Mar 2020 13:58:43 +0100 Subject: Installer: Initial self update --- installer.sh | 82 +++++++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 17 deletions(-) diff --git a/installer.sh b/installer.sh index 843e6bc..56021c4 100755 --- a/installer.sh +++ b/installer.sh @@ -70,18 +70,38 @@ ARGS(){ ;; -fu|--forceupdate) UPDATER_FORCE="1" - ;; + ;; + -supd|--selfupdate) + NIGHTLY="" + self_update + rmdir "$tmp_path" + exit 0 + ;; + -insup|--installupdater) + NIGHTLY="" + install_updater + rmdir "$tmp_path" + exit 0 + ;; + -insdep|--installdeps) + install_dependencies + rmdir "$tmp_path" + exit 0 + ;; -h|--help|*) assign_defaults printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" - printf "\n\t${c_green}--prefix${normal} ${c_red}${normal}\t\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" - printf "\n\t${c_green}--plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" - printf "\n\t${c_green}--defcfg${normal} ${c_red}${normal}\t\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" - printf "\n\t${c_green}--examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" - printf "\n\t${c_green}--dev${normal}\t\t\tInstall nightly version" - printf "\n\t${c_green}-d, --default${normal}\t\tUse default values to install" - printf "\n\t${c_green}-h, --help${normal}\t\tShow this message" - printf "\n\t${c_green}-fu, --forceupdate${normal}\tForce the updater to update without a version check" + printf "\n\t${c_green} --prefix${normal} ${c_red}${normal}\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green} --plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green} --defcfg${normal} ${c_red}${normal}\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green} --examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green} --dev${normal}\t\t\tInstall nightly version" + printf "\n ${c_green}-d,\t --default${normal}\t\tUse default values to install" + printf "\n ${c_green}-fu,\t --forceupdate${normal}\t\tForce the updater to update without a version check" + printf "\n ${c_green}-supd,\t --selfupdate${normal}\t\tUpdates this script to the newest version" + printf "\n ${c_green}-insup, --installupdater${normal}\tInstall the updater (Run with midimonster-updater)" + printf "\n ${c_green}-insdep, --installdeps${normal}\t\tInstall dependencies and exit" + printf "\n ${c_green}-h,\t --help${normal}\t\tShow this message and exit" printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" rmdir "$tmp_path" exit 1 @@ -105,7 +125,11 @@ install_dependencies(){ } ask_questions(){ - printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" + # Only say if necessary + if [ -n "$VAR_PREFIX" ] || [ -n "$VAR_PLUGINS" ] || [ -n "$VAR_DEFAULT_CFG" ] || [ -n "$VAR_EXAMPLE_CFGS" ]; then + printf "${bold}If you don't know what you're doing, just hit enter a few times.${normal}\n\n" + fi + if [ -z "$VAR_PREFIX" ]; then read -e -i "$DEFAULT_PREFIX" -p "PREFIX (Install root directory): " input VAR_PREFIX="${input:-$VAR_PREFIX}" @@ -177,17 +201,38 @@ build(){ # Save data for the updater save_config(){ - rm -rf $updater_dir - printf "Copying updater to %s/updater.sh\n" "$updater_dir" - mkdir -p "$updater_dir" - cp "$0" "$updater_dir/updater.sh" - printf "Creating symlin /usr/bin/midimonster-updater\n" - ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" - chmod +x "$updater_dir/updater.sh" + rm -f "$updater_dir/updater.conf" + mkdir -p "$updater_dir" printf "Exporting updater config\n" printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > "$updater_dir/updater.conf" } +# Updates this script either from the downloaded sources or the remote repository. +self_update(){ + mkdir -p "$updater_dir" + if [ "$NIGHTLY" = "1" ]; then + cp -u "$tmp_path/installer.sh" "$0" + elif [ "$NIGHTLY" != "1" ]; then + wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O "$0" + fi + chmod +x "$0" +} + +install_updater(){ + mkdir -p "$updater_dir" + if [ "$NIGHTLY" = "1" ]; then + printf "Copying updater to %s/updater.sh\n" "$updater_dir" + cp -u "$tmp_path/installer.sh" "$updater_dir/updater.sh" + else + printf "Downloading updater to %s/updater.sh\n" "$updater_dir" + wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O "$updater_dir/updater.sh" + fi + chmod +x "$updater_dir/updater.sh" + printf "Creating symlink /usr/bin/midimonster-updater\n" + ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" + printf "Done.\n" +} + error_handler(){ printf "\nAborting\n" exit 1 @@ -243,12 +288,15 @@ if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then # Run updater steps prepare_repo + install_updater + save_config build else # Run installer steps install_dependencies prepare_repo ask_questions + install_updater save_config build fi -- cgit v1.2.3 From 5087d4ebde3f9a1ec775928ec25f50099be3d8ad Mon Sep 17 00:00:00 2001 From: Spacelord Date: Thu, 12 Mar 2020 14:31:21 +0100 Subject: Add travis irc notification --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.travis.yml b/.travis.yml index a5de2f4..c3bcc12 100644 --- a/.travis.yml +++ b/.travis.yml @@ -154,6 +154,15 @@ before_install: # Download libraries to link with on Windows - if [ "$TASK" == "windows" ]; then mkdir libs; wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; mv lua53.dll libs; fi +notifications: + irc: + channels: + - "rc.hackint.org#midimonster" + on_success: change # default: always + on_failure: always # default: always + nick: MIDIMonster CI + use_notice: true + deploy: provider: releases file_glob: true -- cgit v1.2.3 From 378296f1b49c64f20c61966d2dcc8c8143181a07 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Thu, 12 Mar 2020 14:40:42 +0100 Subject: Fix typo --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c3bcc12..b9c2b9d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -157,7 +157,7 @@ before_install: notifications: irc: channels: - - "rc.hackint.org#midimonster" + - "irc.hackint.org#midimonster" on_success: change # default: always on_failure: always # default: always nick: MIDIMonster CI -- cgit v1.2.3 From 4b120a64f68fd7f36e8080981d68d0830d113205 Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 12 Mar 2020 21:51:43 +0100 Subject: Fix rate-limited frame synthesis --- .travis.yml | 2 +- backends/artnet.c | 4 ++-- backends/sacn.c | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index b9c2b9d..d7c25b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -160,7 +160,7 @@ notifications: - "irc.hackint.org#midimonster" on_success: change # default: always on_failure: always # default: always - nick: MIDIMonster CI + nick: mm_ci use_notice: true deploy: diff --git a/backends/artnet.c b/backends/artnet.c index 7d5d9ee..34fc82d 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -280,8 +280,8 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) //check output rate limit, request next frame if(frame_delta < ARTNET_FRAME_TIMEOUT){ artnet_fd[data->fd_index].output_instance[u].mark = 1; - if(!next_frame || next_frame > (ARTNET_KEEPALIVE_INTERVAL - frame_delta)){ - next_frame = (ARTNET_KEEPALIVE_INTERVAL - frame_delta); + if(!next_frame || next_frame > (ARTNET_FRAME_TIMEOUT - frame_delta)){ + next_frame = (ARTNET_FRAME_TIMEOUT - frame_delta); } return 0; } diff --git a/backends/sacn.c b/backends/sacn.c index c9be8ff..495bdf3 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -374,8 +374,8 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //check if ratelimiting engaged if(frame_delta < SACN_FRAME_TIMEOUT){ global_cfg.fd[data->fd_index].universe[u].mark = 1; - if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_KEEPALIVE_INTERVAL - frame_delta)){ - global_cfg.next_frame = (SACN_KEEPALIVE_INTERVAL - frame_delta); + if(!global_cfg.next_frame || global_cfg.next_frame > (SACN_FRAME_TIMEOUT - frame_delta)){ + global_cfg.next_frame = (SACN_FRAME_TIMEOUT - frame_delta); } return 0; } -- cgit v1.2.3 From 45945a5130f6f3910aa3924c1f1e5ddcf2ef3283 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Fri, 13 Mar 2020 23:04:15 +0100 Subject: Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aeaeb2a..12d87eb 100644 --- a/README.md +++ b/README.md @@ -172,7 +172,7 @@ chmod +x ./installer.sh ./installer.sh ``` -The installer can also be used for automating installations by specifying additional +The installer can also be used for automating installations or upgrades by specifying additional command line arguments. To see a list of valid arguments, run the installer with the `--help` argument. -- cgit v1.2.3 From 05cdf563fcc4c7835ec422fa5d7ee86b68a9f1df Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 14 Mar 2020 21:45:09 +0100 Subject: Restructure backend and instance registry --- TODO | 1 - backend.c | 268 +++++++++++++++++++++++++++++------------------------- backend.h | 4 +- backends/artnet.c | 5 - backends/sacn.c | 5 - config.c | 2 +- 6 files changed, 147 insertions(+), 138 deletions(-) diff --git a/TODO b/TODO index 1ea91f4..900cc1b 100644 --- a/TODO +++ b/TODO @@ -7,4 +7,3 @@ make event collectors threadsafe to stop marshalling data... collect & check backend API version windows strerror move all connection establishment to _start to be able to hot-stop/start all backends -only call _handle and _interval on backends with instances diff --git a/backend.c b/backend.c index ef8f66a..65cbd7d 100644 --- a/backend.c +++ b/backend.c @@ -8,10 +8,15 @@ #include "midimonster.h" #include "backend.h" -static size_t nbackends = 0; -static backend* backends = NULL; -static size_t ninstances = 0; -static instance** instances = NULL; +static struct { + size_t n; + backend* backends; + instance*** instances; +} registry = { + .n = 0 +}; + +//TODO move channel store into registry static size_t nchannels = 0; static channel** channels = NULL; @@ -20,11 +25,11 @@ int backends_handle(size_t nfds, managed_fd* fds){ int rv = 0; managed_fd xchg; - for(u = 0; u < nbackends && !rv; u++){ + for(u = 0; u < registry.n && !rv; u++){ n = 0; for(p = 0; p < nfds; p++){ - if(fds[p].backend == backends + u){ + if(fds[p].backend == registry.backends + u){ xchg = fds[n]; fds[n] = fds[p]; fds[p] = xchg; @@ -32,10 +37,13 @@ int backends_handle(size_t nfds, managed_fd* fds){ } } - DBGPF("Notifying backend %s of %lu waiting FDs\n", backends[u].name, n); - rv |= backends[u].process(n, fds); - if(rv){ - fprintf(stderr, "Backend %s failed to handle input\n", backends[u].name); + //handle if there is data ready or the backend has active instances for polling + if(n || registry.instances[u]){ + DBGPF("Notifying backend %s of %" PRIsize_t " waiting FDs\n", registry.backends[u].name, n); + rv |= registry.backends[u].process(n, fds); + if(rv){ + fprintf(stderr, "Backend %s failed to handle input\n", registry.backends[u].name); + } } } return rv; @@ -45,28 +53,29 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ size_t u, p, n; int rv = 0; channel_value xval; - channel* xchnl; - - //TODO eliminate duplicates - for(u = 0; u < ninstances && !rv; u++){ - n = 0; + channel* xchnl = NULL; - for(p = 0; p < nev; p++){ - if(c[p]->instance == instances[u]){ - xval = v[n]; - xchnl = c[n]; + for(u = 0; u < nev && !rv; u++){ + //sort for this instance + n = u + 1; + for(p = u + 1; p < nev; p++){ + if(c[p]->instance == c[u]->instance){ + xval = v[p]; + xchnl = c[p]; - v[n] = v[p]; - c[n] = c[p]; + v[p] = v[n]; + c[p] = c[n]; - v[p] = xval; - c[p] = xchnl; + v[n] = xval; + c[n] = xchnl; n++; } } - DBGPF("Calling handler for instance %s with %lu events\n", instances[u]->name, n); - rv |= instances[u]->backend->handle(instances[u], n, c, v); + //TODO eliminate duplicates + DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); + rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); + u = n; } return 0; @@ -76,17 +85,17 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ size_t u; for(u = 0; u < nchannels; u++){ if(channels[u]->instance == inst && channels[u]->ident == ident){ - DBGPF("Requested channel %lu on instance %s already exists, reusing\n", ident, inst->name); + DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing\n", ident, inst->name); return channels[u]; } } if(!create){ - DBGPF("Requested unknown channel %lu on instance %s\n", ident, inst->name); + DBGPF("Requested unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); return NULL; } - DBGPF("Creating previously unknown channel %lu on instance %s\n", ident, inst->name); + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); if(!new_chan){ fprintf(stderr, "Failed to allocate memory\n"); @@ -106,34 +115,46 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ return channels[nchannels++]; } -instance* mm_instance(){ - instance** new_inst = realloc(instances, (ninstances + 1) * sizeof(instance*)); - if(!new_inst){ - //TODO free - fprintf(stderr, "Failed to allocate memory\n"); - ninstances = 0; - return NULL; - } - instances = new_inst; - instances[ninstances] = calloc(1, sizeof(instance)); - if(!instances[ninstances]){ - fprintf(stderr, "Failed to allocate memory\n"); - return NULL; +instance* mm_instance(backend* b){ + size_t u = 0, n = 0; + + for(u = 0; u < registry.n; u++){ + if(registry.backends + u == b){ + //count existing instances + for(n = 0; registry.instances[u] && registry.instances[u][n]; n++){ + } + + //extend + registry.instances[u] = realloc(registry.instances[u], (n + 2) * sizeof(instance*)); + if(!registry.instances[u]){ + fprintf(stderr, "Failed to allocate memory\n"); + return NULL; + } + //sentinel + registry.instances[u][n + 1] = NULL; + registry.instances[u][n] = calloc(1, sizeof(instance)); + if(!registry.instances[u][n]){ + fprintf(stderr, "Failed to allocate memory\n"); + } + registry.instances[u][n]->backend = b; + return registry.instances[u][n]; + } } - return instances[ninstances++]; + //this should never happen + return NULL; } MM_API instance* mm_instance_find(char* name, uint64_t ident){ - size_t u; - backend* b = backend_match(name); - if(!b){ - return NULL; - } - - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b && instances[u]->ident == ident){ - return instances[u]; + size_t b = 0; + instance** iter = NULL; + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + for(iter = registry.instances[b]; iter && *iter; iter++){ + if((*iter)->ident == ident){ + return *iter; + } + } } } @@ -141,55 +162,40 @@ MM_API instance* mm_instance_find(char* name, uint64_t ident){ } MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ - backend* b = backend_match(name); - size_t n = 0, u; - //count number of affected instances - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - n++; - } + size_t b = 0, i = 0; + if(!ninst || !inst){ + return 1; } - *ninst = n; + for(b = 0; b < registry.n; b++){ + if(!strcmp(registry.backends[b].name, name)){ + //count instances + for(i = 0; registry.instances[b] && registry.instances[b][i]; i++){ + } - if(!n){ - *inst = NULL; - return 0; - } + *ninst = i; + if(!i){ + *inst = NULL; + return 0; + } - *inst = calloc(n, sizeof(instance*)); - if(!*inst){ - fprintf(stderr, "Failed to allocate memory\n"); - return 1; - } + *inst = calloc(i, sizeof(instance*)); + if(!*inst){ + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } - n = 0; - for(u = 0; u < ninstances; u++){ - if(instances[u]->backend == b){ - (*inst)[n] = instances[u]; - n++; + memcpy(*inst, registry.instances[b], i * sizeof(instance*)); + return 0; } } - return 0; -} - -void instances_free(){ - size_t u; - for(u = 0; u < ninstances; u++){ - free(instances[u]->name); - instances[u]->name = NULL; - instances[u]->backend = NULL; - free(instances[u]); - instances[u] = NULL; - } - free(instances); - ninstances = 0; + return 1; } void channels_free(){ size_t u; for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %lu on instance %s\n", channels[u]->ident, channels[u]->instance->name); + DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels[u]->ident, channels[u]->instance->name); if(channels[u]->impl && channels[u]->instance->backend->channel_free){ channels[u]->instance->backend->channel_free(channels[u]); } @@ -200,11 +206,12 @@ void channels_free(){ nchannels = 0; } + backend* backend_match(char* name){ size_t u; - for(u = 0; u < nbackends; u++){ - if(!strcmp(backends[u].name, name)){ - return backends + u; + for(u = 0; u < registry.n; u++){ + if(!strcmp(registry.backends[u].name, name)){ + return registry.backends + u; } } return NULL; @@ -212,9 +219,12 @@ backend* backend_match(char* name){ instance* instance_match(char* name){ size_t u; - for(u = 0; u < ninstances; u++){ - if(!strcmp(instances[u]->name, name)){ - return instances[u]; + instance** iter = NULL; + for(u = 0; u < registry.n; u++){ + for(iter = registry.instances[u]; iter && *iter; iter++){ + if(!strcmp(name, (*iter)->name)){ + return *iter; + } } } return NULL; @@ -224,16 +234,17 @@ struct timeval backend_timeout(){ size_t u; uint32_t res, secs = 1, msecs = 0; - for(u = 0; u < nbackends; u++){ - if(backends[u].interval){ - res = backends[u].interval(); + for(u = 0; u < registry.n; u++){ + //only call interval if backend has instances + if(registry.instances[u] && registry.backends[u].interval){ + res = registry.backends[u].interval(); if((res / 1000) < secs){ - DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, backends[u].name); + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); secs = res / 1000; msecs = res % 1000; } else if(res / 1000 == secs && (res % 1000) < msecs){ - DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, backends[u].name); + DBGPF("Updating interval to %" PRIu32 " msecs by request from %s", res, registry.backends[u].name); msecs = res % 1000; } } @@ -248,14 +259,16 @@ struct timeval backend_timeout(){ MM_API int mm_backend_register(backend b){ if(!backend_match(b.name)){ - backends = realloc(backends, (nbackends + 1) * sizeof(backend)); - if(!backends){ + registry.backends = realloc(registry.backends, (registry.n + 1) * sizeof(backend)); + registry.instances = realloc(registry.instances, (registry.n + 1) * sizeof(instance**)); + if(!registry.backends || !registry.instances){ fprintf(stderr, "Failed to allocate memory\n"); - nbackends = 0; + registry.n = 0; return 1; } - backends[nbackends] = b; - nbackends++; + registry.backends[registry.n] = b; + registry.instances[registry.n] = NULL; + registry.n++; fprintf(stderr, "Registered backend %s\n", b.name); return 0; @@ -265,29 +278,25 @@ MM_API int mm_backend_register(backend b){ int backends_start(){ int rv = 0, current; - size_t n, u, p; instance** inst = NULL; + size_t n, u; - for(u = 0; u < nbackends; u++){ - //only start backends that have instances - for(p = 0; p < ninstances && instances[p]->backend != backends + u; p++){ - } - - //backend has no instances, skip the start call - if(p == ninstances){ + for(u = 0; u < registry.n; u++){ + //skip backends without instances + if(!registry.instances[u]){ continue; } //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", backends[u].name); + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for initialization of backend %s\n", registry.backends[u].name); return 1; } //start the backend - current = backends[u].start(n, inst); + current = registry.backends[u].start(n, inst); if(current){ - fprintf(stderr, "Failed to start backend %s\n", backends[u].name); + fprintf(stderr, "Failed to start backend %s\n", registry.backends[u].name); } //clean up @@ -302,20 +311,33 @@ int backends_stop(){ size_t u, n; instance** inst = NULL; - for(u = 0; u < nbackends; u++){ + //shut down the registry + for(u = 0; u < registry.n; u++){ //fetch list of instances - if(mm_backend_instances(backends[u].name, &n, &inst)){ - fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", backends[u].name); - n = 0; + if(mm_backend_instances(registry.backends[u].name, &n, &inst)){ + fprintf(stderr, "Failed to fetch instance list for shutdown of backend %s\n", registry.backends[u].name); inst = NULL; + n = 0; } - backends[u].shutdown(n, inst); + registry.backends[u].shutdown(n, inst); free(inst); inst = NULL; + + //free instances + for(inst = registry.instances[u]; inst && *inst; inst++){ + free((*inst)->name); + (*inst)->name = NULL; + (*inst)->backend = NULL; + free(*inst); + } + free(registry.instances[u]); + registry.instances[u] = NULL; } - free(backends); - nbackends = 0; + channels_free(); + free(registry.backends); + free(registry.instances); + registry.n = 0; return 0; } diff --git a/backend.h b/backend.h index de9b5dc..6a69508 100644 --- a/backend.h +++ b/backend.h @@ -8,9 +8,7 @@ instance* instance_match(char* name); struct timeval backend_timeout(); int backends_start(); int backends_stop(); -void instances_free(); -void channels_free(); -instance* mm_instance(); +instance* mm_instance(backend* b); /* Backend API */ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create); diff --git a/backends/artnet.c b/backends/artnet.c index 34fc82d..caab6e0 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -321,11 +321,6 @@ static inline int artnet_process_frame(instance* inst, artnet_pkt* frame){ chan = data->data.channel + MAPPED_CHANNEL(data->data.map[p]); } - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", p, inst->name); - return 1; - } - if(IS_WIDE(data->data.map[p])){ data->data.map[MAPPED_CHANNEL(data->data.map[p])] &= ~MAP_MARK; wide_val = data->data.in[p] << ((data->data.map[p] & MAP_COARSE) ? 8 : 0); diff --git a/backends/sacn.c b/backends/sacn.c index 495bdf3..bd5c75a 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -436,11 +436,6 @@ static int sacn_process_frame(instance* inst, sacn_frame_root* frame, sacn_frame chan = inst_data->data.channel + MAPPED_CHANNEL(inst_data->data.map[u]); } - if(!chan){ - LOGPF("Active channel %" PRIsize_t " on %s not known to core", u, inst->name); - return 1; - } - //generate value if(IS_WIDE(inst_data->data.map[u])){ inst_data->data.map[MAPPED_CHANNEL(inst_data->data.map[u])] &= ~MAP_MARK; diff --git a/config.c b/config.c index 920e161..aef4f87 100644 --- a/config.c +++ b/config.c @@ -388,7 +388,7 @@ static int config_line(char* line){ return 1; } - current_instance = mm_instance(); + current_instance = mm_instance(current_backend); if(!current_instance){ return 1; } -- cgit v1.2.3 From 6da00154f7745a4705047fc73ce42b7036b0bbdc Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 15 Mar 2020 12:19:50 +0100 Subject: Fix build and CI exits --- .travis-ci.sh | 14 ++++++++++---- backend.c | 4 +++- midimonster.c | 4 ---- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 3b7d7f5..c832f2c 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -21,7 +21,7 @@ if [ "$TASK" = "spellcheck" ]; then fi # Run codespell to find some more - cs_results=$(xargs codespell --quiet 2 --regex "[a-zA-Z0-9][\\-'a-zA-Z0-9]+[a-zA-Z0-9]" <<< "$spellcheck_files" 2>&1) + cs_results=$(xargs codespell --quiet 2 <<< "$spellcheck_files" 2>&1) cs_errors=$(wc -l <<< "$cs_results") if [ "$cs_errors" -ne 0 ]; then printf "Codespell found %s errors:\n\n" "$cs_errors" @@ -62,11 +62,15 @@ elif [ "$TASK" = "codesmell" ]; then elif [ "$TASK" = "sanitize" ]; then # Run sanitized compile travis_fold start "make_sanitize" - make sanitize; + if make sanitize; then + exit "$?" + fi travis_fold end "make_sanitize" elif [ "$TASK" = "windows" ]; then travis_fold start "make_windows" - make windows; + if make windows; then + exit "$?" + fi make -C backends lua.dll travis_fold end "make_windows" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then @@ -87,7 +91,9 @@ elif [ "$TASK" = "windows" ]; then else # Otherwise compile as normal travis_fold start "make" - make full; + if make full; then + exit "$?" + fi travis_fold end "make" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then travis_fold start "deploy_unix" diff --git a/backend.c b/backend.c index 65cbd7d..cb8bf3f 100644 --- a/backend.c +++ b/backend.c @@ -311,6 +311,9 @@ int backends_stop(){ size_t u, n; instance** inst = NULL; + //channels before instances to support proper shutdown procedures + channels_free(); + //shut down the registry for(u = 0; u < registry.n; u++){ //fetch list of instances @@ -335,7 +338,6 @@ int backends_stop(){ registry.instances[u] = NULL; } - channels_free(); free(registry.backends); free(registry.instances); registry.n = 0; diff --git a/midimonster.c b/midimonster.c index 2b0b7a0..8e217e7 100644 --- a/midimonster.c +++ b/midimonster.c @@ -466,8 +466,6 @@ int main(int argc, char** argv){ if(config_read(cfg_file)){ fprintf(stderr, "Failed to read configuration file %s\n", cfg_file); backends_stop(); - channels_free(); - instances_free(); map_free(); fds_free(); plugins_close(); @@ -497,8 +495,6 @@ int main(int argc, char** argv){ bail: //free all data backends_stop(); - channels_free(); - instances_free(); map_free(); fds_free(); event_free(); -- cgit v1.2.3 From 846a49199ba011a00e4814bacac524a6fe9b299a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 16 Mar 2020 22:19:27 +0100 Subject: Hash-index event routing table --- installer.sh | 87 +++++++++++++---------------------- midimonster.c | 142 +++++++++++++++++++++++++++++++--------------------------- midimonster.h | 2 + 3 files changed, 109 insertions(+), 122 deletions(-) diff --git a/installer.sh b/installer.sh index 56021c4..111c783 100755 --- a/installer.sh +++ b/installer.sh @@ -26,13 +26,13 @@ latest_version="$(curl --silent "https://api.github.com/repos/cbdevnet/midimonst # make invocation arguments makeargs="all" -normal=$(tput sgr0) -dim=$(tput dim) -bold=$(tput bold) -uline=$(tput smul) -c_red=$(tput setaf 1) -c_green=$(tput setaf 2) -c_mag=$(tput setaf 5) +normal="$(tput sgr0)" +dim="$(tput dim)" +bold="$(tput bold)" +uline="$(tput smul)" +c_red="$(tput setaf 1)" +c_green="$(tput setaf 2)" +c_mag="$(tput setaf 5)" DEFAULT_PREFIX="/usr" DEFAULT_PLUGINPATH="/lib/midimonster" @@ -49,7 +49,7 @@ assign_defaults(){ ARGS(){ for i in "$@"; do - case $i in + case "$i" in --prefix=*) VAR_PREFIX="${i#*=}" ;; @@ -71,37 +71,28 @@ ARGS(){ -fu|--forceupdate) UPDATER_FORCE="1" ;; - -supd|--selfupdate) - NIGHTLY="" - self_update - rmdir "$tmp_path" + --install-updater) + NIGHTLY=1 prepare_repo + install_script exit 0 ;; - -insup|--installupdater) - NIGHTLY="" - install_updater - rmdir "$tmp_path" - exit 0 - ;; - -insdep|--installdeps) + --install-dependencies) install_dependencies - rmdir "$tmp_path" exit 0 ;; -h|--help|*) assign_defaults printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" - printf "\n\t${c_green} --prefix${normal} ${c_red}${normal}\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" - printf "\n\t${c_green} --plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" - printf "\n\t${c_green} --defcfg${normal} ${c_red}${normal}\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" - printf "\n\t${c_green} --examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" - printf "\n\t${c_green} --dev${normal}\t\t\tInstall nightly version" - printf "\n ${c_green}-d,\t --default${normal}\t\tUse default values to install" - printf "\n ${c_green}-fu,\t --forceupdate${normal}\t\tForce the updater to update without a version check" - printf "\n ${c_green}-supd,\t --selfupdate${normal}\t\tUpdates this script to the newest version" - printf "\n ${c_green}-insup, --installupdater${normal}\tInstall the updater (Run with midimonster-updater)" - printf "\n ${c_green}-insdep, --installdeps${normal}\t\tInstall dependencies and exit" - printf "\n ${c_green}-h,\t --help${normal}\t\tShow this message and exit" + printf "\n\t${c_green}--prefix${normal} ${c_red}${normal}\t\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green}--plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green}--defcfg${normal} ${c_red}${normal}\t\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green}--examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green}--dev${normal}\t\t\t\tInstall nightly version" + printf "\n\t${c_green}-d, --default${normal}\t\t\tUse default values to install" + printf "\n\t${c_green}-fu, --forceupdate${normal}\t\tForce the updater to update without a version check" + printf "\n\t${c_green}--install-updater${normal}\t\tInstall the updater (Run with midimonster-updater)" + printf "\n\t${c_green}--install-dependencies${normal}\t\tInstall dependencies and exit" + printf "\n\t${c_green}-h, --help${normal}\t\t\tShow this message and exit" printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" rmdir "$tmp_path" exit 1 @@ -116,7 +107,7 @@ install_dependencies(){ for dependency in ${deps[@]}; do if [ "$(dpkg-query -W -f='${Status}' "$dependency" 2>/dev/null | grep -c "ok installed")" -eq 0 ]; then printf "Installing %s\n" "$dependency" - apt-get install $dependency + apt-get install "$dependency" else printf "%s already installed!\n" "$dependency" fi @@ -181,7 +172,7 @@ prepare_repo(){ printf "Finding latest stable version...\n" last_tag=$(git describe --abbrev=0) printf "Checking out %s...\n" "$last_tag" - git checkout -f -q $last_tag + git checkout -f -q "$last_tag" fi } @@ -207,30 +198,14 @@ save_config(){ printf "VAR_PREFIX=%s\nVAR_PLUGINS=%s\nVAR_DEFAULT_CFG=%s\nVAR_DESTDIR=%s\nVAR_EXAMPLE_CFGS=%s\n" "$VAR_PREFIX" "$VAR_PLUGINS" "$VAR_DEFAULT_CFG" "$VAR_DESTDIR" "$VAR_EXAMPLE_CFGS" > "$updater_dir/updater.conf" } -# Updates this script either from the downloaded sources or the remote repository. -self_update(){ +# Updates this script using the one from the checked out repo (containing the requested version) +install_script(){ mkdir -p "$updater_dir" - if [ "$NIGHTLY" = "1" ]; then - cp -u "$tmp_path/installer.sh" "$0" - elif [ "$NIGHTLY" != "1" ]; then - wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O "$0" - fi - chmod +x "$0" -} - -install_updater(){ - mkdir -p "$updater_dir" - if [ "$NIGHTLY" = "1" ]; then - printf "Copying updater to %s/updater.sh\n" "$updater_dir" - cp -u "$tmp_path/installer.sh" "$updater_dir/updater.sh" - else - printf "Downloading updater to %s/updater.sh\n" "$updater_dir" - wget https://raw.githubusercontent.com/cbdevnet/midimonster/master/installer.sh -O "$updater_dir/updater.sh" - fi + printf "Copying updater to %s/updater.sh\n" "$updater_dir" + cp "$tmp_path/installer.sh" "$updater_dir/updater.sh" chmod +x "$updater_dir/updater.sh" printf "Creating symlink /usr/bin/midimonster-updater\n" ln -s "$updater_dir/updater.sh" "/usr/bin/midimonster-updater" - printf "Done.\n" } error_handler(){ @@ -238,7 +213,7 @@ error_handler(){ exit 1 } -cleanup() { +cleanup(){ if [ -d "$tmp_path" ]; then printf "Cleaning up temporary files...\n" rm -rf "$tmp_path" @@ -288,7 +263,7 @@ if [ -f "$updater_dir/updater.conf" ] || [ "$UPDATER_FORCE" = "1" ]; then # Run updater steps prepare_repo - install_updater + install_script save_config build else @@ -296,7 +271,7 @@ else install_dependencies prepare_repo ask_questions - install_updater + install_script save_config build fi diff --git a/midimonster.c b/midimonster.c index 8e217e7..afb6a2b 100644 --- a/midimonster.c +++ b/midimonster.c @@ -29,25 +29,34 @@ typedef struct /*_mm_channel_mapping*/ { channel** to; } channel_mapping; -static size_t mappings = 0; -static channel_mapping* map = NULL; +static struct { + //routing_hash is set up for 256 buckets + size_t entries[256]; + channel_mapping* map[256]; + + event_collection pool[2]; + event_collection* events; +} routing = { + .events = routing.pool +}; + static size_t fds = 0; static managed_fd* fd = NULL; static volatile sig_atomic_t fd_set_dirty = 1; static uint64_t global_timestamp = 0; -static event_collection event_pool[2] = { - {0}, - {0} -}; -static event_collection* primary = event_pool; - volatile static sig_atomic_t shutdown_requested = 0; static void signal_handler(int signum){ shutdown_requested = 1; } +static size_t routing_hash(channel* key){ + uint64_t repr = (uint64_t) key; + //return 8bit hash for 256 buckets, not ideal but it works + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} + MM_API uint64_t mm_timestamp(){ return global_timestamp; } @@ -67,53 +76,66 @@ static void update_timestamp(){ } int mm_map_channel(channel* from, channel* to){ - size_t u, m; + size_t u, m, bucket = routing_hash(from); + //find existing source mapping - for(u = 0; u < mappings; u++){ - if(map[u].from == from){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == from){ break; } } //create new entry - if(u == mappings){ - map = realloc(map, (mappings + 1) * sizeof(channel_mapping)); - if(!map){ + if(u == routing.entries[bucket]){ + routing.map[bucket] = realloc(routing.map[bucket], (routing.entries[bucket] + 1) * sizeof(channel_mapping)); + if(!routing.map[bucket]){ + routing.entries[bucket] = 0; fprintf(stderr, "Failed to allocate memory\n"); return 1; } - memset(map + mappings, 0, sizeof(channel_mapping)); - mappings++; - map[u].from = from; + + memset(routing.map[bucket] + routing.entries[bucket], 0, sizeof(channel_mapping)); + routing.entries[bucket]++; + routing.map[bucket][u].from = from; } //check whether the target is already mapped - for(m = 0; m < map[u].destinations; m++){ - if(map[u].to[m] == to){ + for(m = 0; m < routing.map[bucket][u].destinations; m++){ + if(routing.map[bucket][u].to[m] == to){ return 0; } } - map[u].to = realloc(map[u].to, (map[u].destinations + 1) * sizeof(channel*)); - if(!map[u].to){ + //add a mapping target + routing.map[bucket][u].to = realloc(routing.map[bucket][u].to, (routing.map[bucket][u].destinations + 1) * sizeof(channel*)); + if(!routing.map[bucket][u].to){ fprintf(stderr, "Failed to allocate memory\n"); - map[u].destinations = 0; + routing.map[bucket][u].destinations = 0; return 1; } - map[u].to[map[u].destinations] = to; - map[u].destinations++; + routing.map[bucket][u].to[routing.map[bucket][u].destinations] = to; + routing.map[bucket][u].destinations++; return 0; } -static void map_free(){ - size_t u; - for(u = 0; u < mappings; u++){ - free(map[u].to); +static void routing_cleanup(){ + size_t u, n; + + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + for(n = 0; n < routing.entries[u]; n++){ + free(routing.map[u][n].to); + } + free(routing.map[u]); + routing.map[u] = NULL; + routing.entries[u] = 0; + } + + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + free(routing.pool[u].channel); + free(routing.pool[u].value); + routing.pool[u].alloc = 0; } - free(map); - mappings = 0; - map = NULL; } MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ @@ -170,7 +192,6 @@ MM_API int mm_manage_fd(int new_fd, char* back, int manage, void* impl){ static void fds_free(){ size_t u; for(u = 0; u < fds; u++){ - //TODO free impl if(fd[u].fd >= 0){ close(fd[u].fd); fd[u].fd = -1; @@ -182,56 +203,46 @@ static void fds_free(){ } MM_API int mm_channel_event(channel* c, channel_value v){ - size_t u, p; + size_t u, p, bucket = routing_hash(c); //find mapped channels - for(u = 0; u < mappings; u++){ - if(map[u].from == c){ + for(u = 0; u < routing.entries[bucket]; u++){ + if(routing.map[bucket][u].from == c){ break; } } - if(u == mappings){ + if(u == routing.entries[bucket]){ //target-only channel return 0; } //resize event structures to fit additional events - if(primary->n + map[u].destinations >= primary->alloc){ - primary->channel = realloc(primary->channel, (primary->alloc + map[u].destinations) * sizeof(channel*)); - primary->value = realloc(primary->value, (primary->alloc + map[u].destinations) * sizeof(channel_value)); + if(routing.events->n + routing.map[bucket][u].destinations >= routing.events->alloc){ + routing.events->channel = realloc(routing.events->channel, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel*)); + routing.events->value = realloc(routing.events->value, (routing.events->alloc + routing.map[bucket][u].destinations) * sizeof(channel_value)); - if(!primary->channel || !primary->value){ + if(!routing.events->channel || !routing.events->value){ fprintf(stderr, "Failed to allocate memory\n"); - primary->alloc = 0; - primary->n = 0; + routing.events->alloc = 0; + routing.events->n = 0; return 1; } - primary->alloc += map[u].destinations; + routing.events->alloc += routing.map[bucket][u].destinations; } //enqueue channel events //FIXME this might lead to one channel being mentioned multiple times in an apply call - for(p = 0; p < map[u].destinations; p++){ - primary->channel[primary->n + p] = map[u].to[p]; - primary->value[primary->n + p] = v; + memcpy(routing.events->channel + routing.events->n, routing.map[bucket][u].to, routing.map[bucket][u].destinations * sizeof(channel*)); + for(p = 0; p < routing.map[bucket][u].destinations; p++){ + routing.events->value[routing.events->n + p] = v; } - primary->n += map[u].destinations; + routing.events->n += routing.map[bucket][u].destinations; return 0; } -static void event_free(){ - size_t u; - - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - free(event_pool[u].channel); - free(event_pool[u].value); - event_pool[u].alloc = 0; - } -} - static void version(){ printf("MIDIMonster %s\n", MIDIMONSTER_VERSION); } @@ -336,13 +347,13 @@ static int core_process(size_t nfds, managed_fd* signaled_fds){ return 1; } - while(primary->n){ + while(routing.events->n){ //swap primary and secondary event collectors - DBGPF("Swapping event collectors, %lu events in primary\n", primary->n); - for(u = 0; u < sizeof(event_pool) / sizeof(event_collection); u++){ - if(primary != event_pool + u){ - secondary = primary; - primary = event_pool + u; + DBGPF("Swapping event collectors, %lu events in primary\n", routing.events->n); + for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ + if(routing.events != routing.pool + u){ + secondary = routing.events; + routing.events = routing.pool + u; break; } } @@ -466,7 +477,7 @@ int main(int argc, char** argv){ if(config_read(cfg_file)){ fprintf(stderr, "Failed to read configuration file %s\n", cfg_file); backends_stop(); - map_free(); + routing_cleanup(); fds_free(); plugins_close(); config_free(); @@ -495,9 +506,8 @@ int main(int argc, char** argv){ bail: //free all data backends_stop(); - map_free(); + routing_cleanup(); fds_free(); - event_free(); plugins_close(); config_free(); platform_shutdown(); diff --git a/midimonster.h b/midimonster.h index dc0e255..61467c1 100644 --- a/midimonster.h +++ b/midimonster.h @@ -247,6 +247,8 @@ MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); * Register (manage = 1) or unregister (manage = 0) a file descriptor * to be selected on. The backend will be notified when the descriptor * becomes ready to read via its registered mmbackend_process_fd call. + * The `impl` argument will be provided within the corresponding + * managed_fd structure upon callback. */ MM_API int mm_manage_fd(int fd, char* backend, int manage, void* impl); -- cgit v1.2.3 From 636a9592eab1963c2b1a77bd59eaff317fa55b75 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Mar 2020 01:09:52 +0100 Subject: Fix registry apply bug, fix openpixelcontrol broadcast --- backend.c | 3 +-- backends/openpixelcontrol.c | 10 +++++----- midimonster.c | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/backend.c b/backend.c index cb8bf3f..9e7989e 100644 --- a/backend.c +++ b/backend.c @@ -55,7 +55,7 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ channel_value xval; channel* xchnl = NULL; - for(u = 0; u < nev && !rv; u++){ + for(u = 0; u < nev && !rv; u = n){ //sort for this instance n = u + 1; for(p = u + 1; p < nev; p++){ @@ -75,7 +75,6 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ //TODO eliminate duplicates DBGPF("Calling handler for instance %s with %" PRIsize_t " events\n", c[u]->instance->name, n - u); rv |= c[u]->instance->backend->handle(c[u]->instance, n - u, c + u, v + u); - u = n; } return 0; diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 168e077..2a5e01f 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -495,11 +495,11 @@ static ssize_t openpixel_client_headerdata(instance* inst, openpixel_client* cli } else{ client->buffer = openpixel_buffer_find(data, client->hdr.strip, 1); - } - //if no buffer or mode mismatch, ignore data - if(client->buffer < 0 - || data->mode != client->hdr.mode){ - client->buffer = -2; //mark for ignore + //if no buffer or mode mismatch, ignore data + if(client->buffer < 0 + || data->mode != client->hdr.mode){ + client->buffer = -2; //mark for ignore + } } client->left = be16toh(client->hdr.length); client->offset = 0; diff --git a/midimonster.c b/midimonster.c index afb6a2b..1691671 100644 --- a/midimonster.c +++ b/midimonster.c @@ -409,7 +409,7 @@ static int core_loop(){ #ifndef _WIN32 fprintf(stderr, "select failed: %s\n", strerror(errno)); #else - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error_message, 0, NULL); fprintf(stderr, "select failed: %s\n", error_message); LocalFree(error_message); -- cgit v1.2.3 From daf9ac5b883cf981a163654d58932bfbdce619d9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 17 Mar 2020 23:58:06 +0100 Subject: Output routing info to log --- configs/launchctl-sacn.cfg | 2 +- midimonster.c | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/configs/launchctl-sacn.cfg b/configs/launchctl-sacn.cfg index dedfc0f..10a736a 100644 --- a/configs/launchctl-sacn.cfg +++ b/configs/launchctl-sacn.cfg @@ -7,7 +7,7 @@ name = MIDIMonster [backend sacn] -bind = 0.0.0.0 +bind = 0.0.0.0 5568 local [midi lc] read = Launch Control diff --git a/midimonster.c b/midimonster.c index 1691671..5109eab 100644 --- a/midimonster.c +++ b/midimonster.c @@ -456,6 +456,7 @@ static int core_loop(){ int main(int argc, char** argv){ int rv = EXIT_FAILURE; char* cfg_file = DEFAULT_CFG; + size_t u, n = 0, max = 0; //parse commandline arguments if(args_parse(argc, argv, &cfg_file)){ @@ -494,6 +495,14 @@ int main(int argc, char** argv){ signal(SIGINT, signal_handler); + //count and report mappings + for(u = 0; u < sizeof(routing.map) / sizeof(routing.map[0]); u++){ + n += routing.entries[u]; + max = max(max, routing.entries[u]); + } + LOGPF("Routing %" PRIsize_t " sources, largest bucket has %" PRIsize_t " entries", + n, max); + if(!fds){ fprintf(stderr, "No descriptors registered for multiplexing\n"); } -- cgit v1.2.3 From 9718e10c7f4151cea895f515c785c14e0021d967 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 18 Mar 2020 22:36:35 +0100 Subject: Implement default channel handlers for Lua/Python --- backends/lua.c | 17 +++++++++++++- backends/lua.h | 2 ++ backends/lua.md | 10 +++++---- backends/python.c | 65 +++++++++++++++++++++++++++++++++++++----------------- backends/python.h | 3 +++ backends/python.md | 8 ++++--- 6 files changed, 77 insertions(+), 28 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index e7ba9f9..498a037 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -308,6 +308,11 @@ static int lua_configure_instance(instance* inst, char* option, char* value){ } return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance configuration parameter %s for instance %s", option, inst->name); return 1; @@ -461,7 +466,8 @@ static int lua_start(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; for(p = 0; p < data->channels; p++){ //exclude reserved names - if(strcmp(data->channel_name[p], "output") + if(!data->default_handler + && strcmp(data->channel_name[p], "output") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") @@ -473,6 +479,14 @@ static int lua_start(size_t n, instance** inst){ data->reference[p] = LUA_NOREF; } } + else if(data->default_handler){ + lua_getglobal(data->interpreter, data->default_handler); + data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); + if(data->reference[p] == LUA_REFNIL){ + data->reference[p] = LUA_NOREF; + LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); + } + } } } @@ -504,6 +518,7 @@ static int lua_shutdown(size_t n, instance** inst){ free(data->reference); free(data->input); free(data->output); + free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index ebe2046..743f978 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -29,6 +29,8 @@ typedef struct /*_lua_instance_data*/ { double* input; double* output; lua_State* interpreter; + + char* default_handler; } lua_instance_data; typedef struct /*_lua_interval_callback*/ { diff --git a/backends/lua.md b/backends/lua.md index db4cf39..96e53c8 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -6,7 +6,8 @@ and manipulate events using the Lua scripting language. Every instance has its own interpreter state which can be loaded with custom handler scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) -with the value (as a Lua `number` type) as parameter. +with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler +may be supplied in the configuration. The following functions are provided within the Lua interpreter for interaction with the MIDIMonster @@ -42,9 +43,10 @@ The `lua` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `script` | `script.lua` | none | Lua source file (relative to configuration file)| +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `script` | `script.lua` | none | Lua source file (relative to configuration file) | +| `default-handler` | `handler` | none | Name of a function to be called as handler for all incoming channels (instead of the per-channel handlers) | A single instance may have multiple `script` options specified, which will all be read cumulatively. diff --git a/backends/python.c b/backends/python.c index 70c2548..735e838 100644 --- a/backends/python.c +++ b/backends/python.c @@ -378,6 +378,11 @@ static int python_configure_instance(instance* inst, char* option, char* value){ PyEval_ReleaseThread(data->interpreter); return 0; } + else if(!strcmp(option, "default-handler")){ + free(data->default_handler); + data->default_handler = strdup(value); + return 0; + } LOGPF("Unknown instance parameter %s for instance %s", option, inst->name); return 1; @@ -600,38 +605,56 @@ static int python_handle(size_t num, managed_fd* fds){ return 0; } +static PyObject* python_resolve_symbol(char* spec_raw){ + char* module_name = NULL, *object_name = NULL, *spec = strdup(spec_raw); + PyObject* module = NULL, *result = NULL; + + module = PyImport_AddModule("__main__"); + object_name = spec; + module_name = strchr(object_name, '.'); + if(module_name){ + *module_name = 0; + //returns borrowed reference + module = PyImport_AddModule(object_name); + + if(!module){ + LOGPF("Module %s for symbol %s.%s is not loaded", object_name, object_name, module_name + 1); + return NULL; + } + + object_name = module_name + 1; + + //returns new reference + result = PyObject_GetAttrString(module, object_name); + } + + free(spec); + return result; +} + static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; - PyObject* module = NULL; size_t u, p; - char* module_name = NULL, *channel_name = NULL; //resolve channel references to handler functions for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + DBGPF("Starting up instance %s", inst[u]->name); //switch to interpreter PyEval_RestoreThread(data->interpreter); - for(p = 0; p < data->channels; p++){ - module = PyImport_AddModule("__main__"); - channel_name = data->channel[p].name; - module_name = strchr(channel_name, '.'); - if(module_name){ - *module_name = 0; - //returns borrowed reference - module = PyImport_AddModule(channel_name); - - if(!module){ - LOGPF("Module %s for qualified channel %s.%s is not loaded on instance %s", channel_name, channel_name, module_name + 1, inst[u]->name); - return 1; - } - *module_name = '.'; - channel_name = module_name + 1; - } + if(data->default_handler){ + data->handler = python_resolve_symbol(data->default_handler); + } - //returns new reference - data->channel[p].handler = PyObject_GetAttrString(module, channel_name); + for(p = 0; p < data->channels; p++){ + if(!strchr(data->channel[p].name, '.') && data->handler){ + data->channel[p].handler = data->handler; + } + else{ + data->channel[p].handler = python_resolve_symbol(data->channel[p].name); + } } //release interpreter @@ -654,6 +677,7 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->channel[p].handler); } free(data->channel); + free(data->default_handler); //do not free data here, needed for shutting down interpreters } @@ -675,6 +699,7 @@ static int python_shutdown(size_t n, instance** inst){ for(p = 0; p handler); DBGPF("Shutting down interpreter for instance %s", inst[u]->name); //swap to interpreter and end it, GIL is held after this but state is NULL diff --git a/backends/python.h b/backends/python.h index 8ca12f9..a40098b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -41,4 +41,7 @@ typedef struct /*_python_instance_data*/ { size_t channels; mmpython_channel* channel; mmpython_channel* current_channel; + + char* default_handler; + PyObject* handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index f06e504..6852a79 100644 --- a/backends/python.md +++ b/backends/python.md @@ -6,6 +6,7 @@ to route, generate and manipulate channel events using the Python 3 scripting la Every instance has its own interpreter, which can be loaded with multiple Python modules. These modules may contain member functions accepting a single `float` parameter, which can then be used as target channels. For each incoming event, the handler function is called. +Channels in the global scope may be assigned a default handler function. Python modules may also register `socket` objects (and an associated callback function) with the MIDIMonster core, which will then alert the module when there is data ready to be read. @@ -67,9 +68,10 @@ The `python` backend does not take any global configuration. #### Instance configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------------------------------| -| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------------------------------| +| `module` | `my_handlers.py` | none | (Path to) Python module source file, relative to configuration file location | +| `default-handler` | `mu_handlers.default` | none | Function to be called as handler for all top-level channels (not belonging to a module) | A single instance may have multiple `module` options specified. This will make all handlers available within their module namespaces (see the section on channel specification). -- cgit v1.2.3 From 2d66d5cee9bf3ed5779f65d8a99b40ee5181bf30 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 21:50:33 +0100 Subject: Implement Lua threading --- backends/lua.c | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- backends/lua.h | 7 ++++ backends/lua.md | 30 ++++++++++++--- 3 files changed, 144 insertions(+), 7 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 498a037..b66a27a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -10,6 +10,7 @@ #define LUA_REGISTRY_KEY "_midimonster_lua_instance" #define LUA_REGISTRY_CURRENT_CHANNEL "_midimonster_lua_channel" +#define LUA_REGISTRY_CURRENT_THREAD "_midimonster_lua_thread" static size_t timers = 0; static lua_timer* timer = NULL; @@ -20,6 +21,9 @@ static int timer_fd = -1; static uint64_t last_timestamp; #endif +static size_t threads = 0; +static lua_thread* thread = NULL; + MM_PLUGIN_API int init(){ backend lua = { #ifndef MMBACKEND_LUA_TIMERFD @@ -86,6 +90,12 @@ static int lua_update_timerfd(){ interval = timer[n].interval; } } + + for(n = 0; n < threads; n++){ + if(thread[n].timeout && (!interval || thread[n].timeout < interval)){ + interval = thread[n].timeout; + } + } DBGPF("Recalculating timers, minimum is %" PRIu64, interval); //calculate gcd of all timers if any are active @@ -100,7 +110,8 @@ static int lua_update_timerfd(){ gcd = residual; } //since we round everything, 10 is the lowest interval we get - if(interval == 10){ + if(interval <= 10){ + interval = 10; break; } } @@ -126,6 +137,89 @@ static int lua_update_timerfd(){ return 0; } +static void lua_thread_resume(size_t current_thread){ + //push coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnumber(thread[current_thread].thread, current_thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); + + //call thread main + DBGPF("Resuming thread %" PRIsize_t " on %s", current_thread, thread[current_thread].instance->name); + if(lua_resume(thread[current_thread].thread, NULL, 0) != LUA_YIELD){ + DBGPF("Thread %" PRIsize_t " on %s terminated", current_thread, thread[current_thread].instance->name); + thread[current_thread].timeout = 0; + } + + //remove coroutine reference + lua_pushstring(thread[current_thread].thread, LUA_REGISTRY_CURRENT_THREAD); + lua_pushnil(thread[current_thread].thread); + lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = NULL; + size_t u = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + + //make space for a new thread + thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); + if(!thread){ + threads = 0; + LOG("Failed to allocate memory"); + return 0; + } + threads++; + + thread[u].thread = lua_newthread(interpreter); + thread[u].instance = inst; + thread[u].timeout = 0; + thread[u].reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + + DBGPF("Registered thread %" PRIsize_t " on %s", threads, inst->name); + + //push thread main + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + lua_pushvalue(interpreter, 1); + lua_xmove(interpreter, thread[u].thread, 1); + + lua_thread_resume(u); + lua_update_timerfd(); + return 0; +} + +static int lua_callback_sleep(lua_State* interpreter){ + uint64_t timeout = 0; + size_t current_thread = threads; + if(lua_gettop(interpreter) != 1){ + LOGPF("Sleep function called with %d arguments, expected number", lua_gettop(interpreter)); + return 0; + } + + timeout = luaL_checkinteger(interpreter, 1); + + lua_pushstring(interpreter, LUA_REGISTRY_CURRENT_THREAD); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + + current_thread = luaL_checkinteger(interpreter, -1); + + if(current_thread < threads){ + DBGPF("Yielding for %" PRIu64 "msec on thread %" PRIsize_t, timeout, current_thread); + thread[current_thread].timeout = timeout; + lua_yield(interpreter, 0); + } + return 0; +} + static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; @@ -341,6 +435,8 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "output_value", lua_callback_output_value); lua_register(data->interpreter, "input_channel", lua_callback_input_channel); lua_register(data->interpreter, "timestamp", lua_callback_timestamp); + lua_register(data->interpreter, "thread", lua_callback_thread); + lua_register(data->interpreter, "sleep", lua_callback_sleep); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -454,6 +550,17 @@ static int lua_handle(size_t num, managed_fd* fds){ } } } + + //check for threads to wake up + for(n = 0; n < threads; n++){ + if(thread[n].timeout && delta >= thread[n].timeout){ + lua_thread_resume(n); + lua_update_timerfd(); + } + else if(thread[n].timeout){ + thread[n].timeout -= delta; + } + } return 0; } @@ -468,6 +575,8 @@ static int lua_start(size_t n, instance** inst){ //exclude reserved names if(!data->default_handler && strcmp(data->channel_name[p], "output") + && strcmp(data->channel_name[p], "thread") + && strcmp(data->channel_name[p], "sleep") && strcmp(data->channel_name[p], "input_value") && strcmp(data->channel_name[p], "output_value") && strcmp(data->channel_name[p], "input_channel") @@ -526,6 +635,9 @@ static int lua_shutdown(size_t n, instance** inst){ free(timer); timer = NULL; timers = 0; + free(thread); + thread = NULL; + threads = 0; #ifdef MMBACKEND_LUA_TIMERFD close(timer_fd); timer_fd = -1; diff --git a/backends/lua.h b/backends/lua.h index 743f978..d8e720a 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -39,3 +39,10 @@ typedef struct /*_lua_interval_callback*/ { lua_State* interpreter; int reference; } lua_timer; + +typedef struct /*_lua_coroutine*/ { + instance* instance; + lua_State* thread; + int reference; + uint64_t timeout; +} lua_thread; diff --git a/backends/lua.md b/backends/lua.md index 96e53c8..05509b6 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -19,24 +19,41 @@ The following functions are provided within the Lua interpreter for interaction | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | +| `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | +| `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | Example script: -``` +```lua function bar(value) - output("foo", value / 2) + output("foo", value / 2); end step = 0 function toggle() - output("bar", step * 1.0) + output("bar", step * 1.0); step = (step + 1) % 2; end +function run_show() + while(true) do + sleep(1000); + output("narf", 0); + sleep(1000); + output("narf", 1.0); + end +end + interval(toggle, 1000) +thread(run_show) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. +Threads are implemented as Lua coroutines, not operating system threads. This means that +cooperative multithreading is required, which can be achieved by calling the `sleep(number)` +function from within a running thread. Calling that function from any other context is +not supported. + #### Global configuration The `lua` backend does not take any global configuration. @@ -61,9 +78,10 @@ lua1.foo > lua2.bar #### Known bugs / problems -Using any of the interface functions (`output`, `interval`, `input_value`, `output_value`, `input_channel`, -`timestamp`) as an input channel name to a Lua instance will not call any handler functions. -Using these names as arguments to the output and value interface functions works as intended. +Using any of the interface functions (`output`, `interval`, etc.) as an input channel name to a +Lua instance will not call any handler functions. Using these names as arguments to the output and +value interface functions works as intended. When using a default handler, the default handler will +be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. -- cgit v1.2.3 From 03a75ea3b4e72f09843bc8b12f8bac84a2f27709 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 20 Mar 2020 23:35:53 +0100 Subject: Restructure lua channel resolution --- backends/lua.c | 105 +++++++++++++++++++++++++++++------------------------- backends/lua.h | 14 +++++--- backends/python.c | 3 +- 3 files changed, 68 insertions(+), 54 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index b66a27a..b71ca97 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -245,13 +245,13 @@ static int lua_callback_output(lua_State* interpreter){ //find correct channel & output value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ + if(!strcmp(channel_name, data->channel[n].name)){ channel = mm_channel(inst, n, 0); if(!channel){ return 0; } mm_channel_event(channel, val); - data->output[n] = val.normalised; + data->channel[n].out = val.normalised; return 0; } } @@ -357,8 +357,8 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //find correct channel & return value for(n = 0; n < data->channels; n++){ - if(!strcmp(channel_name, data->channel_name[n])){ - lua_pushnumber(interpreter, (input) ? data->input[n] : data->output[n]); + if(!strcmp(channel_name, data->channel[n].name)){ + lua_pushnumber(interpreter, (input) ? data->channel[n].in : data->channel[n].out); return 1; } } @@ -453,26 +453,23 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ //find matching channel for(u = 0; u < data->channels; u++){ - if(!strcmp(spec, data->channel_name[u])){ + if(!strcmp(spec, data->channel[u].name)){ break; } } //allocate new channel if(u == data->channels){ - data->channel_name = realloc(data->channel_name, (u + 1) * sizeof(char*)); - data->reference = realloc(data->reference, (u + 1) * sizeof(int)); - data->input = realloc(data->input, (u + 1) * sizeof(double)); - data->output = realloc(data->output, (u + 1) * sizeof(double)); - if(!data->channel_name || !data->reference || !data->input || !data->output){ + data->channel = realloc(data->channel, (data->channels + 1) * sizeof(lua_channel_data)); + if(!data->channel){ LOG("Failed to allocate memory"); + data->channels = 0; return NULL; } - data->reference[u] = LUA_NOREF; - data->input[u] = data->output[u] = 0.0; - data->channel_name[u] = strdup(spec); - if(!data->channel_name[u]){ + data->channel[u].in = data->channel[u].out = 0.0; + data->channel[u].name = strdup(spec); + if(!data->channel[u].name){ LOG("Failed to allocate memory"); return NULL; } @@ -483,23 +480,24 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ } static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ - size_t n = 0; + size_t n = 0, ident; lua_instance_data* data = (lua_instance_data*) inst->impl; //handle all incoming events for(n = 0; n < num; n++){ - data->input[c[n]->ident] = v[n].normalised; + ident = c[n]->ident; + data->channel[ident].in = v[n].normalised; //call lua channel handlers if present - if(data->reference[c[n]->ident] != LUA_NOREF){ + if(data->channel[ident].reference != LUA_NOREF){ //push the channel name lua_pushstring(data->interpreter, LUA_REGISTRY_CURRENT_CHANNEL); - lua_pushstring(data->interpreter, data->channel_name[c[n]->ident]); + lua_pushstring(data->interpreter, data->channel[ident].name); lua_settable(data->interpreter, LUA_REGISTRYINDEX); - lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->reference[c[n]->ident]); + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->channel[ident].reference); lua_pushnumber(data->interpreter, v[n].normalised); if(lua_pcall(data->interpreter, 1, 0, 0) != LUA_OK){ - LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel_name[c[n]->ident], lua_tostring(data->interpreter, -1)); + LOGPF("Failed to call handler for %s.%s: %s", inst->name, data->channel[ident].name, lua_tostring(data->interpreter, -1)); lua_pop(data->interpreter, 1); } } @@ -564,37 +562,51 @@ static int lua_handle(size_t num, managed_fd* fds){ return 0; } +static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ + int reference = LUA_REFNIL; + + //exclude reserved names + if(!strcmp(symbol, "output") + || !strcmp(symbol, "thread") + || !strcmp(symbol, "sleep") + || !strcmp(symbol, "input_value") + || !strcmp(symbol, "output_value") + || !strcmp(symbol, "input_channel") + || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "interval")){ + return LUA_NOREF; + } + + lua_getglobal(interpreter, symbol); + reference = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(reference == LUA_REFNIL){ + return LUA_NOREF; + } + return reference; +} + static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; + int default_handler; //resolve channels to their handler functions for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - //exclude reserved names - if(!data->default_handler - && strcmp(data->channel_name[p], "output") - && strcmp(data->channel_name[p], "thread") - && strcmp(data->channel_name[p], "sleep") - && strcmp(data->channel_name[p], "input_value") - && strcmp(data->channel_name[p], "output_value") - && strcmp(data->channel_name[p], "input_channel") - && strcmp(data->channel_name[p], "timestamp") - && strcmp(data->channel_name[p], "interval")){ - lua_getglobal(data->interpreter, data->channel_name[p]); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - } + default_handler = LUA_NOREF; + + //try to resolve default handler if given + if(data->default_handler){ + default_handler = lua_resolve_symbol(data->interpreter, data->default_handler); + if(default_handler == LUA_NOREF){ + LOGPF("Failed to resolve default handler %s on %s", data->default_handler, inst[u]->name); } - else if(data->default_handler){ - lua_getglobal(data->interpreter, data->default_handler); - data->reference[p] = luaL_ref(data->interpreter, LUA_REGISTRYINDEX); - if(data->reference[p] == LUA_REFNIL){ - data->reference[p] = LUA_NOREF; - LOGPF("Failed to resolve default handler function %s on instance %s", data->default_handler, inst[u]->name); - } + } + + for(p = 0; p < data->channels; p++){ + data->channel[p].reference = default_handler; + if(!data->default_handler){ + data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); } } } @@ -621,12 +633,9 @@ static int lua_shutdown(size_t n, instance** inst){ lua_close(data->interpreter); //cleanup channel data for(p = 0; p < data->channels; p++){ - free(data->channel_name[p]); + free(data->channel[p].name); } - free(data->channel_name); - free(data->reference); - free(data->input); - free(data->output); + free(data->channel); free(data->default_handler); free(inst[u]->impl); } diff --git a/backends/lua.h b/backends/lua.h index d8e720a..3c7568b 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -22,14 +22,18 @@ static int lua_shutdown(size_t n, instance** inst); static uint32_t lua_interval(); #endif +typedef struct /*_lua_channel*/ { + char* name; + int reference; + double in; + double out; +} lua_channel_data; + typedef struct /*_lua_instance_data*/ { size_t channels; - char** channel_name; - int* reference; - double* input; - double* output; - lua_State* interpreter; + lua_channel_data* channel; + lua_State* interpreter; char* default_handler; } lua_instance_data; diff --git a/backends/python.c b/backends/python.c index 735e838..c658f30 100644 --- a/backends/python.c +++ b/backends/python.c @@ -78,7 +78,8 @@ static void python_timer_recalculate(){ } //10msec is absolute lower limit and minimum gcd due to rounding - if(next_interval == 10){ + if(next_interval <= 10){ + next_interval = 10; break; } } -- cgit v1.2.3 From 5cc2ea308632c335f347d7a1002123c7152fd851 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 21 Mar 2020 02:01:43 +0100 Subject: Refactor global channel store --- backend.c | 81 +++++++++++++++++++++++++++++++++++------------------------ midimonster.h | 54 +++++++++++++++++++-------------------- 2 files changed, 74 insertions(+), 61 deletions(-) diff --git a/backend.c b/backend.c index 9e7989e..003980f 100644 --- a/backend.c +++ b/backend.c @@ -16,9 +16,21 @@ static struct { .n = 0 }; -//TODO move channel store into registry -static size_t nchannels = 0; -static channel** channels = NULL; +//the global channel store was converted from a naive list to a hashmap of lists for performance reasons +static struct { + //channelstore hash is set up for 256 buckets + size_t n[256]; + channel** entry[256]; +} channels = { + .n = { + 0 + } +}; + +static size_t channelstore_hash(instance* inst, uint64_t ident){ + uint64_t repr = ((uint64_t) inst) ^ ident; + return (repr ^ (repr >> 8) ^ (repr >> 16) ^ (repr >> 24) ^ (repr >> 32)) & 0xFF; +} int backends_handle(size_t nfds, managed_fd* fds){ size_t u, p, n; @@ -81,11 +93,12 @@ int backends_notify(size_t nev, channel** c, channel_value* v){ } MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ - size_t u; - for(u = 0; u < nchannels; u++){ - if(channels[u]->instance == inst && channels[u]->ident == ident){ - DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing\n", ident, inst->name); - return channels[u]; + size_t u, bucket = channelstore_hash(inst, ident); + for(u = 0; u < channels.n[bucket]; u++){ + if(channels.entry[bucket][u]->instance == inst + && channels.entry[bucket][u]->ident == ident){ + DBGPF("Requested channel %" PRIu64 " on instance %s already exists, reusing (%" PRIsize_t " search steps)\n", ident, inst->name, u); + return channels.entry[bucket][u]; } } @@ -94,24 +107,23 @@ MM_API channel* mm_channel(instance* inst, uint64_t ident, uint8_t create){ return NULL; } - DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s\n", ident, inst->name); - channel** new_chan = realloc(channels, (nchannels + 1) * sizeof(channel*)); - if(!new_chan){ + DBGPF("Creating previously unknown channel %" PRIu64 " on instance %s, bucket %" PRIsize_t "\n", ident, inst->name, bucket); + channels.entry[bucket] = realloc(channels.entry[bucket], (channels.n[bucket] + 1) * sizeof(channel*)); + if(!channels.entry[bucket]){ fprintf(stderr, "Failed to allocate memory\n"); - nchannels = 0; + channels.n[bucket] = 0; return NULL; } - channels = new_chan; - channels[nchannels] = calloc(1, sizeof(channel)); - if(!channels[nchannels]){ + channels.entry[bucket][channels.n[bucket]] = calloc(1, sizeof(channel)); + if(!channels.entry[bucket][channels.n[bucket]]){ fprintf(stderr, "Failed to allocate memory\n"); return NULL; } - channels[nchannels]->instance = inst; - channels[nchannels]->ident = ident; - return channels[nchannels++]; + channels.entry[bucket][channels.n[bucket]]->instance = inst; + channels.entry[bucket][channels.n[bucket]]->ident = ident; + return channels.entry[bucket][(channels.n[bucket]++)]; } instance* mm_instance(backend* b){ @@ -191,21 +203,6 @@ MM_API int mm_backend_instances(char* name, size_t* ninst, instance*** inst){ return 1; } -void channels_free(){ - size_t u; - for(u = 0; u < nchannels; u++){ - DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels[u]->ident, channels[u]->instance->name); - if(channels[u]->impl && channels[u]->instance->backend->channel_free){ - channels[u]->instance->backend->channel_free(channels[u]); - } - free(channels[u]); - channels[u] = NULL; - } - free(channels); - nchannels = 0; -} - - backend* backend_match(char* name){ size_t u; for(u = 0; u < registry.n; u++){ @@ -306,6 +303,24 @@ int backends_start(){ return rv; } +static void channels_free(){ + size_t u, p; + for(u = 0; u < sizeof(channels.n) / sizeof(channels.n[0]); u++){ + DBGPF("Cleaning up channel registry bucket %" PRIsize_t " with %" PRIsize_t " channels", u, channels.n[u]); + for(p = 0; p < channels.n[u]; p++){ + DBGPF("Destroying channel %" PRIu64 " on instance %s\n", channels.entry[u][p]->ident, channels.entry[u][p]->instance->name); + //call the channel_free function if the backend supports it + if(channels.entry[u][p]->impl && channels.entry[u][p]->instance->backend->channel_free){ + channels.entry[u][p]->instance->backend->channel_free(channels.entry[u][p]); + } + free(channels.entry[u][p]); + } + free(channels.entry[u]); + channels.entry[u] = NULL; + channels.n[u] = 0; + } +} + int backends_stop(){ size_t u, n; instance** inst = NULL; diff --git a/midimonster.h b/midimonster.h index 61467c1..75eb30a 100644 --- a/midimonster.h +++ b/midimonster.h @@ -192,8 +192,8 @@ typedef struct _backend_instance { /* * Instance channel structure - * Backends may either manage their own channel registry - * or use the memory returned by mm_channel() + * Backends may either manage their own channel registry or use the global + * channel store via the mm_channel() API */ typedef struct _backend_channel { instance* instance; @@ -218,25 +218,24 @@ MM_API int mm_backend_register(backend b); /* * Finds an instance matching the specified backend and identifier. - * Since setting an identifier for an instance is optional, - * this may not work depending on the backend. - * Instance identifiers may for example be set in the backends - * mmbackend_start call. + * Since setting an identifier for an instance is optional, this may not work + * depending on the backend. Instance identifiers may for example be set in the + * backends mmbackend_start call. */ MM_API instance* mm_instance_find(char* backend, uint64_t ident); /* - * Provides a pointer to a channel structure, pre-filled with - * the provided instance reference and identifier. - * The `create` parameter is a boolean flag indicating whether - * a channel matching the `ident` parameter should be created if - * none exists. If the instance already registered a channel - * matching `ident`, a pointer to it is returned. - * This API is just a convenience function. The array of channels is - * only used for mapping internally, creating and managing your own - * channel store is possible. When returning pointers from a - * backend-local channel store, the returned pointers must stay - * valid over the lifetime of the instance. + * Provides a pointer to a channel structure, pre-filled with the provided + * instance reference and identifier. + * The `create` parameter is a boolean flag indicating whether a channel + * matching the `ident` parameter should be created in the global channel store + * if none exists yet. If the instance already registered a channel matching + * `ident`, a pointer to the existing channel is returned. + * This API is just a convenience function. Creating and managing a + * backend-internal channel store is possible (and encouraged for performance + * reasons). When returning pointers from a backend-local channel store, the + * returned pointers must stay valid over the lifetime of the instance and + * provide valid `instance` members, as they are used for callbacks. * For each channel with a non-NULL `impl` field registered using * this function, the backend will receive a call to its channel_free * function (if it exists). @@ -244,17 +243,16 @@ MM_API instance* mm_instance_find(char* backend, uint64_t ident); MM_API channel* mm_channel(instance* i, uint64_t ident, uint8_t create); /* - * Register (manage = 1) or unregister (manage = 0) a file descriptor - * to be selected on. The backend will be notified when the descriptor - * becomes ready to read via its registered mmbackend_process_fd call. - * The `impl` argument will be provided within the corresponding - * managed_fd structure upon callback. + * Register (manage = 1) or unregister (manage = 0) a file descriptor to be + * selected on. The backend will be notified when the descriptor becomes ready + * to read via its registered mmbackend_process_fd call. The `impl` argument + * will be provided within the corresponding managed_fd structure upon callback. */ MM_API int mm_manage_fd(int fd, char* backend, int manage, void* impl); /* - * Notifies the core of a channel event. Called by backends to - * inject events gathered from their backing implementation. + * Notifies the core of a channel event. Called by backends to inject events + * gathered from their backing implementation. */ MM_API int mm_channel_event(channel* c, channel_value v); @@ -266,14 +264,14 @@ MM_API int mm_backend_instances(char* backend, size_t* n, instance*** i); /* * Query an internal timestamp, which is updated every core iteration. - * This timestamp should not be used as a performance counter, but can be - * used for timeouting. Resolution is milliseconds. + * This timestamp should not be used as a performance counter, but can be used + * for timeouting. Resolution is milliseconds. */ MM_API uint64_t mm_timestamp(); /* - * Create a channel-to-channel mapping. This API should not - * be used by backends. It is only exported for core modules. + * Create a channel-to-channel mapping. This API should not be used by backends. + * It is only exported for core modules. */ int mm_map_channel(channel* from, channel* to); #endif -- cgit v1.2.3 From d1baab6ce4b13c562867147ed60906dfe651ae81 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 21 Mar 2020 19:00:28 +0100 Subject: Enable setting channel values at load time for lua --- backends/lua.c | 29 +++++++++++++++++++---------- backends/lua.h | 1 + 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index b71ca97..7f80cc7 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -17,9 +17,8 @@ static lua_timer* timer = NULL; uint64_t timer_interval = 0; #ifdef MMBACKEND_LUA_TIMERFD static int timer_fd = -1; -#else -static uint64_t last_timestamp; #endif +static uint64_t last_timestamp = 0; static size_t threads = 0; static lua_thread* thread = NULL; @@ -224,7 +223,6 @@ static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - channel* channel = NULL; instance* inst = NULL; lua_instance_data* data = NULL; @@ -243,15 +241,21 @@ static int lua_callback_output(lua_State* interpreter){ channel_name = lua_tostring(interpreter, 1); val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); + //if not started yet, create any requested channels so scripts may set them at load time + if(!last_timestamp && channel_name){ + lua_channel(inst, (char*) channel_name, mmchannel_output); + } + //find correct channel & output value for(n = 0; n < data->channels; n++){ if(!strcmp(channel_name, data->channel[n].name)){ - channel = mm_channel(inst, n, 0); - if(!channel){ - return 0; - } - mm_channel_event(channel, val); data->channel[n].out = val.normalised; + if(!last_timestamp){ + data->channel[n].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, n, 0), val); + } return 0; } } @@ -589,6 +593,7 @@ static int lua_start(size_t n, instance** inst){ size_t u, p; lua_instance_data* data = NULL; int default_handler; + channel_value v; //resolve channels to their handler functions for(u = 0; u < n; u++){ @@ -608,6 +613,11 @@ static int lua_start(size_t n, instance** inst){ if(!data->default_handler){ data->channel[p].reference = lua_resolve_symbol(data->interpreter, data->channel[p].name); } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } } } @@ -617,9 +627,8 @@ static int lua_start(size_t n, instance** inst){ if(mm_manage_fd(timer_fd, BACKEND_NAME, 1, NULL)){ return 1; } - #else - last_timestamp = mm_timestamp(); #endif + last_timestamp = mm_timestamp(); return 0; } diff --git a/backends/lua.h b/backends/lua.h index 3c7568b..4583dfe 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -27,6 +27,7 @@ typedef struct /*_lua_channel*/ { int reference; double in; double out; + uint8_t mark; } lua_channel_data; typedef struct /*_lua_instance_data*/ { -- cgit v1.2.3 From aa02ccf3abf183207b24fcbb9460cbd904a698e2 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 22 Mar 2020 00:30:13 +0100 Subject: Enable load-time channel setting for python --- backends/python.c | 26 +++++++++++++++++--------- backends/python.h | 1 + 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/backends/python.c b/backends/python.c index c658f30..9f1d642 100644 --- a/backends/python.c +++ b/backends/python.c @@ -116,7 +116,6 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; const char* channel_name = NULL; - channel* chan = NULL; channel_value val = { {0} }; @@ -127,19 +126,22 @@ static PyObject* mmpy_output(PyObject* self, PyObject* args){ } val.normalised = clamp(val.normalised, 1.0, 0.0); + //if not started yet, create any requested channels so we can set them at load time + if(!last_timestamp){ + python_channel(inst, (char*) channel_name, mmchannel_output); + } for(u = 0; u < data->channels; u++){ if(!strcmp(data->channel[u].name, channel_name)){ DBGPF("Setting channel %s.%s to %f", inst->name, channel_name, val.normalised); - chan = mm_channel(inst, u, 0); - //this should never happen - if(!chan){ - LOGPF("Failed to fetch parsed channel %s.%s", inst->name, channel_name); - break; - } data->channel[u].out = val.normalised; - mm_channel_event(chan, val); - break; + if(!last_timestamp){ + data->channel[u].mark = 1; + } + else{ + mm_channel_event(mm_channel(inst, u, 0), val); + } + return 0; } } @@ -636,6 +638,7 @@ static PyObject* python_resolve_symbol(char* spec_raw){ static int python_start(size_t n, instance** inst){ python_instance_data* data = NULL; size_t u, p; + channel_value v; //resolve channel references to handler functions for(u = 0; u < n; u++){ @@ -656,6 +659,11 @@ static int python_start(size_t n, instance** inst){ else{ data->channel[p].handler = python_resolve_symbol(data->channel[p].name); } + //push initial values + if(data->channel[p].mark){ + v.normalised = data->channel[p].out; + mm_channel_event(mm_channel(inst[u], p, 0), v); + } } //release interpreter diff --git a/backends/python.h b/backends/python.h index a40098b..020aeac 100644 --- a/backends/python.h +++ b/backends/python.h @@ -16,6 +16,7 @@ typedef struct /*_python_channel_data*/ { PyObject* handler; double in; double out; + uint8_t mark; } mmpython_channel; typedef struct /*_mmpy_registered_socket*/ { -- cgit v1.2.3 From e8d87602618b4a9fab056f1d03b87c706d5f1f9f Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 23 Mar 2020 01:30:14 +0100 Subject: Update rtpmidi to current API --- backends/Makefile | 2 +- backends/rtpmidi.c | 23 ++++++----------------- backends/rtpmidi.h | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index 8956a20..c19d9dd 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -1,7 +1,7 @@ .PHONY: all clean full LINUX_BACKENDS = midi.so evdev.so WINDOWS_BACKENDS = artnet.dll osc.dll loopback.dll sacn.dll maweb.dll winmidi.dll openpixelcontrol.dll rtpmidi.dll -BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.dll +BACKENDS = artnet.so osc.so loopback.so sacn.so lua.so maweb.so jack.so openpixelcontrol.so python.so rtpmidi.so OPTIONAL_BACKENDS = ola.so BACKEND_LIB = libmmbackend.o diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index c39139a..2eec871 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -357,24 +357,17 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } -static instance* rtpmidi_instance(){ - rtpmidi_instance_data* data = NULL; - instance* inst = mm_instance(); - - if(!inst){ - return NULL; - } - - data = calloc(1, sizeof(rtpmidi_instance_data)); +static int rtpmidi_instance(instance* inst){ + rtpmidi_instance_data* data = calloc(1, sizeof(rtpmidi_instance_data)); if(!data){ LOG("Failed to allocate memory"); - return NULL; + return 1; } data->fd = -1; data->control_fd = -1; inst->impl = data; - return inst; + return 0; } static channel* rtpmidi_channel(instance* inst, char* spec, uint8_t flags){ @@ -910,7 +903,7 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - sendto(data->control_fd, &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers @@ -922,7 +915,7 @@ static int rtpmidi_service(){ //append session name to packet memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(data->control_fd, invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } } } @@ -947,10 +940,6 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ cfg.last_service = mm_timestamp(); } - if(!num){ - return 0; - } - for(u = 0; u < num; u++){ if(!fds[u].impl){ //TODO handle mDNS discovery input diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index db6237b..a08cf0f 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -6,7 +6,7 @@ MM_PLUGIN_API int init(); static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); -static instance* rtpmidi_instance(); +static int rtpmidi_instance(instance* inst); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); -- cgit v1.2.3 From 18db3856a3a3e81f3e2050e3f137e6e15103f9a4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 24 Mar 2020 23:04:12 +0100 Subject: Simplify backend code according to new guarantees --- backends/artnet.c | 5 ----- backends/evdev.c | 8 -------- backends/jack.c | 52 +++++++++++++++++++++++++--------------------------- backends/lua.c | 3 ++- backends/osc.c | 14 +++++--------- backends/sacn.c | 9 --------- backends/winmidi.c | 5 ----- 7 files changed, 32 insertions(+), 64 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index caab6e0..c59a79d 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -378,11 +378,6 @@ static int artnet_handle(size_t num, managed_fd* fds){ } } - if(!num){ - //early exit - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/evdev.c b/backends/evdev.c index af5ec74..8a14200 100644 --- a/backends/evdev.c +++ b/backends/evdev.c @@ -357,10 +357,6 @@ static int evdev_handle(size_t num, managed_fd* fds){ int read_status; struct input_event ev; - if(!num){ - return 0; - } - for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; if(!inst){ @@ -437,10 +433,6 @@ static int evdev_set(instance* inst, size_t num, channel** c, channel_value* v) int32_t value = 0; uint64_t range = 0; - if(!num){ - return 0; - } - if(!data->output_enabled){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; diff --git a/backends/jack.c b/backends/jack.c index c862096..d430c60 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -549,38 +549,36 @@ static int mmjack_handle(size_t num, managed_fd* fds){ ssize_t bytes; uint8_t recv_buf[1024]; - if(num){ - for(u = 0; u < num; u++){ - inst = (instance*) fds[u].impl; - data = (mmjack_instance_data*) inst->impl; - bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); - if(bytes < 0){ - LOGPF("Failed to receive on feedback socket for instance %s", inst->name); - return 1; - } + for(u = 0; u < num; u++){ + inst = (instance*) fds[u].impl; + data = (mmjack_instance_data*) inst->impl; + bytes = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); + if(bytes < 0){ + LOGPF("Failed to receive on feedback socket for instance %s", inst->name); + return 1; + } - for(p = 0; p < data->ports; p++){ - if(data->port[p].input && data->port[p].mark){ - pthread_mutex_lock(&data->port[p].lock); - switch(data->port[p].type){ - case port_cv: - mmjack_handle_cv(inst, p, data->port + p); - break; - case port_midi: - mmjack_handle_midi(inst, p, data->port + p); - break; - default: - LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); - break; - } - - data->port[p].mark = 0; - pthread_mutex_unlock(&data->port[p].lock); + for(p = 0; p < data->ports; p++){ + if(data->port[p].input && data->port[p].mark){ + pthread_mutex_lock(&data->port[p].lock); + switch(data->port[p].type){ + case port_cv: + mmjack_handle_cv(inst, p, data->port + p); + break; + case port_midi: + mmjack_handle_midi(inst, p, data->port + p); + break; + default: + LOGPF("Output handler not implemented for unknown channel type on %s.%s", inst->name, data->port[p].name); + break; } + + data->port[p].mark = 0; + pthread_mutex_unlock(&data->port[p].lock); } } } - + if(config.jack_shutdown){ LOG("Server disconnected"); return 1; diff --git a/backends/lua.c b/backends/lua.c index 7f80cc7..968193e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -471,7 +471,8 @@ static channel* lua_channel(instance* inst, char* spec, uint8_t flags){ return NULL; } - data->channel[u].in = data->channel[u].out = 0.0; + //initialize new channel + memset(data->channel + u, 0, sizeof(lua_channel_data)); data->channel[u].name = strdup(spec); if(!data->channel[u].name){ LOG("Failed to allocate memory"); diff --git a/backends/osc.c b/backends/osc.c index 754c290..f40f3f5 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -231,7 +231,7 @@ static int osc_path_validate(char* path, uint8_t allow_patterns){ char pattern_chars[] = "?[]{}*"; size_t u, c; uint8_t square_open = 0, curly_open = 0; - + if(path[0] != '/'){ LOGPF("%s is not a valid OSC path: Missing root /", path); return 1; @@ -331,7 +331,7 @@ static int osc_path_match(char* pattern, char* path){ } if(pattern[match_end + 1] == '-' && pattern[match_end + 2] != ']'){ - if((pattern[match_end] > pattern[match_end + 2] + if((pattern[match_end] > pattern[match_end + 2] && path[u] >= pattern[match_end + 2] && path[u] <= pattern[match_end]) || (pattern[match_end] <= pattern[match_end + 2] @@ -666,7 +666,7 @@ static int osc_output_channel(instance* inst, size_t channel){ memcpy(xmit_buf, data->root, strlen(data->root)); offset += strlen(data->root); } - + memcpy(xmit_buf + offset, data->channel[channel].path, strlen(data->channel[channel].path)); offset += strlen(data->channel[channel].path) + 1; offset = osc_align(offset); @@ -703,16 +703,12 @@ static int osc_output_channel(instance* inst, size_t channel){ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ size_t evt = 0, mark = 0; int rv = 0; + osc_instance_data* data = (osc_instance_data*) inst->impl; osc_channel_ident ident = { .label = 0 }; osc_parameter_value current; - if(!num){ - return 0; - } - - osc_instance_data* data = (osc_instance_data*) inst->impl; if(!data->dest_len){ LOGPF("Instance %s does not have a destination, output is disabled (%" PRIsize_t " channels)", inst->name, num); return 0; @@ -747,7 +743,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ mark = 1; } } - + if(mark){ //output all marked channels for(evt = 0; !rv && evt < num; evt++){ diff --git a/backends/sacn.c b/backends/sacn.c index bd5c75a..bce3887 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -330,10 +330,6 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ uint32_t frame_delta = 0; sacn_instance_data* data = (sacn_instance_data*) inst->impl; - if(!num){ - return 0; - } - if(!data->xmit_prio){ LOGPF("Instance %s not enabled for output (%" PRIsize_t " channel events)", inst->name, num); return 0; @@ -554,11 +550,6 @@ static int sacn_handle(size_t num, managed_fd* fds){ } } - //early exit - if(!num){ - return 0; - } - for(u = 0; u < num; u++){ do{ bytes_read = recv(fds[u].fd, recv_buf, sizeof(recv_buf), 0); diff --git a/backends/winmidi.c b/backends/winmidi.c index d9b3047..753f25c 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -185,11 +185,6 @@ static int winmidi_set(instance* inst, size_t num, channel** c, channel_value* v }; size_t u; - //early exit - if(!num){ - return 0; - } - if(!data->device_out){ LOGPF("Instance %s has no output device", inst->name); return 0; -- cgit v1.2.3 From 2a079f72483aa853d68430883b2281f436512c6b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 26 Mar 2020 22:42:38 +0100 Subject: Implement lua cleanup handlers --- TODO | 3 --- backends/lua.c | 74 +++++++++++++++++++++++++++++++++++++-------------------- backends/lua.h | 1 + backends/lua.md | 14 ++++++++--- 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/TODO b/TODO index 900cc1b..ccad973 100644 --- a/TODO +++ b/TODO @@ -1,9 +1,6 @@ keepalive channels per backend? Note source in channel value struct -Optimize core channel search (store backend offset) udp backends may ignore MTU -mm_managed_fd.impl is not freed currently (and is heaped most of the time anyway) -> documentation make event collectors threadsafe to stop marshalling data... collect & check backend API version windows strerror -move all connection establishment to _start to be able to hot-stop/start all backends diff --git a/backends/lua.c b/backends/lua.c index 968193e..7424f65 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -155,8 +155,19 @@ static void lua_thread_resume(size_t current_thread){ lua_settable(thread[current_thread].thread, LUA_REGISTRYINDEX); } -static int lua_callback_thread(lua_State* interpreter){ +static instance* lua_fetch_instance(lua_State* interpreter){ instance* inst = NULL; + + //get instance pointer from registry + lua_pushstring(interpreter, LUA_REGISTRY_KEY); + lua_gettable(interpreter, LUA_REGISTRYINDEX); + inst = (instance*) lua_touserdata(interpreter, -1); + lua_pop(interpreter, 1); + return inst; +} + +static int lua_callback_thread(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); size_t u = threads; if(lua_gettop(interpreter) != 1){ LOGPF("Thread function called with %d arguments, expected function", lua_gettop(interpreter)); @@ -165,11 +176,6 @@ static int lua_callback_thread(lua_State* interpreter){ luaL_checktype(interpreter, 1, LUA_TFUNCTION); - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - //make space for a new thread thread = realloc(thread, (threads + 1) * sizeof(lua_thread)); if(!thread){ @@ -223,20 +229,14 @@ static int lua_callback_output(lua_State* interpreter){ size_t n = 0; channel_value val; const char* channel_name = NULL; - instance* inst = NULL; - lua_instance_data* data = NULL; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 2){ LOGPF("Output function called with %d arguments, expected 2 (string, number)", lua_gettop(interpreter)); return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - data = (lua_instance_data*) inst->impl; - //fetch function parameters channel_name = lua_tostring(interpreter, 1); val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); @@ -264,6 +264,28 @@ static int lua_callback_output(lua_State* interpreter){ return 0; } +static int lua_callback_cleanup_handler(lua_State* interpreter){ + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; + int current_handler = data->cleanup_handler; + + if(lua_gettop(interpreter) != 1){ + LOGPF("Cleanup handler function called with %d arguments, expected 1 (function)", lua_gettop(interpreter)); + return 0; + } + + luaL_checktype(interpreter, 1, LUA_TFUNCTION); + + data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); + if(current_handler == LUA_NOREF){ + lua_pushnil(interpreter); + return 1; + } + lua_rawgeti(interpreter, LUA_REGISTRYINDEX, current_handler); + luaL_unref(interpreter, LUA_REGISTRYINDEX, current_handler); + return 1; +} + static int lua_callback_interval(lua_State* interpreter){ size_t n = 0; uint64_t interval = 0; @@ -274,10 +296,6 @@ static int lua_callback_interval(lua_State* interpreter){ return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - //fetch and round the interval interval = luaL_checkinteger(interpreter, 2); if(interval % 10 < 5){ @@ -341,21 +359,15 @@ static int lua_callback_interval(lua_State* interpreter){ static int lua_callback_value(lua_State* interpreter, uint8_t input){ size_t n = 0; - instance* inst = NULL; - lua_instance_data* data = NULL; const char* channel_name = NULL; + instance* inst = lua_fetch_instance(interpreter); + lua_instance_data* data = (lua_instance_data*) inst->impl; if(lua_gettop(interpreter) != 1){ LOGPF("get_value function called with %d arguments, expected 1 (string)", lua_gettop(interpreter)); return 0; } - //get instance pointer from registry - lua_pushstring(interpreter, LUA_REGISTRY_KEY); - lua_gettable(interpreter, LUA_REGISTRYINDEX); - inst = (instance*) lua_touserdata(interpreter, -1); - data = (lua_instance_data*) inst->impl; - //fetch argument channel_name = lua_tostring(interpreter, 1); @@ -425,6 +437,7 @@ static int lua_instance(instance* inst){ //load the interpreter data->interpreter = luaL_newstate(); + data->cleanup_handler = LUA_NOREF; if(!data->interpreter){ LOG("Failed to initialize interpreter"); free(data); @@ -441,6 +454,7 @@ static int lua_instance(instance* inst){ lua_register(data->interpreter, "timestamp", lua_callback_timestamp); lua_register(data->interpreter, "thread", lua_callback_thread); lua_register(data->interpreter, "sleep", lua_callback_sleep); + lua_register(data->interpreter, "cleanup_handler", lua_callback_cleanup_handler); //store instance pointer to the lua state lua_pushstring(data->interpreter, LUA_REGISTRY_KEY); @@ -578,6 +592,7 @@ static int lua_resolve_symbol(lua_State* interpreter, char* symbol){ || !strcmp(symbol, "output_value") || !strcmp(symbol, "input_channel") || !strcmp(symbol, "timestamp") + || !strcmp(symbol, "cleanup_handler") || !strcmp(symbol, "interval")){ return LUA_NOREF; } @@ -639,6 +654,13 @@ static int lua_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (lua_instance_data*) inst[u]->impl; + + //call cleanup function if one is registered + if(data->cleanup_handler != LUA_NOREF){ + lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); + lua_pcall(data->interpreter, 0, 0, 0); + } + //stop the interpreter lua_close(data->interpreter); //cleanup channel data diff --git a/backends/lua.h b/backends/lua.h index 4583dfe..5587bf9 100644 --- a/backends/lua.h +++ b/backends/lua.h @@ -35,6 +35,7 @@ typedef struct /*_lua_instance_data*/ { lua_channel_data* channel; lua_State* interpreter; + int cleanup_handler; char* default_handler; } lua_instance_data; diff --git a/backends/lua.md b/backends/lua.md index 05509b6..30d7580 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -13,14 +13,15 @@ The following functions are provided within the Lua interpreter for interaction | Function | Usage example | Description | |-------------------------------|-------------------------------|---------------------------------------| -| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel | +| `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | -| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel | -| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel | +| `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | +| `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | +| `cleanup_handler(function)` | | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example script: ```lua @@ -43,8 +44,12 @@ function run_show() end end +function save_values() +end + interval(toggle, 1000) thread(run_show) +cleanup_handler(save_values) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -86,6 +91,9 @@ be called. Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + To build (and run) the `lua` backend on Windows, a compiled version of the Lua 5.3 library is required. For various reasons (legal, separations of concern, not wanting to ship binary data in the repository), the MIDIMonster project can not provide this file within this repository. -- cgit v1.2.3 From 253125ea28925e5207c375987ac36468327bed66 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 27 Mar 2020 21:40:45 +0100 Subject: Implement python cleanup handlers --- backends/lua.c | 9 +++-- backends/lua.md | 3 +- backends/python.c | 99 ++++++++++++++++++++++++++++++++++++------------------ backends/python.h | 1 + backends/python.md | 16 ++++++--- 5 files changed, 87 insertions(+), 41 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 7424f65..127933a 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -274,10 +274,13 @@ static int lua_callback_cleanup_handler(lua_State* interpreter){ return 0; } - luaL_checktype(interpreter, 1, LUA_TFUNCTION); + if(lua_type(interpreter, 1) != LUA_TFUNCTION && lua_type(interpreter, 1) != LUA_TNIL){ + LOG("Cleanup handler function parameter was neither nil nor a function"); + return 0; + } data->cleanup_handler = luaL_ref(interpreter, LUA_REGISTRYINDEX); - if(current_handler == LUA_NOREF){ + if(current_handler == LUA_NOREF || current_handler == LUA_REFNIL){ lua_pushnil(interpreter); return 1; } @@ -656,7 +659,7 @@ static int lua_shutdown(size_t n, instance** inst){ data = (lua_instance_data*) inst[u]->impl; //call cleanup function if one is registered - if(data->cleanup_handler != LUA_NOREF){ + if(data->cleanup_handler != LUA_NOREF && data->cleanup_handler != LUA_REFNIL){ lua_rawgeti(data->interpreter, LUA_REGISTRYINDEX, data->cleanup_handler); lua_pcall(data->interpreter, 0, 0, 0); } diff --git a/backends/lua.md b/backends/lua.md index 30d7580..e59e513 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -15,13 +15,13 @@ The following functions are provided within the Lua interpreter for interaction |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | | `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | +| `cleanup_handler(function)` | `cleanup_handler(shutdown)` | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | | `input_channel()` | `print(input_channel())` | Returns the name of the input channel whose handler function is currently running or `nil` if in an `interval`'ed function (or the initial parse step) | | `timestamp()` | `print(timestamp())` | Returns the core timestamp for this iteration with millisecond resolution. This is not a performance timer, but intended for timeouting, etc | | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | -| `cleanup_handler(function)` | | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example script: ```lua @@ -45,6 +45,7 @@ function run_show() end function save_values() + -- Store state to a file, for example end interval(toggle, 1000) diff --git a/backends/python.c b/backends/python.c index 9f1d642..4c9248d 100644 --- a/backends/python.c +++ b/backends/python.c @@ -257,6 +257,33 @@ static PyObject* mmpy_interval(PyObject* self, PyObject* args){ return Py_None; } +static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ + instance* inst = *((instance**) PyModule_GetState(self)); + python_instance_data* data = (python_instance_data*) inst->impl; + PyObject* current_handler = data->cleanup_handler; + + if(!PyArg_ParseTuple(args, "O", &(data->cleanup_handler)) + || (data->cleanup_handler != Py_None && !PyCallable_Check(data->cleanup_handler))){ + data->cleanup_handler = current_handler; + return NULL; + } + + if(data->cleanup_handler == Py_None){ + data->cleanup_handler = NULL; + } + else{ + Py_INCREF(data->cleanup_handler); + } + + if(!current_handler){ + Py_INCREF(Py_None); + return Py_None; + } + + Py_DECREF(current_handler); + return current_handler; +} + static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ instance* inst = *((instance**) PyModule_GetState(self)); python_instance_data* data = (python_instance_data*) inst->impl; @@ -264,12 +291,10 @@ static PyObject* mmpy_manage_fd(PyObject* self, PyObject* args){ size_t u = 0, last_free = 0; int fd = -1; - if(!PyArg_ParseTuple(args, "OO", &handler, &sock)){ - return NULL; - } - - if(handler != Py_None && !PyCallable_Check(handler)){ - PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable"); + if(!PyArg_ParseTuple(args, "OO", &handler, &sock) + || sock == Py_None + || (handler != Py_None && !PyCallable_Check(handler))){ + PyErr_SetString(PyExc_TypeError, "manage() requires either None or a callable and a socket-like object"); return NULL; } @@ -398,13 +423,14 @@ static PyObject* mmpy_init(){ }; static PyMethodDef mmpy_methods[] = { - {"output", mmpy_output, METH_VARARGS, "Output a channel event"}, - {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel"}, - {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel"}, + {"output", mmpy_output, METH_VARARGS, "Output a channel event on the instance"}, + {"inputvalue", mmpy_input_value, METH_VARARGS, "Get last input value for a channel on the instance"}, + {"outputvalue", mmpy_output_value, METH_VARARGS, "Get the last output value for a channel on the instance"}, {"current", mmpy_current_handler, METH_VARARGS, "Get the name of the currently executing channel handler"}, {"timestamp", mmpy_timestamp, METH_VARARGS, "Get the core timestamp (in milliseconds)"}, {"manage", mmpy_manage_fd, METH_VARARGS, "(Un-)register a socket or file descriptor for notifications"}, {"interval", mmpy_interval, METH_VARARGS, "Register or update an interval handler"}, + {"cleanup_handler", mmpy_cleanup_handler, METH_VARARGS, "Register or update the instances cleanup handler"}, {0} }; @@ -674,29 +700,44 @@ static int python_start(size_t n, instance** inst){ static int python_shutdown(size_t n, instance** inst){ size_t u, p; + PyObject* result = NULL; python_instance_data* data = NULL; - //clean up channels - //this needs to be done before stopping the interpreters, - //because the handler references are refcounted - for(u = 0; u < n; u++){ - data = (python_instance_data*) inst[u]->impl; - for(p = 0; p < data->channels; p++){ - free(data->channel[p].name); - Py_XDECREF(data->channel[p].handler); + //if there are no instances, the python interpreter is not started, so cleanup can be skipped + if(python_main){ + //release interval references + for(p = 0; p < intervals; p++){ + //swap to interpreter + PyEval_RestoreThread(interval[p].interpreter); + Py_XDECREF(interval[p].reference); + PyEval_ReleaseThread(interval[p].interpreter); } - free(data->channel); - free(data->default_handler); - //do not free data here, needed for shutting down interpreters - } - if(python_main){ - //just used to lock the GIL + //lock the GIL for later interpreter release PyEval_RestoreThread(python_main); for(u = 0; u < n; u++){ data = (python_instance_data*) inst[u]->impl; + //swap to interpreter to be safe for releasing the references + PyThreadState_Swap(data->interpreter); + + //run cleanup handler before cleaning up channel data to allow reading channel data + if(data->cleanup_handler){ + result = PyObject_CallFunction(data->cleanup_handler, NULL); + Py_XDECREF(result); + Py_XDECREF(data->cleanup_handler); + } + + //clean up channels + for(p = 0; p < data->channels; p++){ + free(data->channel[p].name); + Py_XDECREF(data->channel[p].handler); + } + free(data->channel); + free(data->default_handler); + Py_XDECREF(data->handler); + //close sockets for(p = 0; p < data->sockets; p++){ close(data->socket[p].fd); //FIXME does python do this on its own? @@ -704,26 +745,18 @@ static int python_shutdown(size_t n, instance** inst){ Py_XDECREF(data->socket[p].handler); } - //release interval references - for(p = 0; p handler); - + //shut down interpreter, GIL is held after this but state is NULL DBGPF("Shutting down interpreter for instance %s", inst[u]->name); - //swap to interpreter and end it, GIL is held after this but state is NULL - PyThreadState_Swap(data->interpreter); PyErr_Clear(); //PyThreadState_Clear(data->interpreter); Py_EndInterpreter(data->interpreter); - free(data); } //shut down main interpreter PyThreadState_Swap(python_main); if(Py_FinalizeEx()){ - LOG("Failed to destroy python interpreters"); + LOG("Failed to shut down python library"); } PyMem_RawFree(program_name); } diff --git a/backends/python.h b/backends/python.h index 020aeac..539389b 100644 --- a/backends/python.h +++ b/backends/python.h @@ -45,4 +45,5 @@ typedef struct /*_python_instance_data*/ { char* default_handler; PyObject* handler; + PyObject* cleanup_handler; } python_instance_data; diff --git a/backends/python.md b/backends/python.md index 6852a79..ab0fb38 100644 --- a/backends/python.md +++ b/backends/python.md @@ -17,13 +17,14 @@ The `midimonster` module provides the following functions: | Function | Usage example | Description | |-------------------------------|---------------------------------------|-----------------------------------------------| -| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel | -| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel | -| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel | +| `output(string, float)` | `midimonster.output("foo", 0.75)` | Output a value event to a channel on this instance | +| `inputvalue(string)` | `midimonster.inputvalue("foo")` | Get the last input value on a channel of this instance | +| `outputvalue(string)` | `midimonster.outputvalue("bar")` | Get the last output value on a channel of this instance | | `current()` | `print(midimonster.current())` | Returns the name of the input channel whose handler function is currently running or `None` if the interpreter was called from another context | | `timestamp()` | `print(midimonster.timestamp())` | Get the internal core timestamp (in milliseconds) | | `interval(function, long)` | `midimonster.interval(toggle, 100)` | Register a function to be called periodically. Interval is specified in milliseconds (accurate to 10msec). Calling `interval` with the same function again updates the interval. Specifying the interval as `0` cancels the interval | -| `manage(function, socket)` | `midimonster.manage(handler, socket)`| Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler | +| `manage(function, socket)` | `midimonster.manage(handler, socket)` | Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler | +| `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | Example Python module: ```python @@ -48,12 +49,16 @@ def socket_handler(sock): def ping(): print(midimonster.timestamp()) +def save_positions(): + # Store some data to disk + # Register an interval midimonster.interval(ping, 1000) # Create and register a client socket (add error handling as you like) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", 8990)) midimonster.manage(socket_handler, s) +midimonster.cleanup_handler(save_positions) ``` Input values range between 0.0 and 1.0, output values are clamped to the same range. @@ -92,6 +97,9 @@ py1.out1 > py2.module.handler Output values will not trigger corresponding input event handlers unless the channel is mapped back in the MIDIMonster configuration. This is intentional. +Output events generated from cleanup handlers called during shutdown will not be routed, as the core +routing facility has already shut down at this point. There are no plans to change this behaviour. + Importing a Python module named `midimonster` is probably a bad idea and thus unsupported. The MIDIMonster is, at its core, single-threaded. Do not try to use Python's `threading` -- cgit v1.2.3 From 5fdb638454ce90eb565555dfece5030a2ec4a576 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Mar 2020 01:09:58 +0100 Subject: Fix reference counting bug and lua timing --- backends/lua.c | 6 ++---- backends/python.c | 7 +++++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index 127933a..e2f3b0e 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -533,7 +533,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ } static int lua_handle(size_t num, managed_fd* fds){ - uint64_t delta = timer_interval; + uint64_t delta = mm_timestamp() - last_timestamp; + last_timestamp = mm_timestamp(); size_t n; #ifdef MMBACKEND_LUA_TIMERFD @@ -547,9 +548,6 @@ static int lua_handle(size_t num, managed_fd* fds){ LOGPF("Failed to read timer: %s", strerror(errno)); return 1; } - #else - delta = mm_timestamp() - last_timestamp; - last_timestamp = mm_timestamp(); #endif //no timers active diff --git a/backends/python.c b/backends/python.c index 4c9248d..94f8e24 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,4 +1,5 @@ #define BACKEND_NAME "python" +#define DEBUG #define PY_SSIZE_T_CLEAN #include @@ -269,9 +270,11 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ } if(data->cleanup_handler == Py_None){ + DBGPF("Cleanup handler removed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); data->cleanup_handler = NULL; } else{ + DBGPF("Cleanup handler installed on %s (previously %s)", inst->name, current_handler ? "active" : "inactive"); Py_INCREF(data->cleanup_handler); } @@ -280,7 +283,7 @@ static PyObject* mmpy_cleanup_handler(PyObject* self, PyObject* args){ return Py_None; } - Py_DECREF(current_handler); + //do not decrease refcount on current_handler here as the reference may be used by python code again return current_handler; } @@ -595,6 +598,7 @@ static int python_handle(size_t num, managed_fd* fds){ //if timer expired, call handler if(interval[u].delta >= interval[u].interval){ interval[u].delta %= interval[u].interval; + DBGPF("Calling interval handler %" PRIsize_t ", last delta %" PRIu64, u, delta); //swap to interpreter PyEval_RestoreThread(interval[u].interpreter); @@ -603,7 +607,6 @@ static int python_handle(size_t num, managed_fd* fds){ Py_XDECREF(result); //release interpreter PyEval_ReleaseThread(interval[u].interpreter); - DBGPF("Calling interval handler %" PRIsize_t, u); } } } -- cgit v1.2.3 From 144e77a2212b2d2f0e3bd689fd7dc231d86a07a6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Mar 2020 17:23:21 +0100 Subject: Add initial development guide --- DEVELOPMENT.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 DEVELOPMENT.md diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..0e677eb --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,40 @@ +# MIDIMonster development guide + +This document serves as a reference for contributors interested in the low-level implementation +of the MIDIMonster. It is currently a work in progress and will be extended as problems come +up and need solving ;) + +## Basics + +All rules are meant as guidelines. There may be situations where they need to be applied +in spirit rather than by the letter. + +### Architectural guidelines + +* Change in functionality or behaviour requires a change in documentation. +* There is more honor in deleting code than there is in adding code. +* The `master` branch must build successfully. Test breaking changes in a branch. +* Commit messages should be in the imperative voice ("When applied, this commit will: "). +* The working language for this repository is english. + +### Code style + +* Tabs for indentations, spaces for word separation +* Lines may not end in spaces or tabs +* There should be no two consecutive spaces (or spaces intermixed with tabs) +* There should be no two consecutive newlines +* All symbol names in `snake_case` except where mandated by external interfaces +* When possible, prefix symbol names with their "namespace" (ie. the relevant section or module name) +* Variables should be appropriately named for what they do + * The name length should be (positively) correlated with usage + * Loop counters may be one-character letters + * Prefer to name unsigned loop counters `u` and signed ones `i` + +#### C specific + +* Prefer lazy designated initializers to `memset()` +* Avoid `atoi`/`itoa`, use `strto[u]l[l]()` and `snprintf()` + +## Architecture + +TBD -- cgit v1.2.3 From 126d9945eb3f4289975198b7437defb8bb13be73 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 28 Mar 2020 23:32:39 +0100 Subject: Extend development guidelines --- DEVELOPMENT.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 0e677eb..90c63ce 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -13,9 +13,12 @@ in spirit rather than by the letter. * Change in functionality or behaviour requires a change in documentation. * There is more honor in deleting code than there is in adding code. + * Corollary: Code is a liability, not an asset. + * But: Benchmark the naive implementation before optimizing prematurely. * The `master` branch must build successfully. Test breaking changes in a branch. * Commit messages should be in the imperative voice ("When applied, this commit will: "). * The working language for this repository is english. +* External dependencies are only acceptable when necessary and available from package repositories. ### Code style @@ -29,11 +32,14 @@ in spirit rather than by the letter. * The name length should be (positively) correlated with usage * Loop counters may be one-character letters * Prefer to name unsigned loop counters `u` and signed ones `i` +* Place comments above the section they are commenting on + * Use inline comments sparingly #### C specific * Prefer lazy designated initializers to `memset()` -* Avoid `atoi`/`itoa`, use `strto[u]l[l]()` and `snprintf()` +* Avoid `atoi()`/`itoa()`, use `strto[u]l[l]()` and `snprintf()` +* Avoid unsafe functions without explicit bounds parameters (eg. `strcat()`). ## Architecture -- cgit v1.2.3 From cf6a8e9a0f125633b1ef7d84e10935695598e654 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Mar 2020 11:14:58 +0200 Subject: Reschedule artnet frame output on EAGAIN --- backends/artnet.c | 38 ++++++++++++++++++++++++++------------ backends/python.c | 1 - 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index c59a79d..585895b 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -206,10 +206,10 @@ static channel* artnet_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int artnet_transmit(instance* inst){ - size_t u; +static int artnet_transmit(instance* inst, artnet_output_universe* output){ artnet_instance_data* data = (artnet_instance_data*) inst->impl; - //output frame + + //build output frame artnet_pkt frame = { .magic = {'A', 'r', 't', '-', 'N', 'e', 't', 0x00}, .opcode = htobe16(OpDmx), @@ -224,16 +224,30 @@ static int artnet_transmit(instance* inst){ memcpy(frame.data, data->data.out, 512); if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } + //reschedule frame output + output->mark = 1; + if(!next_frame || next_frame > ARTNET_SYNTHESIZE_MARGIN){ + next_frame = ARTNET_SYNTHESIZE_MARGIN; + } + return 0; } //update last frame timestamp - for(u = 0; u < artnet_fd[data->fd_index].output_instances; u++){ - if(artnet_fd[data->fd_index].output_instance[u].label == inst->ident){ - artnet_fd[data->fd_index].output_instance[u].last_frame = mm_timestamp(); - artnet_fd[data->fd_index].output_instance[u].mark = 0; - } - } + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -285,7 +299,7 @@ static int artnet_set(instance* inst, size_t num, channel** c, channel_value* v) } return 0; } - return artnet_transmit(inst); + return artnet_transmit(inst, artnet_fd[data->fd_index].output_instance + u); } return 0; @@ -366,7 +380,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ || synthesize_delta >= ARTNET_KEEPALIVE_INTERVAL){ //keepalive timeout inst = mm_instance_find(BACKEND_NAME, artnet_fd[u].output_instance[c].label); if(inst){ - artnet_transmit(inst); + artnet_transmit(inst, artnet_fd[u].output_instance + c); } } diff --git a/backends/python.c b/backends/python.c index 94f8e24..28b95a9 100644 --- a/backends/python.c +++ b/backends/python.c @@ -1,5 +1,4 @@ #define BACKEND_NAME "python" -#define DEBUG #define PY_SSIZE_T_CLEAN #include -- cgit v1.2.3 From 3c6d2ccce4dd9be3f6497fb75d0456900e16ab0c Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 29 Mar 2020 11:28:44 +0200 Subject: Reschedule sacn frame output on EAGAIN --- DEVELOPMENT.md | 6 ++++-- backends/sacn.c | 64 +++++++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 90c63ce..495c4f9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -41,6 +41,8 @@ in spirit rather than by the letter. * Avoid `atoi()`/`itoa()`, use `strto[u]l[l]()` and `snprintf()` * Avoid unsafe functions without explicit bounds parameters (eg. `strcat()`). -## Architecture +# Build pipeline -TBD +# Architecture + +# Debugging diff --git a/backends/sacn.c b/backends/sacn.c index bce3887..b3376d7 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -275,9 +275,10 @@ static channel* sacn_channel(instance* inst, char* spec, uint8_t flags){ return data->data.channel + chan_a; } -static int sacn_transmit(instance* inst){ - size_t u; +static int sacn_transmit(instance* inst, sacn_output_universe* output){ sacn_instance_data* data = (sacn_instance_data*) inst->impl; + + //build sacn frame sacn_data_pdu pdu = { .root = { .preamble_size = htobe16(0x10), @@ -312,16 +313,31 @@ static int sacn_transmit(instance* inst){ memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - } + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output frame for instance %s: %s", inst->name, error); + LocalFree(error); + #endif + return 1; + } - //update last transmit timestamp, unmark instance - for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ - global_cfg.fd[data->fd_index].universe[u].last_frame = mm_timestamp(); - global_cfg.fd[data->fd_index].universe[u].mark = 0; + //reschedule output + output->mark = 1; + if(!global_cfg.next_frame || global_cfg.next_frame > SACN_SYNTHESIZE_MARGIN){ + global_cfg.next_frame = SACN_SYNTHESIZE_MARGIN; } + return 0; } + + //update last transmit timestamp, unmark instance + output->last_frame = mm_timestamp(); + output->mark = 0; return 0; } @@ -357,14 +373,14 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ //send packet if required if(mark){ - if(!data->realtime){ - //find output instance data - for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ - if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ - break; - } + //find output instance data + for(u = 0; u < global_cfg.fd[data->fd_index].universes; u++){ + if(global_cfg.fd[data->fd_index].universe[u].universe == data->uni){ + break; } + } + if(!data->realtime){ frame_delta = mm_timestamp() - global_cfg.fd[data->fd_index].universe[u].last_frame; //check if ratelimiting engaged @@ -376,7 +392,7 @@ static int sacn_set(instance* inst, size_t num, channel** c, channel_value* v){ return 0; } } - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[data->fd_index].universe + u); } return 0; @@ -496,7 +512,19 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + #ifndef _WIN32 + if(errno != EAGAIN){ + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); + } + #else + if(WSAGetLastError() != WSAEWOULDBLOCK){ + char* error = NULL; + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, error); + LocalFree(error); + } + #endif } } } @@ -537,7 +565,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ instance_id.fields.uni = global_cfg.fd[u].universe[c].universe; inst = mm_instance_find(BACKEND_NAME, instance_id.label); if(inst){ - sacn_transmit(inst); + sacn_transmit(inst, global_cfg.fd[u].universe + c); } } -- cgit v1.2.3 From 1891979d878e946941c6f236c3393cae943a4f1d Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 30 Mar 2020 03:15:36 +0200 Subject: Fix Coverity CID 355268 --- backends/lua.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/backends/lua.c b/backends/lua.c index e2f3b0e..d7f2643 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -239,10 +239,15 @@ static int lua_callback_output(lua_State* interpreter){ //fetch function parameters channel_name = lua_tostring(interpreter, 1); + if(!channel_name){ + LOG("Output function called with invalid channel specification"); + return 0; + } + val.normalised = clamp(luaL_checknumber(interpreter, 2), 1.0, 0.0); //if not started yet, create any requested channels so scripts may set them at load time - if(!last_timestamp && channel_name){ + if(!last_timestamp){ lua_channel(inst, (char*) channel_name, mmchannel_output); } @@ -373,6 +378,10 @@ static int lua_callback_value(lua_State* interpreter, uint8_t input){ //fetch argument channel_name = lua_tostring(interpreter, 1); + if(!channel_name){ + LOG("get_value function called with invalid channel specification"); + return 0; + } //find correct channel & return value for(n = 0; n < data->channels; n++){ -- cgit v1.2.3 From bc6b25e14f3ddd8a405a974a4a2e03b9a71d4c9d Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 2 Apr 2020 23:52:28 +0200 Subject: Implement basic DNS parsing --- backends/rtpmidi.c | 246 +++++++++++++++++++++++++++++++++++++++++++--------- backends/rtpmidi.h | 33 +++++++ backends/rtpmidi.md | 4 +- 3 files changed, 237 insertions(+), 46 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 2eec871..97364cf 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -11,9 +11,9 @@ #include "rtpmidi.h" //TODO learn peer ssrcs -//TODO default session join? //TODO default mode? //TODO internal loop mode +//FIXME what qualifies an instance to be announced via mdns? static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -41,6 +41,7 @@ MM_PLUGIN_API int init(){ .conf_instance = rtpmidi_configure_instance, .channel = rtpmidi_channel, .handle = rtpmidi_set, + .interval = rtpmidi_interval, .process = rtpmidi_handle, .start = rtpmidi_start, .shutdown = rtpmidi_shutdown @@ -59,38 +60,90 @@ MM_PLUGIN_API int init(){ return 0; } -static int rtpmidi_configure(char* option, char* value){ - char* host = NULL, *port = NULL; +static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* out){ + size_t offset = 0, output_offset = 0; + uint8_t current_label = 0; + uint16_t ptr_target = 0; - if(!strcmp(option, "mdns-name")){ - if(cfg.mdns_name){ - LOG("Duplicate mdns-name assignment"); - return 1; + //reset output data length and terminate null name + out->length = 0; + if(out->name){ + out->name[0] = 0; + } + + while(start + offset < len){ + current_label = buffer[start + offset]; + + //if we're at a pointer, move there and stop counting data length + if(DNS_POINTER(current_label)){ + if(start + offset + 1 >= len){ + LOG("mDNS internal pointer out of bounds"); + return 1; + } + + //do this before setting the target + if(!ptr_target){ + out->length += 2; + } + + //calculate pointer target + ptr_target = DNS_LABEL_LENGTH(current_label) << 8 | buffer[start + offset + 1]; + + if(ptr_target >= len){ + LOG("mDNS internal pointer target out of bounds"); + return 1; + } + start = ptr_target; + offset = 0; } + else{ + if(DNS_LABEL_LENGTH(current_label) == 0){ + if(!ptr_target){ + out->length++; + } + break; + } - cfg.mdns_name = strdup(value); - if(!cfg.mdns_name){ - LOG("Failed to allocate memory"); - return 1; + //check whether we have the bytes we need + if(start + offset + DNS_LABEL_LENGTH(current_label) > len){ + LOG("mDNS bytes missing"); + return 1; + } + + //check whether we have space in the output + if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){ + out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t)); + out->alloc = output_offset + DNS_LABEL_LENGTH(current_label); + } + + //copy data from this label to output buffer + memcpy(out->name + output_offset, buffer + start + offset + 1, DNS_LABEL_LENGTH(current_label)); + output_offset += DNS_LABEL_LENGTH(current_label) + 1; + offset += DNS_LABEL_LENGTH(current_label) + 1; + out->name[output_offset - 1] = '.'; + out->name[output_offset] = 0; + if(!ptr_target){ + out->length = offset; + } } - return 0; } - else if(!strcmp(option, "mdns-bind")){ - if(cfg.mdns_fd >= 0){ - LOG( "Only one mDNS discovery bind is supported"); - return 1; - } + return 0; +} - mmbackend_parse_hostspec(value, &host, &port, NULL); +static uint32_t rtpmidi_interval(){ + return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); +} - if(!host){ - LOGPF("Not a valid mDNS bind address: %s", value); +static int rtpmidi_configure(char* option, char* value){ + if(!strcmp(option, "mdns-name")){ + if(cfg.mdns_name){ + LOG("Duplicate mdns-name assignment"); return 1; } - cfg.mdns_fd = mmbackend_socket(host, (port ? port : RTPMIDI_MDNS_PORT), SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - LOGPF("Failed to bind mDNS interface: %s", value); + cfg.mdns_name = strdup(value); + if(!cfg.mdns_name){ + LOG("Failed to allocate memory"); return 1; } return 0; @@ -925,6 +978,79 @@ static int rtpmidi_service(){ return 0; } +static int rtpmidi_handle_mdns(){ + uint8_t buffer[RTPMIDI_PACKET_BUFFER]; + dns_header* hdr = (dns_header*) buffer; + dns_rr* rr = NULL; + dns_name name = { + .alloc = 0 + }; + ssize_t bytes = 0; + size_t u = 0, offset = sizeof(dns_header); + + for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ + if(bytes < sizeof(dns_header)){ + continue; + } + LOGPF("%" PRIsize_t " bytes of mDNS data", bytes); + + hdr->id = be16toh(hdr->id); + hdr->questions = be16toh(hdr->questions); + hdr->answers = be16toh(hdr->answers); + hdr->servers = be16toh(hdr->servers); + hdr->additional = be16toh(hdr->additional); + + //TODO Check for ._apple-midi._udp.local. + LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + for(u = 0; u < hdr->questions; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + offset += sizeof(dns_question); + + LOGPF("Question: %s", name.name); + } + + for(u = 0; u < hdr->answers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Answer (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->servers; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Authority (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + + for(u = 0; u < hdr->additional; u++){ + dns_decode_name(buffer, bytes, offset, &name); + offset += name.length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr) + be16toh(rr->data); + + LOGPF("Additional (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + } + } + + free(name.name); + if(bytes <= 0){ + if(errno == EAGAIN){ + return 0; + } + + LOGPF("Error reading from mDNS descriptor: %s", strerror(errno)); + return 1; + } + + return 0; +} + static int rtpmidi_handle(size_t num, managed_fd* fds){ size_t u; int rv = 0; @@ -933,7 +1059,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ //handle service tasks (mdns, clock sync, peer connections) if(mm_timestamp() - cfg.last_service > RTPMIDI_SERVICE_INTERVAL){ - DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); + //DBGPF("Performing service tasks, delta %" PRIu64, mm_timestamp() - cfg.last_service); if(rtpmidi_service()){ return 1; } @@ -942,7 +1068,8 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ for(u = 0; u < num; u++){ if(!fds[u].impl){ - //TODO handle mDNS discovery input + //handle mDNS discovery input + rtpmidi_handle_mdns(); } else{ //handle rtp/control input @@ -963,30 +1090,53 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ return rv; } -static int rtpmidi_start(size_t n, instance** inst){ - size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL; +static int rtpmidi_start_mdns(){ + struct ip_mreq mcast_req = { + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)), + .imr_interface.s_addr = INADDR_ANY + }; - //if mdns name defined and no socket, bind default values - if(cfg.mdns_name && cfg.mdns_fd < 0){ - cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - return 1; - } + struct ipv6_mreq mcast6_req = { + .ipv6mr_multiaddr.s6_addr = {0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfb}, + .ipv6mr_interface = 0 + }; + + if(!cfg.mdns_name){ + LOG("No mDNS name set, disabling AppleMIDI discovery"); + return 0; } - //register mdns fd to core - if(cfg.mdns_fd >= 0){ - if(mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL)){ - LOG("Failed to register mDNS socket with core"); - return 1; - } - fds++; + //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); + if(cfg.mdns_fd < 0){ + LOG("Failed to create requested mDNS descriptor"); + return 1; } - else{ - LOG("No mDNS discovery interface bound, AppleMIDI session discovery disabled"); + + //join ipv4 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + LOGPF("Failed to join IPv4 multicast group for mDNS: %s", strerror(errno)); + return 1; + } + + //join ipv6 multicast group + if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ + LOGPF("Failed to join IPv6 multicast group for mDNS: %s", strerror(errno)); + return 1; } + //register mdns fd to core + return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL); +} + +static int rtpmidi_start(size_t n, instance** inst){ + size_t u, p, fds = 0; + rtpmidi_instance_data* data = NULL; + uint8_t mdns_required = 0; + for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; //check whether instances are explicitly configured to a mode @@ -1013,6 +1163,9 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } + else if(data->mode == apple){ + mdns_required = 1; + } //register fds to core if(mm_manage_fd(data->fd, BACKEND_NAME, 1, inst[u]) || (data->control_fd >= 0 && mm_manage_fd(data->control_fd, BACKEND_NAME, 1, inst[u]))){ @@ -1022,6 +1175,13 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } + if(mdns_required && rtpmidi_start_mdns()){ + return 1; + } + else{ + fds++; + } + LOGPF("Registered %" PRIsize_t " descriptors to core", fds); return 0; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a08cf0f..b1fe26a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -8,6 +8,7 @@ static int rtpmidi_configure(char* option, char* value); static int rtpmidi_configure_instance(instance* instance, char* option, char* value); static int rtpmidi_instance(instance* inst); static channel* rtpmidi_channel(instance* instance, char* spec, uint8_t flags); +static uint32_t rtpmidi_interval(); static int rtpmidi_set(instance* inst, size_t num, channel** c, channel_value* v); static int rtpmidi_handle(size_t num, managed_fd* fds); static int rtpmidi_start(size_t n, instance** inst); @@ -22,6 +23,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 +#define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) +#define DNS_LABEL_LENGTH(a) ((a) & 0x3F) +#define DNS_OPCODE(a) (((a) & 0x78) >> 3) +#define DNS_RESPONSE(a) ((a) & 0x80) + enum /*_rtpmidi_channel_type*/ { none = 0, note = 0x90, @@ -90,6 +96,12 @@ enum applemidi_command { apple_feedback = 0x5253 //RS }; +typedef struct /*_dns_name*/ { + size_t alloc; + char* name; + size_t length; +} dns_name; + #pragma pack(push, 1) typedef struct /*_apple_session_command*/ { uint16_t res1; @@ -128,4 +140,25 @@ typedef struct /*_rtp_midi_command*/ { uint8_t flags; uint8_t length; } rtpmidi_command_header; + +typedef struct /*_dns_header*/ { + uint16_t id; + uint8_t flags[2]; + uint16_t questions; + uint16_t answers; + uint16_t servers; + uint16_t additional; +} dns_header; + +typedef struct /*_dns_question*/ { + uint16_t qtype; + uint16_t qclass; +} dns_question; + +typedef struct /*_dns_rr*/ { + uint16_t rtype; + uint16_t rclass; + uint32_t ttl; + uint16_t data; +} dns_rr; #pragma pack(pop) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index e857a5a..c188039 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -25,7 +25,6 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-bind` | `10.1.2.1 5353` | `:: 5353` | Bind host for the mDNS discovery server | | `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | #### Instance configuration @@ -51,9 +50,8 @@ Common instance configuration parameters |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | | `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times. | | `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | -| `peer` | `10.1.2.3 9001` | none | Configure a direct session peer, bypassing AppleMIDI discovery. May be specified multiple times | Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter (and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. -- cgit v1.2.3 From 789b3b31230750b50ad815add0a42a15760e8b83 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 4 Apr 2020 20:41:04 +0200 Subject: Basic mDNS discovery --- backends/rtpmidi.c | 228 +++++++++++++++++++++++++++++++++++++++++++++++++---- backends/rtpmidi.h | 9 +++ 2 files changed, 220 insertions(+), 17 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 97364cf..9e0a4d4 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -10,6 +10,8 @@ #include "libmmbackend.h" #include "rtpmidi.h" +//#include "../tests/hexdump.c" + //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode @@ -113,6 +115,10 @@ static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* //check whether we have space in the output if(output_offset + DNS_LABEL_LENGTH(current_label) > out->alloc){ out->name = realloc(out->name, (output_offset + DNS_LABEL_LENGTH(current_label) + 2) * sizeof(uint8_t)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } out->alloc = output_offset + DNS_LABEL_LENGTH(current_label); } @@ -130,6 +136,43 @@ static int dns_decode_name(uint8_t* buffer, size_t len, size_t start, dns_name* return 0; } +static int dns_encode_name(char* name, dns_name* out){ + char* save = NULL, *token = NULL; + out->length = 0; + + for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){ + //make space for this label, its length and a trailing root label + if(out->alloc < out->length + strlen(token) + 1 + 1){ + out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } + out->alloc = out->length + strlen(token) + 2; + } + //FIXME check label length before adding + out->name[out->length] = strlen(token); + memcpy(out->name + out->length + 1, token, strlen(token)); + out->length += strlen(token) + 1; + } + + //last-effort allocate a root buffer + if(!out->alloc){ + out->name = calloc(1, sizeof(char)); + if(!out->name){ + LOG("Failed to allocate memory"); + return 1; + } + out->alloc = 1; + } + + //add root label + out->name[out->length] = 0; + out->length++; + + return 0; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -160,7 +203,7 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } -static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* port){ +static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, char* host, char* port){ struct sockaddr_storage sock_addr = { 0 }; @@ -173,19 +216,26 @@ static int rtpmidi_bind_instance(rtpmidi_instance_data* data, char* host, char* return 1; } + if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ + LOGPF("Failed to fetch data port information: %s", strerror(errno)); + return 1; + } + //bind control port if(data->mode == apple){ - if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", strerror(errno)); - return 1; - } - - snprintf(control_port, sizeof(control_port), "%d", be16toh(((struct sockaddr_in*)&sock_addr)->sin_port) - 1); + data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1; + snprintf(control_port, sizeof(control_port), "%d", data->control_port); data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); if(data->control_fd < 0){ - LOGPF("Failed to bind control port %s", control_port); + LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name); return 1; } + + LOGPF("Apple mode instance %s listening on port %d", inst->name, data->control_port); + } + else{ + data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port); + LOGPF("Direct mode instance %s listening on port %d", inst->name, data->control_port); } return 0; @@ -334,7 +384,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return 1; } - return rtpmidi_bind_instance(data, host, port); + return rtpmidi_bind_instance(inst, data, host, port); } else if(!strcmp(option, "learn")){ if(data->mode != direct){ @@ -643,6 +693,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_reject){ //just ignore this for now and retry the invitation + LOGPF("Invitation rejected on instance %s", inst->name); } else if(command->command == apple_leave){ //remove peer from list - this comes in on the control port, but we need to remove the data port... @@ -910,6 +961,144 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } +int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + struct sockaddr_in mcast = { + .sin_family = AF_INET, + .sin_port = htobe16(5353), + .sin_addr.s_addr = htobe32(((uint32_t) 0xe00000fb)) + }; + struct sockaddr_in6 mcast6 = { + .sin6_family = AF_INET6, + .sin6_port = htobe16(5353), + .sin6_addr.s6_addr = {0xff, 0x02, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xfb} + }; + + hdr->id = 0; + hdr->flags[0] = 0x84; + hdr->flags[1] = 0; + hdr->questions = hdr->servers = 0; + hdr->answers = htobe16(4); + hdr->additional = htobe16(1); + offset = sizeof(dns_header); + + //answer 1: SRV FQDN + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->session_name, RTPMIDI_MDNS_DOMAIN); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame); + return 1; + } + + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(33); //SRV RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(120); + offset += sizeof(dns_rr); + + srv = (dns_rr_srv*) (frame + offset); + srv->priority = 0; + srv->weight = 0; + srv->port = htobe16(data->control_port); + offset += sizeof(dns_rr_srv); + + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + rr->data = htobe16(sizeof(dns_rr_srv) + name.length); + + //answer 2: empty TXT (apple asks for it otherwise) + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(16); //TXT RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(1); + offset += sizeof(dns_rr); + frame[offset++] = 0x00; //zero-length TXT + + //answer 3: dns-sd PTR _applemidi + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(12); //PTR RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(2); + offset += sizeof(dns_rr); + + //add backref for PTR + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; + + //answer 4: _applemidi PTR FQDN + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(12); //PTR RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(4500); + rr->data = htobe16(2); + offset += sizeof(dns_rr); + + //add backref for PTR + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + + //additional 1: A host + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); + if(dns_encode_name((char*) frame + offset, &name)){ + LOGPF("Failed to encode name for %s", frame + offset); + return 1; + } + memcpy(frame + offset, name.name, name.length); + offset += name.length; + + rr = (dns_rr*) (frame + offset); + rr->rtype = htobe16(1); //A RR + rr->rclass = htobe16(1); //INADDR + rr->ttl = htobe32(120); + rr->data = htobe16(4); + offset += sizeof(dns_rr); + + //TODO get local addresses here + frame[offset++] = 0x0A; + frame[offset++] = 0x17; + frame[offset++] = 0x01; + frame[offset++] = 0x07; + + //send to ipv4 and ipv6 mcasts + sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); + sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + + free(name.name); + return 0; +} + static int rtpmidi_service(){ size_t n, u, p; instance** inst = NULL; @@ -938,15 +1127,16 @@ static int rtpmidi_service(){ return 1; } - //mdns discovery - if(cfg.mdns_fd >= 0){ - //TODO send applemidi discovery packets - } for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; if(data->mode == apple){ + //mdns discovery + if(cfg.mdns_fd >= 0){ + rtpmidi_mdns_announce(data); + } + for(p = 0; p < data->peers; p++){ if(data->peer[p].active && data->peer[p].connected){ //apple sync @@ -986,7 +1176,7 @@ static int rtpmidi_handle_mdns(){ .alloc = 0 }; ssize_t bytes = 0; - size_t u = 0, offset = sizeof(dns_header); + size_t u = 0, offset = 0; for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ if(bytes < sizeof(dns_header)){ @@ -1000,14 +1190,18 @@ static int rtpmidi_handle_mdns(){ hdr->servers = be16toh(hdr->servers); hdr->additional = be16toh(hdr->additional); + //rfc6762 18.3: opcode != 0 -> ignore + //rfc6762 18.11: response code != 0 -> ignore + //TODO Check for ._apple-midi._udp.local. + offset = sizeof(dns_header); LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); for(u = 0; u < hdr->questions; u++){ dns_decode_name(buffer, bytes, offset, &name); offset += name.length; offset += sizeof(dns_question); - LOGPF("Question: %s", name.name); + LOGPF("Question (%d bytes): %s", name.length, name.name); } for(u = 0; u < hdr->answers; u++){ @@ -1151,7 +1345,7 @@ static int rtpmidi_start(size_t n, instance** inst){ } //if not bound, bind to default - if(data->fd < 0 && rtpmidi_bind_instance(data, RTPMIDI_DEFAULT_HOST, NULL)){ + if(data->fd < 0 && rtpmidi_bind_instance(inst[u], data, RTPMIDI_DEFAULT_HOST, NULL)){ LOGPF("Failed to bind default sockets for instance %s", inst[u]->name); return 1; } @@ -1178,7 +1372,7 @@ static int rtpmidi_start(size_t n, instance** inst){ if(mdns_required && rtpmidi_start_mdns()){ return 1; } - else{ + else if(mdns_required){ fds++; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index b1fe26a..f55b543 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -22,6 +22,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 +#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local" +#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local" #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -67,6 +69,7 @@ typedef struct /*_rtmidi_instance_data*/ { int fd; int control_fd; + uint16_t control_port; /*convenience member set by rtpmidi_bind_instance*/ size_t peers; rtpmidi_peer* peer; @@ -161,4 +164,10 @@ typedef struct /*_dns_rr*/ { uint32_t ttl; uint16_t data; } dns_rr; + +typedef struct /*_dns_rr_srv*/ { + uint16_t priority; + uint16_t weight; + uint16_t port; +} dns_rr_srv; #pragma pack(pop) -- cgit v1.2.3 From f28497227eba6783d7e779a5ccaf2a12c1794a94 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 6 Apr 2020 23:00:48 +0200 Subject: Discover peers, detach from DNS-SD at shutdown --- backends/rtpmidi.c | 265 ++++++++++++++++++++++++++++++++++------------------ backends/rtpmidi.h | 8 +- backends/rtpmidi.md | 21 +++-- 3 files changed, 190 insertions(+), 104 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 9e0a4d4..7ad3eca 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -15,7 +15,6 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode -//FIXME what qualifies an instance to be announced via mdns? static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -173,6 +172,48 @@ static int dns_encode_name(char* name, dns_name* out){ return 0; } +static ssize_t dns_push_rr(uint8_t* buffer, size_t length, dns_rr** out, char* name, uint16_t type, uint16_t class, uint32_t ttl, uint16_t len){ + dns_rr* rr = NULL; + size_t offset = 0; + dns_name encode = { + .alloc = 0 + }; + + //if requested, encode name + if(name && dns_encode_name(name, &encode)){ + LOGPF("Failed to encode DNS name %s", name); + goto bail; + } + + if(encode.length + sizeof(dns_rr) > length){ + LOGPF("Failed to encode DNS name %s, insufficient space", name); + goto bail; + } + + if(name){ + //copy encoded name to buffer + memcpy(buffer, encode.name, encode.length); + offset += encode.length; + } + + rr = (dns_rr*) (buffer + offset); + rr->rtype = htobe16(type); + rr->rclass = htobe16(class); + rr->ttl = htobe32(ttl); + rr->data = htobe16(len); + offset += sizeof(dns_rr); + if(out){ + *out = rr; + } + + free(encode.name); + return offset; + +bail: + free(encode.name); + return -1; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -421,14 +462,18 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); } - else if(!strcmp(option, "session")){ + else if(!strcmp(option, "title")){ if(data->mode != apple){ - LOG("'session' option is only valid for apple mode instances"); + LOG("'title' option is only valid for apple mode instances"); return 1; } - free(data->session_name); - data->session_name = strdup(value); - if(!data->session_name){ + if(strchr(value, '.')){ + LOGPF("Invalid instance title %s on %s: titles may not contain periods", value, inst->name); + return 1; + } + free(data->title); + data->title = strdup(value); + if(!data->title){ LOG("Failed to allocate memory"); return 1; } @@ -684,10 +729,10 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size invite->version = htobe32(2); invite->token = command->token; invite->ssrc = htobe32(data->ssrc); - memcpy(response + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(response + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); - sendto(data->fd, response, sizeof(apple_command) + strlen(data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + sendto(data->fd, response, sizeof(apple_command) + strlen(data->title ? data->title : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); } return 0; } @@ -961,15 +1006,7 @@ static int rtpmidi_handle_control(instance* inst){ return 0; } -int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ - uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; - dns_header* hdr = (dns_header*) frame; - dns_rr* rr = NULL; - dns_rr_srv* srv = NULL; - dns_name name = { - .alloc = 0 - }; - size_t offset = 0; +static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ struct sockaddr_in mcast = { .sin_family = AF_INET, .sin_port = htobe16(5353), @@ -984,6 +1021,63 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ 0x00, 0x00, 0x00, 0xfb} }; + //send to ipv4 and ipv6 mcasts + sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); + sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + return 0; +} + +static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + ssize_t bytes = 0; + + hdr->id = 0; + hdr->flags[0] = 0x84; + hdr->flags[1] = 0; + hdr->questions = hdr->servers = hdr->additional = 0; + hdr->answers = htobe16(1); + offset = sizeof(dns_header); + + //answer 1: _apple-midi PTR FQDN + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_MDNS_DOMAIN); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 0, 0); + if(bytes < 0){ + goto bail; + } + offset += bytes; + + //TODO length-checks here + frame[offset++] = strlen(data->title); + memcpy(frame + offset, data->title, strlen(data->title)); + offset += strlen(data->title); + frame[offset++] = 0xC0; + frame[offset++] = sizeof(dns_header); + rr->data = htobe16(1 + strlen(data->title) + 2); + + free(name.name); + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; +} + +static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + dns_header* hdr = (dns_header*) frame; + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + dns_name name = { + .alloc = 0 + }; + size_t offset = 0; + ssize_t bytes = 0; + hdr->id = 0; hdr->flags[0] = 0x84; hdr->flags[1] = 0; @@ -993,20 +1087,12 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset = sizeof(dns_header); //answer 1: SRV FQDN - snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->session_name, RTPMIDI_MDNS_DOMAIN); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame); - return 1; + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->title, RTPMIDI_MDNS_DOMAIN); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0); + if(bytes < 0){ + goto bail; } - - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(33); //SRV RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(120); - offset += sizeof(dns_rr); + offset += bytes; srv = (dns_rr_srv*) (frame + offset); srv->priority = 0; @@ -1017,7 +1103,7 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); - return 1; + goto bail; } memcpy(frame + offset, name.name, name.length); offset += name.length; @@ -1026,29 +1112,21 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ //answer 2: empty TXT (apple asks for it otherwise) frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(16); //TXT RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(1); - offset += sizeof(dns_rr); + + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 16, 1, 4500, 1); + if(bytes < 0){ + goto bail; + } + offset += bytes; frame[offset++] = 0x00; //zero-length TXT //answer 3: dns-sd PTR _applemidi snprintf((char*) frame + offset, sizeof(frame) - offset, "%s", RTPMIDI_DNSSD_DOMAIN); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame + offset); - return 1; + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 12, 1, 4500, 2); + if(bytes < 0){ + goto bail; } - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(12); //PTR RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(2); - offset += sizeof(dns_rr); + offset += bytes; //add backref for PTR frame[offset++] = 0xC0; @@ -1058,12 +1136,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header) + frame[sizeof(dns_header)] + 1; - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(12); //PTR RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(4500); - rr->data = htobe16(2); - offset += sizeof(dns_rr); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, NULL, 12, 1, 4500, 2); + if(bytes < 0){ + goto bail; + } + offset += bytes; //add backref for PTR frame[offset++] = 0xC0; @@ -1071,19 +1148,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ //additional 1: A host snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); - if(dns_encode_name((char*) frame + offset, &name)){ - LOGPF("Failed to encode name for %s", frame + offset); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 1, 1, 120, 4); + if(bytes < 0){ return 1; } - memcpy(frame + offset, name.name, name.length); - offset += name.length; - - rr = (dns_rr*) (frame + offset); - rr->rtype = htobe16(1); //A RR - rr->rclass = htobe16(1); //INADDR - rr->ttl = htobe32(120); - rr->data = htobe16(4); - offset += sizeof(dns_rr); + offset += bytes; //TODO get local addresses here frame[offset++] = 0x0A; @@ -1091,12 +1160,11 @@ int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0x01; frame[offset++] = 0x07; - //send to ipv4 and ipv6 mcasts - sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); - sendto(cfg.mdns_fd, frame, offset, 0, (struct sockaddr*) &mcast, sizeof(mcast)); - free(name.name); - return 0; + return rtpmidi_mdns_broadcast(frame, offset); +bail: + free(name.name); + return 1; } static int rtpmidi_service(){ @@ -1127,13 +1195,13 @@ static int rtpmidi_service(){ return 1; } - for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0){ + //TODO time-out this properly + if(cfg.mdns_fd >= 0 && data->title){ rtpmidi_mdns_announce(data); } @@ -1156,9 +1224,9 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); //append session name to packet - memcpy(frame + sizeof(apple_command), data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME, strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->session_name ? data->session_name : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } } } @@ -1182,7 +1250,6 @@ static int rtpmidi_handle_mdns(){ if(bytes < sizeof(dns_header)){ continue; } - LOGPF("%" PRIsize_t " bytes of mDNS data", bytes); hdr->id = be16toh(hdr->id); hdr->questions = be16toh(hdr->questions); @@ -1193,42 +1260,53 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - //TODO Check for ._apple-midi._udp.local. offset = sizeof(dns_header); - LOGPF("ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); for(u = 0; u < hdr->questions; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; offset += sizeof(dns_question); - - LOGPF("Question (%d bytes): %s", name.length, name.name); } + //look for a SRV answer for ._apple-midi._udp.local. for(u = 0; u < hdr->answers; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - LOGPF("Answer (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); + if(be16toh(rr->rtype) == 33 + && strlen(name.name) > strlen(RTPMIDI_MDNS_DOMAIN) + && !strcmp(name.name + (strlen(name.name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ + LOGPF("Found possible peer: %s", name.name); + } } for(u = 0; u < hdr->servers; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - - LOGPF("Authority (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); } + //find a matching A/AAAA record in the additional records for(u = 0; u < hdr->additional; u++){ - dns_decode_name(buffer, bytes, offset, &name); + if(dns_decode_name(buffer, bytes, offset, &name)){ + LOG("Failed to decode DNS label"); + return 1; + } offset += name.length; rr = (dns_rr*) (buffer + offset); offset += sizeof(dns_rr) + be16toh(rr->data); - - LOGPF("Additional (%d): %s, %d bytes of data", be16toh(rr->rtype), name.name, be16toh(rr->data)); } } @@ -1386,6 +1464,11 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; + + if(cfg.mdns_fd >= 0 && data->mode == apple && data->title){ + rtpmidi_mdns_detach(data); + } + if(data->fd >= 0){ close(data->fd); } @@ -1394,8 +1477,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(data->control_fd); } - free(data->session_name); - data->session_name = NULL; + free(data->title); + data->title = NULL; free(data->accept); data->accept = NULL; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f55b543..f240586 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -22,8 +22,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_GET_TYPE(a) ((a) & 0x7F) #define RTPMIDI_DEFAULT_NAME "MIDIMonster" #define RTPMIDI_SERVICE_INTERVAL 1000 -#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local" -#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local" +#define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local." +#define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local." #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -77,8 +77,8 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* session_name; /* initiator only */ - char* accept; /* participant only */ + char* title; + char* accept; //direct mode config uint8_t learn_peers; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index c188039..64978ae 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -24,8 +24,8 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration | -| `mdns-name` | `computer1` | none | mDNS hostname to announce, also used as AppleMIDI peer name | +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | #### Instance configuration @@ -48,13 +48,10 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| -| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated) | -| `session` | `Just Jamming` | `MIDIMonster` | Session name to announce via mDNS | -| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). Setting this option makes the instance a session initiator. May be specified multiple times. | -| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). Setting this option makes the instance a session participant | - -Note that AppleMIDI session discovery requires mDNS functionality, thus the `mdns-name` global parameter -(and, depending on your setup, the `mdns-bind` parameter) need to be configured properly. +| `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). | +| `title` | `Just Jamming` | none | Session/device name to announce via mDNS. If unset, the instance will not be announced. | +| `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. | +| `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). | #### Channel specification @@ -85,3 +82,9 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch ``` #### Known bugs / problems + +The mDNS and DNS-SD implementations in this backends are extremely terse, to the point of violating the +specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems +arising from this will be considered a bug only in cases where they hinder normal operation of the backend. + +mDNS discovery may announce flawed records when run on a host with multiple active interface. -- cgit v1.2.3 From c52f3d7f48daaaed52277363beb8325e67d67469 Mon Sep 17 00:00:00 2001 From: cbdev Date: Wed, 8 Apr 2020 22:18:45 +0200 Subject: Restructure mDNS discovery --- backends/rtpmidi.c | 146 +++++++++++++++++++++++++++++++--------------------- backends/rtpmidi.h | 2 + backends/rtpmidi.md | 6 +-- 3 files changed, 92 insertions(+), 62 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 7ad3eca..9d84cde 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -15,6 +15,9 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode +//TODO announce on mdns input +//TODO connect to discovered peers +//TODO push correct addresses to discovery static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -467,8 +470,8 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOG("'title' option is only valid for apple mode instances"); return 1; } - if(strchr(value, '.')){ - LOGPF("Invalid instance title %s on %s: titles may not contain periods", value, inst->name); + if(strchr(value, '.') || strlen(value) > 254){ + LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); return 1; } free(data->title); @@ -689,7 +692,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size accept->token = command->token; accept->ssrc = htobe32(data->ssrc); //add local name to response - //FIXME might want to use the session name in case it is set + //FIXME use instance title instead of mdns_name memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); @@ -722,7 +725,6 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size else{ //send invite on data fd LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); - //FIXME limit max length of session name apple_command* invite = (apple_command*) response; invite->res1 = 0xFFFF; invite->command = htobe16(apple_invite); @@ -1100,6 +1102,8 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ srv->port = htobe16(data->control_port); offset += sizeof(dns_rr_srv); + //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to + //we dont do it because i dont want to snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); @@ -1160,6 +1164,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0x01; frame[offset++] = 0x07; + data->last_announce = mm_timestamp(); free(name.name); return rtpmidi_mdns_broadcast(frame, offset); bail: @@ -1200,8 +1205,8 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - //TODO time-out this properly - if(cfg.mdns_fd >= 0 && data->title){ + if(cfg.mdns_fd >= 0 && data->title + && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ rtpmidi_mdns_announce(data); } @@ -1236,21 +1241,80 @@ static int rtpmidi_service(){ return 0; } +//TODO bounds check all accesses +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host){ + dns_rr* rr = NULL; + dns_rr_srv* srv = NULL; + size_t u = 0, offset = sizeof(dns_header); + uint8_t* session_name = NULL; + + for(u = 0; u < hdr->questions; u++){ + if(dns_decode_name(buffer, length, offset, name)){ + LOG("Failed to decode DNS label"); + return 1; + } + offset += name->length; + offset += sizeof(dns_question); + } + + //look for a SRV answer for ._apple-midi._udp.local. + for(u = 0; u < hdr->answers; u++){ + if(dns_decode_name(buffer, length, offset, name)){ + LOG("Failed to decode DNS label"); + return 1; + } + + //store a pointer to the first label in the current path + //since we decoded the name successfully before and dns_decode_name performs bounds checking, this _should_ be ok + session_name = (DNS_POINTER(buffer[offset])) ? buffer + (DNS_LABEL_LENGTH(buffer[offset]) << 8 | buffer[offset + 1]) : buffer + offset; + + offset += name->length; + rr = (dns_rr*) (buffer + offset); + offset += sizeof(dns_rr); + + if(be16toh(rr->rtype) == 33 + && strlen(name->name) > strlen(RTPMIDI_MDNS_DOMAIN) + && !strcmp(name->name + (strlen(name->name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ + //decode the srv data + srv = (dns_rr_srv*) (buffer + offset); + offset += sizeof(dns_rr_srv); + + if(dns_decode_name(buffer, length, offset, host)){ + LOG("Failed to decode SRV target"); + return 1; + } + + if(!strncmp(host->name, cfg.mdns_name, strlen(cfg.mdns_name)) && host->name[strlen(cfg.mdns_name)] == '.'){ + //ignore loopback packets, we don't care about them + return 0; + } + + //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) + LOGPF("Detected possible peer %.*s on %s Port %d", session_name[0], session_name + 1, host->name, be16toh(srv->port)); + offset -= sizeof(dns_rr_srv); + } + + offset += be16toh(rr->data); + } + + + return 0; +} + static int rtpmidi_handle_mdns(){ uint8_t buffer[RTPMIDI_PACKET_BUFFER]; dns_header* hdr = (dns_header*) buffer; - dns_rr* rr = NULL; dns_name name = { .alloc = 0 - }; + }, host = name; ssize_t bytes = 0; - size_t u = 0, offset = 0; for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ if(bytes < sizeof(dns_header)){ continue; } + //decode basic header hdr->id = be16toh(hdr->id); hdr->questions = be16toh(hdr->questions); hdr->answers = be16toh(hdr->answers); @@ -1260,57 +1324,12 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - offset = sizeof(dns_header); DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - for(u = 0; u < hdr->questions; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - offset += sizeof(dns_question); - } - - //look for a SRV answer for ._apple-midi._udp.local. - for(u = 0; u < hdr->answers; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - - if(be16toh(rr->rtype) == 33 - && strlen(name.name) > strlen(RTPMIDI_MDNS_DOMAIN) - && !strcmp(name.name + (strlen(name.name) - strlen(RTPMIDI_MDNS_DOMAIN)), RTPMIDI_MDNS_DOMAIN)){ - LOGPF("Found possible peer: %s", name.name); - } - } - - for(u = 0; u < hdr->servers; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - } - - //find a matching A/AAAA record in the additional records - for(u = 0; u < hdr->additional; u++){ - if(dns_decode_name(buffer, bytes, offset, &name)){ - LOG("Failed to decode DNS label"); - return 1; - } - offset += name.length; - rr = (dns_rr*) (buffer + offset); - offset += sizeof(dns_rr) + be16toh(rr->data); - } + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host); } free(name.name); + free(host.name); if(bytes <= 0){ if(errno == EAGAIN){ return 0; @@ -1406,7 +1425,7 @@ static int rtpmidi_start_mdns(){ static int rtpmidi_start(size_t n, instance** inst){ size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL; + rtpmidi_instance_data* data = NULL, *other = NULL; uint8_t mdns_required = 0; for(u = 0; u < n; u++){ @@ -1435,7 +1454,16 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } - else if(data->mode == apple){ + else if(data->mode == apple && data->title){ + //check for unique title + for(p = 0; p < u; p++){ + other = (rtpmidi_instance_data*) inst[p]->impl; + if(other->mode == apple && other->title + && !strcmp(data->title, other->title)){ + LOGPF("Instance titles are required to be unique to allow for mDNS discovery, conflict between %s and %s", inst[p]->name, inst[u]->name); + return 1; + } + } mdns_required = 1; } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index f240586..631c45a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -24,6 +24,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_SERVICE_INTERVAL 1000 #define RTPMIDI_MDNS_DOMAIN "_apple-midi._udp.local." #define RTPMIDI_DNSSD_DOMAIN "_services._dns-sd._udp.local." +#define RTPMIDI_ANNOUNCE_INTERVAL (60 * 1000) #define DNS_POINTER(a) (((a) & 0xC0) == 0xC0) #define DNS_LABEL_LENGTH(a) ((a) & 0x3F) @@ -79,6 +80,7 @@ typedef struct /*_rtmidi_instance_data*/ { //apple-midi config char* title; char* accept; + uint64_t last_announce; //direct mode config uint8_t learn_peers; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 64978ae..84a7cd6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -1,7 +1,7 @@ ### The `rtpmidi` backend This backend provides read-write access to RTP MIDI streams, which transfer MIDI data -over the network. +over the network. Notably, it has native support in Apple devices. As the specification for RTP MIDI does not normatively indicate any method for session management, most vendors define their own standards for this. @@ -83,8 +83,8 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems -The mDNS and DNS-SD implementations in this backends are extremely terse, to the point of violating the +The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating the specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems arising from this will be considered a bug only in cases where they hinder normal operation of the backend. -mDNS discovery may announce flawed records when run on a host with multiple active interface. +mDNS discovery may announce flawed records when run on a host with multiple active interfaces. -- cgit v1.2.3 From 11c0c32ec8d7e9185baf733436c221e6742cea84 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 10 Apr 2020 22:43:34 +0200 Subject: Implement OSC bundle format reception (#56) --- backends/osc.c | 105 ++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 75 insertions(+), 30 deletions(-) diff --git a/backends/osc.c b/backends/osc.c index f40f3f5..72a7b50 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -10,6 +10,7 @@ /* * TODO * ping method + * bundle output */ #define osc_align(a) ((((a) / 4) + (((a) % 4) ? 1 : 0)) * 4) @@ -720,7 +721,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ //sanity check if(ident.fields.channel >= data->channels || ident.fields.parameter >= data->channel[ident.fields.channel].params){ - LOG("Channel identifier out of range"); + LOG("Channel identifier out of range, possibly an output channel was not pre-configured"); return 1; } @@ -757,7 +758,7 @@ static int osc_set(instance* inst, size_t num, channel** c, channel_value* v){ return rv; } -static int osc_process_packet(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ +static int osc_process_message(instance* inst, char* local_path, char* format, uint8_t* payload, size_t payload_len){ osc_instance_data* data = (osc_instance_data*) inst->impl; size_t c, p, offset = 0; osc_parameter_value min, max, cur; @@ -809,15 +810,82 @@ static int osc_process_packet(instance* inst, char* local_path, char* format, ui return 0; } +static int osc_process_packet(instance* inst, uint8_t* buffer, size_t len){ + osc_instance_data* data = (osc_instance_data*) inst->impl; + size_t offset = 0, message_length = len; + char* osc_local = NULL, *osc_fmt = NULL; + uint8_t* osc_data = NULL; + uint32_t* bundle_size = NULL; + uint8_t decode_bundle = 0; + + //bundles need at least a header and timestamp + if(len >= 16 && !memcmp(buffer, "#bundle\0", 8)){ + decode_bundle = 1; + offset = 16; + } + + do{ + if(decode_bundle){ + if(len - offset < 4){ + LOGPF("Failed to decode bundle size: %" PRIsize_t " bytes left at %" PRIsize_t " of %" PRIsize_t, len - offset, offset, len); + break; + } + bundle_size = (uint32_t*) (buffer + offset); + message_length = be32toh(*bundle_size); + DBGPF("Next bundle entry has %" PRIsize_t " bytes", message_length); + offset += 4; + + if(len - offset < message_length){ + LOGPF("Bundle member size out of bounds: %" PRIsize_t " bytes left", len - offset); + break; + } + } + + //check for recursive bundles + if(message_length >= 16 && !memcmp(buffer + offset, "#bundle\0", 8)){ + DBGPF("Recursing into sub-bundle of size %" PRIsize_t " on %s", message_length, inst->name); + osc_process_packet(inst, buffer + offset, message_length); + } + //ignore messages if root filter active + else if(data->root && strncmp((char*) (buffer + offset), data->root, min(message_length, strlen(data->root)))){ + DBGPF("Ignoring message due to active root filter %s: data is for %s", data->root, buffer + offset); + } + else{ + //FIXME all these accesses should be checked against message_length + osc_local = (char*) (buffer + offset + (data->root ? strlen(data->root) : 0)); + osc_fmt = (char*) (buffer + offset + osc_align(strlen((char*) (buffer + offset)) + 1)); + + if(*osc_fmt != ','){ + //invalid format string + LOGPF("Invalid format string in packet for instance %s: %s", inst->name, osc_fmt); + } + else{ + osc_fmt++; + + if(osc_global_config.detect){ + LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); + } + + osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); + if(osc_process_message(inst, osc_local, osc_fmt, osc_data, message_length - (osc_data - (uint8_t*) buffer))){ + LOGPF("Failed to process OSC message on %s", inst->name); + } + } + } + + offset += message_length; + } + while(offset < len); + + return 0; +} + static int osc_handle(size_t num, managed_fd* fds){ size_t fd; - char recv_buf[OSC_RECV_BUF]; + uint8_t recv_buf[OSC_RECV_BUF]; instance* inst = NULL; osc_instance_data* data = NULL; ssize_t bytes_read = 0; - char* osc_fmt = NULL; - char* osc_local = NULL; - uint8_t* osc_data = NULL; for(fd = 0; fd < num; fd++){ inst = (instance*) fds[fd].impl; @@ -841,30 +909,7 @@ static int osc_handle(size_t num, managed_fd* fds){ break; } - if(data->root && strncmp(recv_buf, data->root, min(bytes_read, strlen(data->root)))){ - //ignore packet for different root - continue; - } - osc_local = recv_buf + (data->root ? strlen(data->root) : 0); - - osc_fmt = recv_buf + osc_align(strlen(recv_buf) + 1); - if(*osc_fmt != ','){ - //invalid format string - LOGPF("Invalid format string in packet for instance %s", inst->name); - continue; - } - osc_fmt++; - - if(osc_global_config.detect){ - LOGPF("Incoming data: Path %s.%s Format %s", inst->name, osc_local, osc_fmt); - } - - //FIXME check supplied data length - osc_data = (uint8_t*) osc_fmt + (osc_align(strlen(osc_fmt) + 2) - 1); - - if(osc_process_packet(inst, osc_local, osc_fmt, osc_data, bytes_read - (osc_data - (uint8_t*) recv_buf))){ - return 1; - } + osc_process_packet(inst, recv_buf, bytes_read); } while(bytes_read > 0); #ifdef _WIN32 -- cgit v1.2.3 From b53092ac95fa25d0e6e4e4fc3de9531f43038c4f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 11 Apr 2020 18:40:39 +0200 Subject: Use local interface addresses for mDNS announce --- backends/Makefile | 2 +- backends/libmmbackend.c | 15 ++++ backends/libmmbackend.h | 4 + backends/rtpmidi.c | 218 +++++++++++++++++++++++++++++++++++++++++------- backends/rtpmidi.h | 6 ++ backends/rtpmidi.md | 9 +- 6 files changed, 220 insertions(+), 34 deletions(-) diff --git a/backends/Makefile b/backends/Makefile index c19d9dd..1e66995 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -48,7 +48,7 @@ maweb.dll: CFLAGS += -DMAWEB_NO_LIBSSL rtpmidi.so: ADDITIONAL_OBJS += $(BACKEND_LIB) rtpmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) -rtpmidi.dll: LDLIBS += -lws2_32 +rtpmidi.dll: LDLIBS += -lws2_32 -liphlpapi winmidi.dll: ADDITIONAL_OBJS += $(BACKEND_LIB) winmidi.dll: LDLIBS += -lwinmm -lws2_32 diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index b9513ac..ab96646 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -1,6 +1,21 @@ #include "libmmbackend.h" #define LOGPF(format, ...) fprintf(stderr, "libmmbe\t" format "\n", __VA_ARGS__) +#define LOG(message) fprintf(stderr, "libmmbe\t%s\n", (message)) + +int mmbackend_strdup(char** dest, char* src){ + if(*dest){ + free(*dest); + } + + *dest = strdup(src); + + if(!*dest){ + LOG("Failed to allocate memory"); + return 1; + } + return 0; +} void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index aa0d0f0..cb211a6 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -18,6 +18,10 @@ /*** BACKEND IMPLEMENTATION LIBRARY ***/ +/** Convenience functions **/ + +int mmbackend_strdup(char** dest, char* src); + /** Networking functions **/ /* diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 9d84cde..95a50d2 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -7,9 +7,19 @@ #include #include +//mmbackend pulls in windows.h, required before more specific includes #include "libmmbackend.h" #include "rtpmidi.h" +#ifdef _WIN32 +#include +#else +#include +#include +#include +#endif + + //#include "../tests/hexdump.c" //TODO learn peer ssrcs @@ -17,7 +27,9 @@ //TODO internal loop mode //TODO announce on mdns input //TODO connect to discovered peers -//TODO push correct addresses to discovery +//TODO refactor cfg.announces +//TODO windows address discovery +//TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -25,6 +37,10 @@ static struct /*_rtpmidi_global*/ { uint8_t detect; uint64_t last_service; + char* mdns_interface; + size_t addresses; + rtpmidi_addr* address; + size_t announces; rtpmidi_announce* announce; } cfg = { @@ -217,6 +233,137 @@ bail: return -1; } +#ifdef _WIN32 +static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ + size_t u; + + for(u = 0; haystack[u] && needle[u]; u++){ + if(haystack[u] != needle[u]){ + return 1; + } + } + + if(!haystack[u] && needle[u]){ + return 1; + } + return 0; +} + +//this has become available with vista for some reason... +static char* inet_ntop(int family, uint8_t* data, char* buffer, size_t length){ + switch(family){ + case AF_INET: + snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + break; + case AF_INET6: + snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", + data[0], data[1], data[2], data[3], + data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], + data[12], data[13], data[14], data[15]); + break; + } + return buffer; +} +#endif + +static int rtpmidi_announce_addrs(){ + char repr[INET6_ADDRSTRLEN + 1]; + union { + struct sockaddr_in* in4; + struct sockaddr_in6* in6; + struct sockaddr* in; + } addr; + + #ifdef _WIN32 + size_t bytes_alloc = 50 * sizeof(IP_ADAPTER_ADDRESSES); + IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; + IP_ADAPTER_ADDRESSES* addrs = calloc(1, bytes_alloc), *iter = NULL; + if(!addrs){ + LOG("Failed to allocate memory"); + return 1; + } + + if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ + //FIXME might try to resize the result list and retry at some point... + LOG("Failed to query local interface addresses"); + free(addrs); + return 1; + } + + for(iter = addrs; iter; iter = iter->Next){ + //filter interfaces if requested + if(cfg.mdns_interface && rtpmidi_wide_pfxcmp(iter->FriendlyName, cfg.mdns_interface)){ + continue; + } + + for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ + addr.in = unicast_addr->Address.lpSockaddr; + if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ + continue; + } + + cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); + if(!cfg.address){ + cfg.addresses = 0; + LOG("Failed to allocate memory"); + return 1; + } + + cfg.address[cfg.addresses].family = addr.in->sa_family; + memcpy(&cfg.address[cfg.addresses].addr, + (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, + (addr.in->sa_family == AF_INET) ? 4 : 16); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->FriendlyName); + cfg.addresses++; + } + } + + free(addrs); + #else + struct ifaddrs* ifa = NULL, *iter = NULL; + + if(getifaddrs(&ifa)){ + LOGPF("Failed to get adapter address information: %s", strerror(errno)); + return 1; + } + + for(iter = ifa; iter; iter = iter->ifa_next){ + if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name)) + && strcmp(iter->ifa_name, "lo") + && iter->ifa_addr){ + addr.in = iter->ifa_addr; + if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ + continue; + } + + cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); + if(!cfg.address){ + cfg.addresses = 0; + LOG("Failed to allocate memory"); + return 1; + } + + cfg.address[cfg.addresses].family = addr.in->sa_family; + memcpy(&cfg.address[cfg.addresses].addr, + (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, + (addr.in->sa_family == AF_INET) ? 4 : 16); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->ifa_name); + cfg.addresses++; + } + } + + freeifaddrs(ifa); + #endif + + if(!cfg.addresses){ + LOG("Failed to gather local IP addresses for mDNS announce"); + return 1; + } + return 0; +} + static uint32_t rtpmidi_interval(){ return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); } @@ -228,12 +375,15 @@ static int rtpmidi_configure(char* option, char* value){ return 1; } - cfg.mdns_name = strdup(value); - if(!cfg.mdns_name){ - LOG("Failed to allocate memory"); + return mmbackend_strdup(&cfg.mdns_name, value); + } + else if(!strcmp(option, "mdns-interface")){ + if(cfg.mdns_interface){ + LOG("Duplicate mdns-interface assignment"); return 1; } - return 0; + + return mmbackend_strdup(&cfg.mdns_interface, value); } else if(!strcmp(option, "detect")){ cfg.detect = 0; @@ -474,13 +624,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); return 1; } - free(data->title); - data->title = strdup(value); - if(!data->title){ - LOG("Failed to allocate memory"); - return 1; - } - return 0; + return mmbackend_strdup(&data->title, value); } else if(!strcmp(option, "invite")){ if(data->mode != apple){ @@ -495,13 +639,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) LOG("'join' option is only valid for apple mode instances"); return 1; } - free(data->accept); - data->accept = strdup(value); - if(!data->accept){ - LOG("Failed to allocate memory"); - return 1; - } - return 0; + return mmbackend_strdup(&data->accept, value); } LOGPF("Unknown instance configuration option %s on instance %s", option, inst->name); @@ -1069,6 +1207,7 @@ bail: return 1; } +//FIXME this should not exceed 1500 bytes static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; @@ -1077,7 +1216,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ dns_name name = { .alloc = 0 }; - size_t offset = 0; + size_t offset = 0, host_offset = 0, u = 0; ssize_t bytes = 0; hdr->id = 0; @@ -1085,7 +1224,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ hdr->flags[1] = 0; hdr->questions = hdr->servers = 0; hdr->answers = htobe16(4); - hdr->additional = htobe16(1); + hdr->additional = htobe16(cfg.addresses); offset = sizeof(dns_header); //answer 1: SRV FQDN @@ -1150,19 +1289,35 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - //additional 1: A host + //additional 1: first announce addr + host_offset = offset; snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); - bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 1, 1, 120, 4); + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, + (cfg.address[0].family == AF_INET) ? 1 : 28, 1, 120, + (cfg.address[0].family == AF_INET) ? 4 : 16); if(bytes < 0){ return 1; } offset += bytes; - //TODO get local addresses here - frame[offset++] = 0x0A; - frame[offset++] = 0x17; - frame[offset++] = 0x01; - frame[offset++] = 0x07; + memcpy(frame + offset, cfg.address[0].addr, (cfg.address[0].family == AF_INET) ? 4 : 16); + offset += (cfg.address[0].family == AF_INET) ? 4 : 16; + + //push all other announce addresses with a pointer + for(u = 1; u < cfg.addresses; u++){ + frame[offset++] = 0xC0 | (host_offset >> 8); + frame[offset++] = host_offset & 0xFF; + bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, + (cfg.address[u].family == AF_INET) ? 1 : 28, 1, 120, + (cfg.address[u].family == AF_INET) ? 4 : 16); + if(bytes < 0){ + return 1; + } + offset += bytes; + + memcpy(frame + offset, cfg.address[u].addr, (cfg.address[u].family == AF_INET) ? 4 : 16); + offset += (cfg.address[u].family == AF_INET) ? 4 : 16; + } data->last_announce = mm_timestamp(); free(name.name); @@ -1475,7 +1630,7 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } - if(mdns_required && rtpmidi_start_mdns()){ + if(mdns_required && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ return 1; } else if(mdns_required){ @@ -1529,8 +1684,13 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ cfg.announce = NULL; cfg.announces = 0; + free(cfg.address); + cfg.addresses = 0; + free(cfg.mdns_name); cfg.mdns_name = NULL; + free(cfg.mdns_interface); + cfg.mdns_interface = NULL; if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 631c45a..9d2c40a 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -92,6 +92,12 @@ typedef struct /*rtpmidi_announced_instance*/ { char** invite; } rtpmidi_announce; +typedef struct /*_rtpmidi_addr*/ { + int family; + //this is actually a fair bit too big, but whatever + uint8_t addr[sizeof(struct sockaddr_storage)]; +} rtpmidi_addr; + enum applemidi_command { apple_invite = 0x494E, //IN apple_accept = 0x4F4B, //OK diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 84a7cd6..d15c9b5 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -22,10 +22,11 @@ stream, which may lead to inconsistencies during playback. #### Global configuration -| Option | Example value | Default value | Description | -|---------------|-----------------------|-----------------------|-----------------------| -| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | -| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| Option | Example value | Default value | Description | +|-----------------------|-----------------------|-----------------------|-----------------------| +| `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | #### Instance configuration -- cgit v1.2.3 From bac13064352234acea012de30ee36dd51748b97f Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 11 Apr 2020 19:22:57 +0200 Subject: Implement strerror abstraction for Windows --- TODO | 1 - backends/libmmbackend.c | 21 ++++++++++++++++----- backends/libmmbackend.h | 1 + backends/rtpmidi.c | 19 ++++++++++--------- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/TODO b/TODO index 900cc1b..a4a7e32 100644 --- a/TODO +++ b/TODO @@ -5,5 +5,4 @@ udp backends may ignore MTU mm_managed_fd.impl is not freed currently (and is heaped most of the time anyway) -> documentation make event collectors threadsafe to stop marshalling data... collect & check backend API version -windows strerror move all connection establishment to _start to be able to hot-stop/start all backends diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index ab96646..1624f56 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -17,6 +17,17 @@ int mmbackend_strdup(char** dest, char* src){ return 0; } +char* mmbackend_sockstrerror(int err_no){ + #ifdef _WIN32 + static char error[2048] = ""; + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL); + return error; + #else + return strerror(err_no); + #endif +} + void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; @@ -107,18 +118,18 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_REUSEADDR on socket: %s", strerror(errno)); + LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_sockstrerror(errno)); } if(mcast){ yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_BROADCAST on socket: %s", strerror(errno)); + LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_sockstrerror(errno)); } yes = 0; if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", strerror(errno)); + LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_sockstrerror(errno)); } } @@ -156,7 +167,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - LOGPF("Failed to set socket nonblocking: %s", strerror(errno)); + LOGPF("Failed to set socket nonblocking: %s", mmbackend_sockstrerror(errno)); close(fd); return -1; } @@ -174,7 +185,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ sent = send(fd, data + total, 1, 0); #endif if(sent < 0){ - LOGPF("Failed to send: %s", strerror(errno)); + LOGPF("Failed to send: %s", mmbackend_sockstrerror(errno)); return 1; } total += sent; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index cb211a6..f487ee1 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,6 +21,7 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); +char* mmbackend_sockstrerror(int errno); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 95a50d2..581b16c 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -28,7 +28,6 @@ //TODO announce on mdns input //TODO connect to discovered peers //TODO refactor cfg.announces -//TODO windows address discovery //TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { @@ -325,7 +324,7 @@ static int rtpmidi_announce_addrs(){ struct ifaddrs* ifa = NULL, *iter = NULL; if(getifaddrs(&ifa)){ - LOGPF("Failed to get adapter address information: %s", strerror(errno)); + LOGPF("Failed to get adapter address information: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -411,7 +410,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch } if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", strerror(errno)); + LOGPF("Failed to fetch data port information: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -1242,7 +1241,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset += sizeof(dns_rr_srv); //rfc2782 (srv) says to not compress `target`, rfc6762 (mdns) 18.14 says to - //we dont do it because i dont want to + //we don't do it because i don't want to snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.local", cfg.mdns_name); if(dns_encode_name((char*) frame + offset, &name)){ LOGPF("Failed to encode name for %s", frame + offset); @@ -1486,11 +1485,15 @@ static int rtpmidi_handle_mdns(){ free(name.name); free(host.name); if(bytes <= 0){ + #ifdef _WIN32 + if(WSAGetLastError() == WSAEWOULDBLOCK){ + #else if(errno == EAGAIN){ + #endif return 0; } - LOGPF("Error reading from mDNS descriptor: %s", strerror(errno)); + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_sockstrerror(errno)); return 1; } @@ -1564,14 +1567,12 @@ static int rtpmidi_start_mdns(){ //join ipv4 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join IPv4 multicast group for mDNS: %s", strerror(errno)); - return 1; + LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); } //join ipv6 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ - LOGPF("Failed to join IPv6 multicast group for mDNS: %s", strerror(errno)); - return 1; + LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); } //register mdns fd to core -- cgit v1.2.3 From 9a035724c13396e9ccb3a27287a220a178ec1ab6 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 12 Apr 2020 09:28:05 +0200 Subject: Fix errno shadowing --- backends/libmmbackend.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index f487ee1..969de6f 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,7 +21,7 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); -char* mmbackend_sockstrerror(int errno); +char* mmbackend_sockstrerror(int err_no); /** Networking functions **/ -- cgit v1.2.3 From dd90f0a2283e6feb36d553b32f96efae0999a916 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 12 Apr 2020 12:11:32 +0200 Subject: Move sockaddr_ntop implementation to libmmbackend --- backends/libmmbackend.c | 50 +++++++++++++++++++++++++++++++++++++++++++------ backends/libmmbackend.h | 4 +++- backends/rtpmidi.c | 47 +++++++++++++++++++--------------------------- backends/rtpmidi.md | 3 +++ 4 files changed, 69 insertions(+), 35 deletions(-) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 1624f56..2bbc226 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -17,7 +17,7 @@ int mmbackend_strdup(char** dest, char* src){ return 0; } -char* mmbackend_sockstrerror(int err_no){ +char* mmbackend_socket_strerror(int err_no){ #ifdef _WIN32 static char error[2048] = ""; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), @@ -28,6 +28,44 @@ char* mmbackend_sockstrerror(int err_no){ #endif } +const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length){ + union { + struct sockaddr* in; + struct sockaddr_in* in4; + struct sockaddr_in6* in6; + } addr; + addr.in = peer; + #ifdef _WIN32 + uint8_t* data = NULL; + #endif + + switch(addr.in->sa_family){ + //inet_ntop has become available in the winapi with vista, but eh. + #ifdef _WIN32 + case AF_INET6: + data = addr.in6->sin6_addr.s6_addr; + snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", + data[0], data[1], data[2], data[3], + data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], + data[12], data[13], data[14], data[15]); + return buffer; + case AF_INET: + data = (uint8_t*) &(addr.in4->sin_addr.s_addr); + snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); + return buffer; + #else + case AF_INET6: + return inet_ntop(addr.in->sa_family, &(addr.in6->sin6_addr), buffer, length); + case AF_INET: + return inet_ntop(addr.in->sa_family, &(addr.in4->sin_addr), buffer, length); + #endif + default: + snprintf(buffer, length, "Socket family not implemented"); + return buffer; + } +} + void mmbackend_parse_hostspec(char* spec, char** host, char** port, char** options){ size_t u = 0; @@ -118,18 +156,18 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno)); } if(mcast){ yes = 1; if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno)); } yes = 0; if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } @@ -167,7 +205,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin #else int flags = fcntl(fd, F_GETFL, 0); if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0){ - LOGPF("Failed to set socket nonblocking: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to set socket nonblocking: %s", mmbackend_socket_strerror(errno)); close(fd); return -1; } @@ -185,7 +223,7 @@ int mmbackend_send(int fd, uint8_t* data, size_t length){ sent = send(fd, data + total, 1, 0); #endif if(sent < 0){ - LOGPF("Failed to send: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to send: %s", mmbackend_socket_strerror(errno)); return 1; } total += sent; diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 969de6f..557c243 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -5,6 +5,7 @@ #include //#define close closesocket #else +#include #include #include #endif @@ -21,7 +22,8 @@ /** Convenience functions **/ int mmbackend_strdup(char** dest, char* src); -char* mmbackend_sockstrerror(int err_no); +char* mmbackend_socket_strerror(int err_no); +const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 581b16c..5d588bc 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -247,25 +247,9 @@ static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ } return 0; } - -//this has become available with vista for some reason... -static char* inet_ntop(int family, uint8_t* data, char* buffer, size_t length){ - switch(family){ - case AF_INET: - snprintf(buffer, length, "%d.%d.%d.%d", data[0], data[1], data[2], data[3]); - break; - case AF_INET6: - snprintf(buffer, length, "%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X:%02X%02X", - data[0], data[1], data[2], data[3], - data[4], data[5], data[6], data[7], - data[8], data[9], data[10], data[11], - data[12], data[13], data[14], data[15]); - break; - } - return buffer; -} #endif +//TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ char repr[INET6_ADDRSTRLEN + 1]; union { @@ -314,7 +298,7 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->FriendlyName); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->FriendlyName); cfg.addresses++; } } @@ -324,7 +308,7 @@ static int rtpmidi_announce_addrs(){ struct ifaddrs* ifa = NULL, *iter = NULL; if(getifaddrs(&ifa)){ - LOGPF("Failed to get adapter address information: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to get adapter address information: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -348,7 +332,7 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, inet_ntop(addr.in->sa_family, cfg.address[cfg.addresses].addr, repr, sizeof(repr)), iter->ifa_name); + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->ifa_name); cfg.addresses++; } } @@ -410,7 +394,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch } if(getsockname(data->fd, (struct sockaddr*) &sock_addr, &sock_len)){ - LOGPF("Failed to fetch data port information: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to fetch data port information: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -1396,11 +1380,12 @@ static int rtpmidi_service(){ } //TODO bounds check all accesses -static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host){ +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source){ dns_rr* rr = NULL; dns_rr_srv* srv = NULL; size_t u = 0, offset = sizeof(dns_header); uint8_t* session_name = NULL; + char peer_name[1024]; for(u = 0; u < hdr->questions; u++){ if(dns_decode_name(buffer, length, offset, name)){ @@ -1444,7 +1429,7 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd } //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) - LOGPF("Detected possible peer %.*s on %s Port %d", session_name[0], session_name + 1, host->name, be16toh(srv->port)); + LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port)); offset -= sizeof(dns_rr_srv); } @@ -1462,8 +1447,12 @@ static int rtpmidi_handle_mdns(){ .alloc = 0 }, host = name; ssize_t bytes = 0; + struct sockaddr_storage peer_addr; + socklen_t peer_len = sizeof(peer_addr); - for(bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0); bytes > 0; bytes = recv(cfg.mdns_fd, buffer, sizeof(buffer), 0)){ + for(bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); + bytes > 0; + bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ if(bytes < sizeof(dns_header)){ continue; } @@ -1479,7 +1468,9 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.11: response code != 0 -> ignore DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host); + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr); + + peer_len = sizeof(peer_addr); } free(name.name); @@ -1493,7 +1484,7 @@ static int rtpmidi_handle_mdns(){ return 0; } - LOGPF("Error reading from mDNS descriptor: %s", mmbackend_sockstrerror(errno)); + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); return 1; } @@ -1567,12 +1558,12 @@ static int rtpmidi_start_mdns(){ //join ipv4 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //join ipv6 multicast group if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ - LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_sockstrerror(errno)); + LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //register mdns fd to core diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index d15c9b5..33fbe59 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -89,3 +89,6 @@ specifications in multiple cases. Due to the complexity involved in supporting t arising from this will be considered a bug only in cases where they hinder normal operation of the backend. mDNS discovery may announce flawed records when run on a host with multiple active interfaces. + +While this backend should be reasonably stable, there may be problematic edge cases simply due to the +enormous size and scope of the protocols and implementations required to make this work. -- cgit v1.2.3 From f9ae5d3a21f520380b3f761b71e211e6c438227a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 13 Apr 2020 11:15:20 +0200 Subject: Introduce core iteration limit to break deadlocks --- midimonster.c | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/midimonster.c b/midimonster.c index 5109eab..8100607 100644 --- a/midimonster.c +++ b/midimonster.c @@ -10,6 +10,7 @@ #define MM_API __attribute__((dllexport)) #endif #define BACKEND_NAME "core" +#define MM_SWAP_LIMIT 20 #include "midimonster.h" #include "config.h" #include "backend.h" @@ -339,7 +340,7 @@ static int args_parse(int argc, char** argv, char** cfg_file){ static int core_process(size_t nfds, managed_fd* signaled_fds){ event_collection* secondary = NULL; - size_t u; + size_t u, swaps = 0; //run backend processing, collect events DBGPF("%lu backend FDs signaled\n", nfds); @@ -347,7 +348,8 @@ static int core_process(size_t nfds, managed_fd* signaled_fds){ return 1; } - while(routing.events->n){ + //limit number of collector swaps per iteration to prevent complete deadlock + while(routing.events->n && swaps < MM_SWAP_LIMIT){ //swap primary and secondary event collectors DBGPF("Swapping event collectors, %lu events in primary\n", routing.events->n); for(u = 0; u < sizeof(routing.pool) / sizeof(routing.pool[0]); u++){ @@ -368,6 +370,10 @@ static int core_process(size_t nfds, managed_fd* signaled_fds){ secondary->n = 0; } + if(swaps == MM_SWAP_LIMIT){ + LOG("Iteration swap limit hit, a backend may be configured to route events in an infinite loop"); + } + return 0; } -- cgit v1.2.3 From fe6a11b69fdc3ea7c01f8ffca0c0aa26abe6d9ca Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 13 Apr 2020 22:41:29 +0200 Subject: Refactor local address query, document libmmbackend calls --- backends/libmmbackend.h | 22 ++++++++ backends/rtpmidi.c | 133 ++++++++++++++++++------------------------------ backends/rtpmidi.h | 6 +-- 3 files changed, 75 insertions(+), 86 deletions(-) diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 557c243..08f03aa 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -21,8 +21,30 @@ /** Convenience functions **/ +/* + * Duplicate src into *dest, freeing earlier content of *dest if present + * On success, 0 is returned + * On failure, a message is printed, *dest is a NULL pointer and 1 is returned + */ int mmbackend_strdup(char** dest, char* src); + +/* + * Return a formatted error message pertaining to the last socket operation. + * On Linux/OSX, this calls through to strerror using the provided err_no. + * On Windows, err_no is ignored and WSAGetLastError is called to retrieve + * the status of the last operation. This information is then processed via + * FormatMessage into a fixed buffer, which is returned. Thus, this function + * is not thread-safe on Windows. On Linux, refer to strerror's documentation + * for information on thread-safety. + */ char* mmbackend_socket_strerror(int err_no); + +/* + * Wrap / reimplement (on Windows) inet_ntop to work with struct sockaddr* directly. + * Prints the address in a "human-readable" form into buffer. + * Will modify at most length bytes into buffer, output will be zero-terminated. + * This function only works with AF_INET and AF_INET6 addresses. + */ const char* mmbackend_sockaddr_ntop(struct sockaddr* peer, char* buffer, size_t length); /** Networking functions **/ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 5d588bc..b7d938b 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,7 +19,6 @@ #include #endif - //#include "../tests/hexdump.c" //TODO learn peer ssrcs @@ -27,29 +26,34 @@ //TODO internal loop mode //TODO announce on mdns input //TODO connect to discovered peers -//TODO refactor cfg.announces //TODO for some reason, the announce packet generates an exception in the wireshark dissector static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; + char* mdns_interface; + uint8_t detect; uint64_t last_service; - char* mdns_interface; size_t addresses; rtpmidi_addr* address; - size_t announces; - rtpmidi_announce* announce; + size_t invites; + rtpmidi_invite* invite; } cfg = { .mdns_fd = -1, .mdns_name = NULL, + .mdns_interface = NULL, + .detect = 0, .last_service = 0, - .announces = 0, - .announce = NULL + .addresses = 0, + .address = NULL, + + .invites = 0, + .invite = NULL }; MM_PLUGIN_API int init(){ @@ -232,26 +236,9 @@ bail: return -1; } -#ifdef _WIN32 -static int rtpmidi_wide_pfxcmp(wchar_t* haystack, char* needle){ - size_t u; - - for(u = 0; haystack[u] && needle[u]; u++){ - if(haystack[u] != needle[u]){ - return 1; - } - } - - if(!haystack[u] && needle[u]){ - return 1; - } - return 0; -} -#endif - //TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ - char repr[INET6_ADDRSTRLEN + 1]; + char repr[INET6_ADDRSTRLEN + 1] = "", iface[1024] = ""; union { struct sockaddr_in* in4; struct sockaddr_in6* in6; @@ -259,51 +246,27 @@ static int rtpmidi_announce_addrs(){ } addr; #ifdef _WIN32 - size_t bytes_alloc = 50 * sizeof(IP_ADAPTER_ADDRESSES); IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; - IP_ADAPTER_ADDRESSES* addrs = calloc(1, bytes_alloc), *iter = NULL; - if(!addrs){ - LOG("Failed to allocate memory"); - return 1; - } + IP_ADAPTER_ADDRESSES addrs[50] , *iter = NULL; + size_t bytes_alloc = sizeof(addrs); if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ //FIXME might try to resize the result list and retry at some point... LOG("Failed to query local interface addresses"); - free(addrs); return 1; } for(iter = addrs; iter; iter = iter->Next){ + //friendlyname is a wide string, print it into interface for basic conversion and to avoid implementing wide string handling + snprintf(iface, sizeof(iface), "%S", iter->FriendlyName); //filter interfaces if requested - if(cfg.mdns_interface && rtpmidi_wide_pfxcmp(iter->FriendlyName, cfg.mdns_interface)){ + if(cfg.mdns_interface && strncmp(iface, cfg.mdns_interface, min(strlen(iface), strlen(cfg.mdns_interface)))){ continue; } for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ addr.in = unicast_addr->Address.lpSockaddr; - if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ - continue; - } - - cfg.address = realloc(cfg.address, (cfg.addresses + 1) * sizeof(rtpmidi_addr)); - if(!cfg.address){ - cfg.addresses = 0; - LOG("Failed to allocate memory"); - return 1; - } - - cfg.address[cfg.addresses].family = addr.in->sa_family; - memcpy(&cfg.address[cfg.addresses].addr, - (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, - (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %S)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->FriendlyName); - cfg.addresses++; - } - } - - free(addrs); #else struct ifaddrs* ifa = NULL, *iter = NULL; @@ -316,7 +279,9 @@ static int rtpmidi_announce_addrs(){ if((!cfg.mdns_interface || !strcmp(cfg.mdns_interface, iter->ifa_name)) && strcmp(iter->ifa_name, "lo") && iter->ifa_addr){ + snprintf(iface, sizeof(iface), "%s", iter->ifa_name); addr.in = iter->ifa_addr; + #endif if(addr.in->sa_family != AF_INET && addr.in->sa_family != AF_INET6){ continue; } @@ -332,11 +297,13 @@ static int rtpmidi_announce_addrs(){ memcpy(&cfg.address[cfg.addresses].addr, (addr.in->sa_family == AF_INET) ? (void*) &addr.in4->sin_addr.s_addr : (void*) &addr.in6->sin6_addr.s6_addr, (addr.in->sa_family == AF_INET) ? 4 : 16); - LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iter->ifa_name); + + LOGPF("mDNS announce address %" PRIsize_t ": %s (from %s)", cfg.addresses, mmbackend_sockaddr_ntop(addr.in, repr, sizeof(repr)), iface); cfg.addresses++; } } + #ifndef _WIN32 freeifaddrs(ifa); #endif @@ -474,52 +441,52 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag static int rtpmidi_push_invite(instance* inst, char* peer){ size_t u, p; - //check whether the instance is already in the announce list - for(u = 0; u < cfg.announces; u++){ - if(cfg.announce[u].inst == inst){ + //check whether the instance is already in the inviter list + for(u = 0; u < cfg.invites; u++){ + if(cfg.invite[u].inst == inst){ break; } } - //add to the announce list - if(u == cfg.announces){ - cfg.announce = realloc(cfg.announce, (cfg.announces + 1) * sizeof(rtpmidi_announce)); - if(!cfg.announce){ + //add to the inviter list + if(u == cfg.invites){ + cfg.invite = realloc(cfg.invite, (cfg.invites + 1) * sizeof(rtpmidi_invite)); + if(!cfg.invite){ LOG("Failed to allocate memory"); - cfg.announces = 0; + cfg.invites = 0; return 1; } - cfg.announce[u].inst = inst; - cfg.announce[u].invites = 0; - cfg.announce[u].invite = NULL; + cfg.invite[u].inst = inst; + cfg.invite[u].invites = 0; + cfg.invite[u].name = NULL; - cfg.announces++; + cfg.invites++; } - //check whether the peer is already in the invite list - for(p = 0; p < cfg.announce[u].invites; p++){ - if(!strcmp(cfg.announce[u].invite[p], peer)){ + //check whether the requested name is already in the invite list for this instance + for(p = 0; p < cfg.invite[u].invites; p++){ + if(!strcmp(cfg.invite[u].name[p], peer)){ return 0; } } //extend the invite list - cfg.announce[u].invite = realloc(cfg.announce[u].invite, (cfg.announce[u].invites + 1) * sizeof(char*)); - if(!cfg.announce[u].invite){ + cfg.invite[u].name = realloc(cfg.invite[u].name, (cfg.invite[u].invites + 1) * sizeof(char*)); + if(!cfg.invite[u].name){ LOG("Failed to allocate memory"); - cfg.announce[u].invites = 0; + cfg.invite[u].invites = 0; return 1; } //append the new invitee - cfg.announce[u].invite[p] = strdup(peer); - if(!cfg.announce[u].invite[p]){ + cfg.invite[u].name[p] = strdup(peer); + if(!cfg.invite[u].name[p]){ LOG("Failed to allocate memory"); return 1; } - cfg.announce[u].invites++; + cfg.invite[u].invites++; return 0; } @@ -1666,15 +1633,15 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ inst[u]->impl = NULL; } - for(u = 0; u < cfg.announces; u++){ - for(p = 0; p < cfg.announce[u].invites; p++){ - free(cfg.announce[u].invite[p]); + for(u = 0; u < cfg.invites; u++){ + for(p = 0; p < cfg.invite[u].invites; p++){ + free(cfg.invite[u].name[p]); } - free(cfg.announce[u].invite); + free(cfg.invite[u].name); } - free(cfg.announce); - cfg.announce = NULL; - cfg.announces = 0; + free(cfg.invite); + cfg.invite = NULL; + cfg.invites = 0; free(cfg.address); cfg.addresses = 0; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 9d2c40a..de610cd 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -86,11 +86,11 @@ typedef struct /*_rtmidi_instance_data*/ { uint8_t learn_peers; } rtpmidi_instance_data; -typedef struct /*rtpmidi_announced_instance*/ { +typedef struct /*rtpmidi_invited_peer*/ { instance* inst; size_t invites; - char** invite; -} rtpmidi_announce; + char** name; +} rtpmidi_invite; typedef struct /*_rtpmidi_addr*/ { int family; -- cgit v1.2.3 From 4b991a31eed602070d241e77e98d62e06d647289 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Mon, 13 Apr 2020 23:54:33 +0200 Subject: Installer Update --- installer.sh | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/installer.sh b/installer.sh index 111c783..66eef99 100755 --- a/installer.sh +++ b/installer.sh @@ -71,7 +71,7 @@ ARGS(){ -fu|--forceupdate) UPDATER_FORCE="1" ;; - --install-updater) + --install-updater|--selfupdate) NIGHTLY=1 prepare_repo install_script exit 0 @@ -83,19 +83,20 @@ ARGS(){ -h|--help|*) assign_defaults printf "${bold}Usage:${normal} ${0} ${c_green}[OPTIONS]${normal}" - printf "\n\t${c_green}--prefix${normal} ${c_red}${normal}\t\tSet the installation prefix\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" - printf "\n\t${c_green}--plugins${normal} ${c_red}${normal}\tSet the plugin install path\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" - printf "\n\t${c_green}--defcfg${normal} ${c_red}${normal}\t\tSet the default configuration path\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" - printf "\n\t${c_green}--examples${normal} ${c_red}${normal}\tSet the path for example configurations\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" - printf "\n\t${c_green}--dev${normal}\t\t\t\tInstall nightly version" - printf "\n\t${c_green}-d, --default${normal}\t\t\tUse default values to install" - printf "\n\t${c_green}-fu, --forceupdate${normal}\t\tForce the updater to update without a version check" - printf "\n\t${c_green}--install-updater${normal}\t\tInstall the updater (Run with midimonster-updater)" - printf "\n\t${c_green}--install-dependencies${normal}\t\tInstall dependencies and exit" - printf "\n\t${c_green}-h, --help${normal}\t\t\tShow this message and exit" - printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!${normal}\n" + printf "\n\t${c_green}--prefix=${normal}${c_red}${normal}\t\tSet the installation prefix.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PREFIX" + printf "\n\t${c_green}--plugins=${normal}${c_red}${normal}\tSet the plugin install path.\t\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_PLUGINS" + printf "\n\t${c_green}--defcfg=${normal}${c_red}${normal}\t\tSet the default configuration path.\t${c_mag}Default:${normal} ${dim}%s${normal}" "$VAR_DEFAULT_CFG" + printf "\n\t${c_green}--examples=${normal}${c_red}${normal}\tSet the path for example configurations.\t${c_mag}Default:${normal} ${dim}%s${normal}\n" "$VAR_EXAMPLE_CFGS" + printf "\n\t${c_green}--dev${normal}\t\t\tInstall nightly version." + printf "\n\t${c_green}-d,\t--default${normal}\tUse default values to install." + printf "\n\t${c_green}-fu,\t--forceupdate${normal}\tForce the updater to update without a version check." + printf "\n\t${c_green}--selfupdate${normal}\t\tUpdates this script to the newest version and exit." + printf "\n\t${c_green}--install-updater${normal}\tInstall the updater (Run with midimonster-updater) and exit." + printf "\n\t${c_green}--install-dependencies${normal}\tInstall dependencies and exit" + printf "\n\t${c_green}-h,\t--help${normal}\t\tShow this message and exit." + printf "\n\t${uline}${bold}${c_mag}Each argument can be overwritten by another, the last one is used!.${normal}\n" rmdir "$tmp_path" - exit 1 + exit 0 ;; esac shift -- cgit v1.2.3 From b34a3f142495da9a2879b6ff13623d9a50dcfb89 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 17 Apr 2020 23:22:16 +0200 Subject: Extend Windows error reporting, refactor apple command transmission --- backends/rtpmidi.c | 105 +++++++++++++++++++++++++++-------------------------- backends/rtpmidi.h | 7 ++-- 2 files changed, 58 insertions(+), 54 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index b7d938b..85665fd 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -238,7 +238,7 @@ bail: //TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ - char repr[INET6_ADDRSTRLEN + 1] = "", iface[1024] = ""; + char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = ""; union { struct sockaddr_in* in4; struct sockaddr_in6* in6; @@ -247,13 +247,16 @@ static int rtpmidi_announce_addrs(){ #ifdef _WIN32 IP_ADAPTER_UNICAST_ADDRESS_LH* unicast_addr = NULL; - IP_ADAPTER_ADDRESSES addrs[50] , *iter = NULL; + IP_ADAPTER_ADDRESSES addrs[250] , *iter = NULL; size_t bytes_alloc = sizeof(addrs); - if(GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, - NULL, addrs, (unsigned long*) &bytes_alloc) != ERROR_SUCCESS){ + unsigned long status = GetAdaptersAddresses(0, GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, + NULL, addrs, (unsigned long*) &bytes_alloc); + if(status != ERROR_SUCCESS){ //FIXME might try to resize the result list and retry at some point... - LOG("Failed to query local interface addresses"); + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, status, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), iface, sizeof(iface), NULL); + LOGPF("Failed to query local interface addresses (%lu): %s", status, iface); return 1; } @@ -401,11 +404,12 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ //check whether the peer is already in the list + //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present) if(data->peer[u].active && sock_len == data->peer[u].dest_len && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ @@ -433,6 +437,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer[p].active = 1; data->peer[p].learned = learned; data->peer[p].connected = connected; + data->peer[p].invite = invite_reference; data->peer[p].dest = sock_addr; data->peer[p].dest_len = sock_len; return 0; @@ -490,6 +495,37 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } +static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + + apple_command* cmd = (apple_command*) &frame; + cmd->res1 = 0xFFFF; + cmd->command = htobe16(command); + cmd->version = htobe32(2); + cmd->token = ((uint32_t) rand()) << 16 | rand(); + cmd->ssrc = htobe32(data->ssrc); + + //append session name to packet + memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); + + //FIXME should we match sending/receiving ports? if the reference does this, it should be documented + return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, dest, dest_len); +} + +static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; + struct sockaddr_storage dest_addr; + + memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len)); + if(control){ + //calculate remote control port from data port + ((struct sockaddr_in*) &dest_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); + } + + return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command); +} + static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; char* host = NULL, *port = NULL; @@ -563,7 +599,7 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); } - return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0); + return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); } else if(!strcmp(option, "title")){ if(data->mode != apple){ @@ -773,56 +809,36 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //accept the invitation LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); //send accept message - apple_command* accept = (apple_command*) response; - accept->res1 = 0xFFFF; - accept->command = htobe16(apple_accept); - accept->version = htobe32(2); - accept->token = command->token; - accept->ssrc = htobe32(data->ssrc); - //add local name to response - //FIXME use instance title instead of mdns_name - memcpy(response + sizeof(apple_command), cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME, strlen((cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME)) + 1); - sendto(fd, response, sizeof(apple_command) + strlen(cfg.mdns_name ? cfg.mdns_name : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + //TODO accept->token = command->token; + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept); //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); } return 0; } else{ //send reject message LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); - apple_command reject = { - .res1 = 0xFFFF, - .command = htobe16(apple_reject), - .version = htobe32(2), - .token = command->token, - .ssrc = htobe32(data->ssrc) - }; - sendto(fd, (uint8_t*) &reject, sizeof(apple_command), 0, (struct sockaddr*) peer, peer_len); + //TODO .token = command->token, + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject); } return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1); + return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); //FIXME store ssrc, start timesync } else{ - //send invite on data fd + //invite peer data port LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); - apple_command* invite = (apple_command*) response; - invite->res1 = 0xFFFF; - invite->command = htobe16(apple_invite); - invite->version = htobe32(2); - invite->token = command->token; - invite->ssrc = htobe32(data->ssrc); - memcpy(response + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); //calculate data port ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); - sendto(data->fd, response, sizeof(apple_command) + strlen(data->title ? data->title : RTPMIDI_DEFAULT_NAME) + 1, 0, (struct sockaddr*) peer, peer_len); + //send invite + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite); } return 0; } @@ -1064,7 +1080,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1); + return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1, -1); } } return 0; @@ -1281,7 +1297,6 @@ static int rtpmidi_service(){ size_t n, u, p; instance** inst = NULL; rtpmidi_instance_data* data = NULL; - uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; struct sockaddr_storage control_peer; //prepare commands @@ -1294,11 +1309,6 @@ static int rtpmidi_service(){ mm_timestamp() * 10 } }; - apple_command* invite = (apple_command*) &frame; - invite->res1 = 0xFFFF; - invite->command = htobe16(apple_invite); - invite->version = htobe32(2); - invite->token = ((uint32_t) rand()) << 16 | rand(); if(mm_backend_instances(BACKEND_NAME, &n, &inst)){ LOG("Failed to fetch instances"); @@ -1329,14 +1339,7 @@ static int rtpmidi_service(){ else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); - invite->ssrc = htobe32(data->ssrc); - //calculate remote control port from data port - memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); - ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - //append session name to packet - memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); - - sendto(data->control_fd, (char*) invite, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite); } } } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index de610cd..a5d4f4c 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -61,8 +61,9 @@ typedef struct /*_rtpmidi_peer*/ { socklen_t dest_len; //uint32_t ssrc; uint8_t active; //marked for reuse - uint8_t learned; //learned / configured peer + uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown) uint8_t connected; //currently in active session + ssize_t invite; //invite-list index for apple-mode learned peers } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { @@ -98,14 +99,14 @@ typedef struct /*_rtpmidi_addr*/ { uint8_t addr[sizeof(struct sockaddr_storage)]; } rtpmidi_addr; -enum applemidi_command { +typedef enum { apple_invite = 0x494E, //IN apple_accept = 0x4F4B, //OK apple_reject = 0x4E4F, //NO apple_leave = 0x4259, //BY apple_sync = 0x434B, //CK apple_feedback = 0x5253 //RS -}; +} applemidi_command; typedef struct /*_dns_name*/ { size_t alloc; -- cgit v1.2.3 From 5ca868f4bf5a48e0d6bd456c02fe834e80dd9fcf Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 18 Apr 2020 17:34:29 +0200 Subject: Use instance name as mDNS announce name --- backends/rtpmidi.c | 64 ++++++++++++++++++----------------------------------- backends/rtpmidi.h | 1 - backends/rtpmidi.md | 3 +-- 3 files changed, 22 insertions(+), 46 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 85665fd..3e998ee 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -507,10 +507,10 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl cmd->ssrc = htobe32(data->ssrc); //append session name to packet - memcpy(frame + sizeof(apple_command), data->title ? data->title : RTPMIDI_DEFAULT_NAME, strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1); + memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1); //FIXME should we match sending/receiving ports? if the reference does this, it should be documented - return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen((data->title ? data->title : RTPMIDI_DEFAULT_NAME)) + 1, 0, dest, dest_len); + return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); } static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ @@ -601,17 +601,6 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); } - else if(!strcmp(option, "title")){ - if(data->mode != apple){ - LOG("'title' option is only valid for apple mode instances"); - return 1; - } - if(strchr(value, '.') || strlen(value) > 254){ - LOGPF("Invalid instance title %s on %s: Must be shorter than 254 characters, no periods", value, inst->name); - return 1; - } - return mmbackend_strdup(&data->title, value); - } else if(!strcmp(option, "invite")){ if(data->mode != apple){ LOG("'invite' option is only valid for apple mode instances"); @@ -1133,7 +1122,7 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ return 0; } -static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ +static int rtpmidi_mdns_detach(instance* inst){ uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; dns_rr* rr = NULL; @@ -1159,12 +1148,12 @@ static int rtpmidi_mdns_detach(rtpmidi_instance_data* data){ offset += bytes; //TODO length-checks here - frame[offset++] = strlen(data->title); - memcpy(frame + offset, data->title, strlen(data->title)); - offset += strlen(data->title); + frame[offset++] = strlen(inst->name); + memcpy(frame + offset, inst->name, strlen(inst->name)); + offset += strlen(inst->name); frame[offset++] = 0xC0; frame[offset++] = sizeof(dns_header); - rr->data = htobe16(1 + strlen(data->title) + 2); + rr->data = htobe16(1 + strlen(inst->name) + 2); free(name.name); return rtpmidi_mdns_broadcast(frame, offset); @@ -1174,7 +1163,8 @@ bail: } //FIXME this should not exceed 1500 bytes -static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ +static int rtpmidi_mdns_announce(instance* inst){ + rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; dns_header* hdr = (dns_header*) frame; dns_rr* rr = NULL; @@ -1194,7 +1184,7 @@ static int rtpmidi_mdns_announce(rtpmidi_instance_data* data){ offset = sizeof(dns_header); //answer 1: SRV FQDN - snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", data->title, RTPMIDI_MDNS_DOMAIN); + snprintf((char*) frame + offset, sizeof(frame) - offset, "%s.%s", inst->name, RTPMIDI_MDNS_DOMAIN); bytes = dns_push_rr(frame + offset, sizeof(frame) - offset, &rr, (char*) frame + offset, 33, 1, 120, 0); if(bytes < 0){ goto bail; @@ -1320,9 +1310,9 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0 && data->title + if(cfg.mdns_fd >= 0 && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ - rtpmidi_mdns_announce(data); + rtpmidi_mdns_announce(inst[u]); } for(p = 0; p < data->peers; p++){ @@ -1542,8 +1532,8 @@ static int rtpmidi_start_mdns(){ static int rtpmidi_start(size_t n, instance** inst){ size_t u, p, fds = 0; - rtpmidi_instance_data* data = NULL, *other = NULL; - uint8_t mdns_required = 0; + rtpmidi_instance_data* data = NULL; + uint8_t mdns_requested = 0; for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; @@ -1571,17 +1561,8 @@ static int rtpmidi_start(size_t n, instance** inst){ data->peer[p].connected = 1; } } - else if(data->mode == apple && data->title){ - //check for unique title - for(p = 0; p < u; p++){ - other = (rtpmidi_instance_data*) inst[p]->impl; - if(other->mode == apple && other->title - && !strcmp(data->title, other->title)){ - LOGPF("Instance titles are required to be unique to allow for mDNS discovery, conflict between %s and %s", inst[p]->name, inst[u]->name); - return 1; - } - } - mdns_required = 1; + else if(data->mode == apple){ + mdns_requested = 1; } //register fds to core @@ -1592,10 +1573,10 @@ static int rtpmidi_start(size_t n, instance** inst){ fds += (data->control_fd >= 0) ? 2 : 1; } - if(mdns_required && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ - return 1; + if(mdns_requested && (rtpmidi_announce_addrs() || rtpmidi_start_mdns())){ + LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers"); } - else if(mdns_required){ + else if(mdns_requested){ fds++; } @@ -1610,8 +1591,8 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; - if(cfg.mdns_fd >= 0 && data->mode == apple && data->title){ - rtpmidi_mdns_detach(data); + if(cfg.mdns_fd >= 0 && data->mode == apple){ + rtpmidi_mdns_detach(inst[u]); } if(data->fd >= 0){ @@ -1622,9 +1603,6 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ close(data->control_fd); } - free(data->title); - data->title = NULL; - free(data->accept); data->accept = NULL; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a5d4f4c..a6b76d9 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -79,7 +79,6 @@ typedef struct /*_rtmidi_instance_data*/ { uint16_t sequence; //apple-midi config - char* title; char* accept; uint64_t last_announce; diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 33fbe59..a0098b0 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -25,7 +25,7 @@ stream, which may lead to inconsistencies during playback. | Option | Example value | Default value | Description | |-----------------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | -| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances with `title` configuration will be announced via mDNS if set. | +| `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances will be announced via mDNS if set. | | `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | #### Instance configuration @@ -50,7 +50,6 @@ Common instance configuration parameters | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|-----------------------| | `bind` | `10.1.2.1 9001` | `:: ` | Local network address to bind to (note that AppleMIDI requires two consecutive port numbers to be allocated). | -| `title` | `Just Jamming` | none | Session/device name to announce via mDNS. If unset, the instance will not be announced. | | `invite` | `pad` | none | Devices to send invitations to when discovered (the special value `*` invites all discovered peers). May be specified multiple times. | | `join` | `Just Jamming` | none | Session for which to accept invitations (the special value `*` accepts the first invitation seen). | -- cgit v1.2.3 From 7c1a283c9213dd273d04af72e24b08beb83aec12 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 15:27:28 +0200 Subject: Connect discovered mDNS peers --- backends/rtpmidi.c | 147 +++++++++++++++++++++++++++++++++++++++++++---------- backends/rtpmidi.h | 2 +- 2 files changed, 120 insertions(+), 29 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 3e998ee..443967d 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -24,9 +24,10 @@ //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode -//TODO announce on mdns input -//TODO connect to discovered peers -//TODO for some reason, the announce packet generates an exception in the wireshark dissector +//TODO for some reason, the announce packet generates an exception in the wireshark dns dissector +//TODO rename and document most functions +//TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them +//TODO ipv6-mapped-ipv4 creates problens when connecting on a ipv4-bound instance static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -378,7 +379,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch return 1; } - LOGPF("Apple mode instance %s listening on port %d", inst->name, data->control_port); + LOGPF("Apple mode instance %s listening on ports %d (control) and %d (data)", inst->name, data->control_port, data->control_port + 1); } else{ data->control_port = be16toh(((struct sockaddr_in*)&sock_addr)->sin_port); @@ -404,7 +405,7 @@ static char* rtpmidi_type_name(uint8_t type){ return "unknown"; } -static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storage sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ +static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr* sock_addr, socklen_t sock_len, uint8_t learned, uint8_t connected, ssize_t invite_reference){ size_t u, p = data->peers; for(u = 0; u < data->peers; u++){ @@ -412,7 +413,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag //TODO this probably should take into account the invite_reference (-1 for initiator peers or if unknown but may be present) if(data->peer[u].active && sock_len == data->peer[u].dest_len - && !memcmp(&data->peer[u].dest, &sock_addr, sock_len)){ + && !memcmp(&data->peer[u].dest, sock_addr, sock_len)){ //if yes, update connection flag (but not learned flag because that doesn't change) data->peer[u].connected = connected; return 0; @@ -438,7 +439,7 @@ static int rtpmidi_push_peer(rtpmidi_instance_data* data, struct sockaddr_storag data->peer[p].learned = learned; data->peer[p].connected = connected; data->peer[p].invite = invite_reference; - data->peer[p].dest = sock_addr; + memcpy(&(data->peer[p].dest), sock_addr, sock_len); data->peer[p].dest_len = sock_len; return 0; } @@ -495,7 +496,7 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } -static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command){ +static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; @@ -503,7 +504,7 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl cmd->res1 = 0xFFFF; cmd->command = htobe16(command); cmd->version = htobe32(2); - cmd->token = ((uint32_t) rand()) << 16 | rand(); + cmd->token = token ? token : (((uint32_t) rand()) << 16 | rand()); cmd->ssrc = htobe32(data->ssrc); //append session name to packet @@ -513,17 +514,17 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); } -static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command){ +static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; struct sockaddr_storage dest_addr; memcpy(&dest_addr, &(data->peer[peer].dest), min(sizeof(dest_addr), data->peer[peer].dest_len)); if(control){ //calculate remote control port from data port - ((struct sockaddr_in*) &dest_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); + ((struct sockaddr_in*) &dest_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &dest_addr)->sin_port) - 1); } - return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command); + return rtpmidi_applecommand(inst, (struct sockaddr*) &dest_addr, data->peer[peer].dest_len, control, command, token); } static int rtpmidi_configure_instance(instance* inst, char* option, char* value){ @@ -596,10 +597,10 @@ static int rtpmidi_configure_instance(instance* inst, char* option, char* value) //apple peers are specified using the control port, but we want to store the data port as peer if(data->mode == apple){ - ((struct sockaddr_in*) &sock_addr)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); + ((struct sockaddr_in*) &sock_addr)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) + 1); } - return rtpmidi_push_peer(data, sock_addr, sock_len, 0, 0, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 0, 0, -1); } else if(!strcmp(option, "invite")){ if(data->mode != apple){ @@ -798,36 +799,34 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size //accept the invitation LOGPF("Instance %s accepting invitation to session %s%s", inst->name, session_name ? session_name : "UNNAMED", (fd == data->control_fd) ? " (control)":""); //send accept message - //TODO accept->token = command->token; - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_accept, command->token); //push peer if(fd != data->control_fd){ - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1); } return 0; } else{ //send reject message LOGPF("Instance %s rejecting invitation to session %s", inst->name, session_name ? session_name : "UNNAMED"); - //TODO .token = command->token, - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, (fd == data->control_fd) ? 1 : 0, apple_reject, command->token); } return 0; } else if(command->command == apple_accept){ if(fd != data->control_fd){ LOGPF("Instance %s negotiated new peer", inst->name); - return rtpmidi_push_peer(data, *peer, peer_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) peer, peer_len, 1, 1, -1); //FIXME store ssrc, start timesync } else{ //invite peer data port LOGPF("Instance %s peer accepted on control port, inviting data port", inst->name); //calculate data port - ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1); //send invite - rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite); + rtpmidi_applecommand(inst, (struct sockaddr*) peer, peer_len, 0, apple_invite, 0); } return 0; } @@ -837,7 +836,7 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size } else if(command->command == apple_leave){ //remove peer from list - this comes in on the control port, but we need to remove the data port... - ((struct sockaddr_in*) peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) peer)->sin_port) + 1); + ((struct sockaddr_in*) peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) peer)->sin_port) + 1); for(u = 0; u < data->peers; u++){ if(data->peer[u].dest_len == peer_len && !memcmp(&data->peer[u].dest, peer, peer_len)){ @@ -1069,7 +1068,7 @@ static int rtpmidi_handle_data(instance* inst){ if(u == data->peers){ LOGPF("Learned new peer on %s", inst->name); - return rtpmidi_push_peer(data, sock_addr, sock_len, 1, 1, -1); + return rtpmidi_push_peer(data, (struct sockaddr*) &sock_addr, sock_len, 1, 1, -1); } } return 0; @@ -1322,14 +1321,14 @@ static int rtpmidi_service(){ sync.ssrc = htobe32(data->ssrc); //calculate remote control port from data port memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); - ((struct sockaddr_in*) &control_peer)->sin_port = be16toh(htobe16(((struct sockaddr_in*) &control_peer)->sin_port) - 1); + ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1); sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers DBGPF("Instance %s inviting configured peer %" PRIsize_t, inst[u]->name, p); - rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite); + rtpmidi_peer_applecommand(inst[u], p, 1, apple_invite, 0); } } } @@ -1339,8 +1338,98 @@ static int rtpmidi_service(){ return 0; } +static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, socklen_t peer_len, uint16_t control_port){ + //due to mdns restrictions, session names can at most be 255 characters long + char session_name[1024] = ""; + rtpmidi_instance_data* data = NULL; + size_t u, n, p; + uint8_t done = 0; + + //modify peer to match the data port for the indicated control port + ((struct sockaddr_in*) peer)->sin_port = htobe16(control_port + 1); + snprintf(session_name, sizeof(session_name), "%.*s", session_raw[0], session_raw + 1); + + //find instances that invite exactly this peer + for(u = 0; u < cfg.invites; u++){ + for(n = 0; n < cfg.invite[u].invites; n++){ + if(strlen(cfg.invite[u].name[n]) == session_raw[0] + && !strcmp(cfg.invite[u].name[n], session_name)){ + done = 1; + data = (rtpmidi_instance_data*) cfg.invite[u].inst->impl; + DBGPF("Peer %s explicitly invited on instance %s", session_name, cfg.invite[u].inst->name); + + //check whether this peer (or its equivalent on another protocol) is already connected + for(p = 0; p < data->peers; p++){ + //FIXME might want to scan for explicitly configured peers that match the announced peer + if(data->peer[p].active + && data->peer[p].learned + && data->peer[p].invite == n){ + //we already learned of this peer + break; + } + } + + if(p == data->peers){ + //push a new peer + if(rtpmidi_push_peer(data, peer, peer_len, 1, 0, n)){ + return 1; + } + //find it again + for(p = 0; p < data->peers; p++){ + if(data->peer[p].active + && data->peer[p].learned + && data->peer[p].invite == n){ + //we already learned of this peer + break; + } + } + } + else{ + //if connected, we're done for this instance + //if not, at least the family should match + if(data->peer[p].connected + || data->peer[p].dest.ss_family != peer->sa_family){ + break; + } + + //if not connected and family matches, overwrite + memcpy(&(data->peer[p].dest), peer, data->peer[p].dest_len); + } + + //connect either the pushed or overwritten peer + LOGPF("Inviting peer %s to instance %s", session_name, cfg.invite[u].inst->name); + rtpmidi_peer_applecommand(cfg.invite[u].inst, p, 1, apple_invite, 0); + } + } + } + + //if we found at least one match before, we don't check wildcard invites + if(done){ + return 0; + } + + //find instances with a wildcard invite + for(u = 0; u < cfg.invites; u++){ + for(n = 0; n < cfg.invite[u].invites; n++){ + if(!strcmp(cfg.invite[u].name[n], "*")){ + done = 1; + DBGPF("Peer %.*s implicitly invited on instance %s, converting to explicit invitation", session_name[0], session_name + 1, cfg.invite[u].inst->name); + if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){ + return 1; + } + } + } + } + + //recurse to connect now-explicit invitations + if(done){ + rtpmidi_apple_peermatch(session_raw, peer, peer_len, control_port); + } + return 0; +} + //TODO bounds check all accesses -static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source){ +static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hdr, dns_name* name, dns_name* host, struct sockaddr* source, socklen_t source_len){ dns_rr* rr = NULL; dns_rr_srv* srv = NULL; size_t u = 0, offset = sizeof(dns_header); @@ -1391,6 +1480,8 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd //we just use the packet's source as peer, because who would announce mdns for another host (also implementing an additional registry for this would bloat this backend further) LOGPF("Detected possible peer %.*s on %s (%s) Port %d", session_name[0], session_name + 1, host->name, mmbackend_sockaddr_ntop(source, peer_name, sizeof(peer_name)), be16toh(srv->port)); offset -= sizeof(dns_rr_srv); + + rtpmidi_apple_peermatch(session_name, source, source_len, be16toh(srv->port)); } offset += be16toh(rr->data); @@ -1428,7 +1519,7 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.11: response code != 0 -> ignore DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); - rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr); + rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); } diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index a6b76d9..9d46911 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -63,7 +63,7 @@ typedef struct /*_rtpmidi_peer*/ { uint8_t active; //marked for reuse uint8_t learned; //learned / configured peer (learned peers are marked inactive on session shutdown) uint8_t connected; //currently in active session - ssize_t invite; //invite-list index for apple-mode learned peers + ssize_t invite; //invite-list index for apple-mode learned peers (used to track ipv6/ipv4 overlapping invitations) } rtpmidi_peer; typedef struct /*_rtmidi_instance_data*/ { -- cgit v1.2.3 From ea123f01f769b90b31b4bc78e2bdff5517e7e8de Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 20:48:03 +0200 Subject: Add note to rtpmidi documentation --- backends/rtpmidi.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index a0098b0..2194a05 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -1,7 +1,8 @@ ### The `rtpmidi` backend This backend provides read-write access to RTP MIDI streams, which transfer MIDI data -over the network. Notably, it has native support in Apple devices. +over the network. Notably, RTP MIDI has native support in Apple devices including their +tablets. As the specification for RTP MIDI does not normatively indicate any method for session management, most vendors define their own standards for this. @@ -83,6 +84,9 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems +This backend is currently still a work in progress, and is not mentioned on the main README yet for that reason. +Critical feedback and tests across multiple devices are very welcome. + The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating the specifications in multiple cases. Due to the complexity involved in supporting these protocols, problems arising from this will be considered a bug only in cases where they hinder normal operation of the backend. -- cgit v1.2.3 From a3a79b824a1936868ba4ab5be53998593b0e9047 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 21:11:28 +0200 Subject: Fix Coverity issues --- backends/rtpmidi.c | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 443967d..af90f27 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -164,7 +164,7 @@ static int dns_encode_name(char* name, dns_name* out){ for(token = strtok_r(name, ".", &save); token; token = strtok_r(NULL, ".", &save)){ //make space for this label, its length and a trailing root label - if(out->alloc < out->length + strlen(token) + 1 + 1){ + if(out->alloc < out->length + strlen(token) + 1 + 1 || !out->name){ out->name = realloc(out->name, (out->length + strlen(token) + 2) * sizeof(char)); if(!out->name){ LOG("Failed to allocate memory"); @@ -319,7 +319,7 @@ static int rtpmidi_announce_addrs(){ } static uint32_t rtpmidi_interval(){ - return max(0, RTPMIDI_SERVICE_INTERVAL - (mm_timestamp() - cfg.last_service)); + return max(0, (int64_t) RTPMIDI_SERVICE_INTERVAL - (int64_t) (mm_timestamp() - cfg.last_service)); } static int rtpmidi_configure(char* option, char* value){ @@ -1526,20 +1526,16 @@ static int rtpmidi_handle_mdns(){ free(name.name); free(host.name); - if(bytes <= 0){ - #ifdef _WIN32 - if(WSAGetLastError() == WSAEWOULDBLOCK){ - #else - if(errno == EAGAIN){ - #endif - return 0; - } - - LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); - return 1; + #ifdef _WIN32 + if(WSAGetLastError() == WSAEWOULDBLOCK){ + #else + if(errno == EAGAIN){ + #endif + return 0; } - return 0; + LOGPF("Error reading from mDNS descriptor: %s", mmbackend_socket_strerror(errno)); + return 1; } static int rtpmidi_handle(size_t num, managed_fd* fds){ -- cgit v1.2.3 From f053dc36fd515e8d7952e1bdefdc0050226a27c4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 19 Apr 2020 21:28:21 +0200 Subject: Fix unchecked returns --- backends/jack.c | 4 +++- backends/rtpmidi.c | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/backends/jack.c b/backends/jack.c index d430c60..c84ed0f 100644 --- a/backends/jack.c +++ b/backends/jack.c @@ -205,7 +205,9 @@ static int mmjack_process(jack_nframes_t nframes, void* instp){ //notify the main thread if(mark){ DBGPF("Notifying handler thread for instance %s", inst->name); - send(data->fd, "c", 1, 0); + if(send(data->fd, "c", 1, 0) != 1){ + DBGPF("Failed to notify main thread on %s", inst->name); + } } return rv; } diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index af90f27..6aef5a3 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -19,15 +19,13 @@ #include #endif -//#include "../tests/hexdump.c" - //TODO learn peer ssrcs //TODO default mode? //TODO internal loop mode //TODO for some reason, the announce packet generates an exception in the wireshark dns dissector //TODO rename and document most functions //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them -//TODO ipv6-mapped-ipv4 creates problens when connecting on a ipv4-bound instance +//TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance static struct /*_rtpmidi_global*/ { int mdns_fd; @@ -496,9 +494,10 @@ static int rtpmidi_push_invite(instance* inst, char* peer){ return 0; } -static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ +static int rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t dest_len, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; uint8_t frame[RTPMIDI_PACKET_BUFFER] = ""; + ssize_t bytes = 0; apple_command* cmd = (apple_command*) &frame; cmd->res1 = 0xFFFF; @@ -511,10 +510,15 @@ static ssize_t rtpmidi_applecommand(instance* inst, struct sockaddr* dest, sockl memcpy(frame + sizeof(apple_command), inst->name, strlen(inst->name) + 1); //FIXME should we match sending/receiving ports? if the reference does this, it should be documented - return sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); + bytes = sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); + if(bytes != sizeof(apple_command) + strlen(inst->name) + 1){ + LOGPF("Failed to transmit session command on %s", inst->name); + return 1; + } + return 0; } -static ssize_t rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ +static int rtpmidi_peer_applecommand(instance* inst, size_t peer, uint8_t control, applemidi_command command, uint32_t token){ rtpmidi_instance_data* data = (rtpmidi_instance_data*) inst->impl; struct sockaddr_storage dest_addr; @@ -875,7 +879,9 @@ static int rtpmidi_handle_applemidi(instance* inst, int fd, uint8_t* frame, size return 0; } - sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len); + if(sendto(fd, response, sizeof(apple_sync_frame), 0, (struct sockaddr*) peer, peer_len) != sizeof(apple_sync_frame)){ + LOG("Failed to output sync frame"); + } return 0; } else if(command->command == apple_feedback){ @@ -1116,6 +1122,7 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ }; //send to ipv4 and ipv6 mcasts + //FIXME much as it pains me, this should probably be split into two descriptors, one for ipv4 and one for ipv6 sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); return 0; @@ -1323,7 +1330,9 @@ static int rtpmidi_service(){ memcpy(&control_peer, &(data->peer[u].dest), sizeof(control_peer)); ((struct sockaddr_in*) &control_peer)->sin_port = htobe16(be16toh(((struct sockaddr_in*) &control_peer)->sin_port) - 1); - sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len); + if(sendto(data->control_fd, (char*) &sync, sizeof(apple_sync_frame), 0, (struct sockaddr*) &control_peer, data->peer[u].dest_len) != sizeof(apple_sync_frame)){ + LOG("Failed to output sync frame"); + } } else if(data->peer[p].active && !data->peer[p].learned && (mm_timestamp() / 1000) % 10 == 0){ //try to invite pre-defined unconnected applemidi peers -- cgit v1.2.3 From 8773fca1f7d2ffed68b6af4967b537d989a1e5b5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 20 Apr 2020 20:38:58 +0200 Subject: Print platform-specific error messages for socket calls --- backends/artnet.c | 15 +++++---------- backends/maweb.c | 2 +- backends/openpixelcontrol.c | 2 +- backends/osc.c | 4 ++-- backends/rtpmidi.c | 15 ++++++++++++++- backends/rtpmidi.md | 4 ++-- backends/sacn.c | 35 ++++++++++++----------------------- 7 files changed, 37 insertions(+), 40 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 585895b..76962b4 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -224,17 +224,12 @@ static int artnet_transmit(instance* inst, artnet_output_universe* output){ memcpy(frame.data, data->data.out, 512); if(sendto(artnet_fd[data->fd_index].fd, (uint8_t*) &frame, sizeof(frame), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output frame for instance %s: %s", inst->name, error); - LocalFree(error); + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } //reschedule frame output @@ -414,7 +409,7 @@ static int artnet_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data: %s", strerror(errno)); + LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ diff --git a/backends/maweb.c b/backends/maweb.c index 6861d75..2804508 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -774,7 +774,7 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ - LOGPF("Failed to receive: %s", strerror(errno)); + LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); //TODO close, reopen return 1; } diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 2a5e01f..1f8d46a 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -534,7 +534,7 @@ static int openpixel_client_handle(instance* inst, int fd){ ssize_t bytes = recv(fd, buffer, sizeof(buffer), 0); if(bytes <= 0){ if(bytes < 0){ - LOGPF("Failed to receive from client: %s", strerror(errno)); + LOGPF("Failed to receive from client: %s", mmbackend_socket_strerror(errno)); } //close the connection diff --git a/backends/osc.c b/backends/osc.c index 72a7b50..8b552b9 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -696,7 +696,7 @@ static int osc_output_channel(instance* inst, size_t channel){ //output packet if(sendto(data->fd, xmit_buf, offset, 0, (struct sockaddr*) &(data->dest), data->dest_len) < 0){ - LOGPF("Failed to transmit packet: %s", strerror(errno)); + LOGPF("Failed to transmit packet: %s", mmbackend_socket_strerror(errno)); } return 0; } @@ -917,7 +917,7 @@ static int osc_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data for instance %s: %s", inst->name, strerror(errno)); + LOGPF("Failed to receive data for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 6aef5a3..52cb0c5 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -27,6 +27,20 @@ //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them //TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance +/* + * CAVEAT EMPTOR: This is one of the largest backends yet, due to the + * sheer number of protocols involved and their respective complexity. + * The following RFCs may be useful for understanding this backend: + * * RFC 6295 (MIDI Payload for RTP) + * * RFC 1035 (DNS) + * * RFC 6762 (mDNS) + * * RFC 6763 (DNS Service Discovery) + * * RFC 2782 (SRV RR for DNS) + * * To a lesser extent, RFC3550 (RTP) + * Additionally, a strong understanding of the MIDI data stream as well as the details of multicast + * networking for IPv4 and IPv6 are very helpful. + */ + static struct /*_rtpmidi_global*/ { int mdns_fd; char* mdns_name; @@ -235,7 +249,6 @@ bail: return -1; } -//TODO this should be trimmed down a bit static int rtpmidi_announce_addrs(){ char repr[INET6_ADDRSTRLEN + 1] = "", iface[2048] = ""; union { diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 2194a05..13fd1f6 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -15,8 +15,8 @@ selectable per-instance, with some methods requiring additional global configura configured in the instance configuration as well as previously unknown peers that voluntarily send data to the instance. * AppleMIDI session management: The instance will be able to communicate (either as participant - or initiator) in an AppleMIDI session, which can optionally be announced via mDNS (better - known as "Bonjour" to Apple users). + or initiator) in an AppleMIDI session, which will be announced via mDNS (better + known as "Bonjour" to Apple users) if possible. Note that instances that receive data from multiple peers will combine all inputs into one stream, which may lead to inconsistencies during playback. diff --git a/backends/sacn.c b/backends/sacn.c index b3376d7..0444949 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -101,7 +101,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ if(flags & mcast_loop){ //set IP_MCAST_LOOP to allow local applications to receive output if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ - LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", strerror(errno)); + LOGPF("Failed to re-enable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } @@ -313,17 +313,12 @@ static int sacn_transmit(instance* inst, sacn_output_universe* output){ memcpy((((uint8_t*)pdu.data.data) + 1), data->data.out, 512); if(sendto(global_cfg.fd[data->fd_index].fd, (uint8_t*) &pdu, sizeof(pdu), 0, (struct sockaddr*) &data->dest_addr, data->dest_len) < 0){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output frame for instance %s: %s", inst->name, strerror(errno)); - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output frame for instance %s: %s", inst->name, error); - LocalFree(error); + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output frame for instance %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } @@ -512,19 +507,13 @@ static void sacn_discovery(size_t fd){ memcpy(pdu.data.data, global_cfg.fd[fd].universe + page * 512, universes * sizeof(uint16_t)); if(sendto(global_cfg.fd[fd].fd, (uint8_t*) &pdu, sizeof(pdu) - (512 - universes) * sizeof(uint16_t), 0, (struct sockaddr*) &discovery_dest, sizeof(discovery_dest)) < 0){ - #ifndef _WIN32 - if(errno != EAGAIN){ - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, strerror(errno)); - } - #else + #ifdef _WIN32 if(WSAGetLastError() != WSAEWOULDBLOCK){ - char* error = NULL; - FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, - NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, error); - LocalFree(error); - } + #else + if(errno != EAGAIN){ #endif + LOGPF("Failed to output universe discovery frame for interface %" PRIsize_t ": %s", fd, mmbackend_socket_strerror(errno)); + } } } } @@ -603,7 +592,7 @@ static int sacn_handle(size_t num, managed_fd* fds){ #else if(bytes_read < 0 && errno != EAGAIN){ #endif - LOGPF("Failed to receive data: %s", strerror(errno)); + LOGPF("Failed to receive data: %s", mmbackend_socket_strerror(errno)); } if(bytes_read == 0){ @@ -655,7 +644,7 @@ static int sacn_start(size_t n, instance** inst){ if(!data->unicast_input){ mcast_req.imr_multiaddr.s_addr = htobe32(((uint32_t) 0xefff0000) | ((uint32_t) data->uni)); if(setsockopt(global_cfg.fd[data->fd_index].fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ - LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, strerror(errno)); + LOGPF("Failed to join Multicast group for universe %u on instance %s: %s", data->uni, inst[u]->name, mmbackend_socket_strerror(errno)); } } -- cgit v1.2.3 From 918fb606174dcf42553be65e3d2306996c52488f Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 21 Apr 2020 00:13:51 +0200 Subject: Implement recursive configuration file parsing (Fixes #59) --- README.md | 4 ++++ config.c | 25 +++++++++++++++++++++++-- midimonster.c | 2 +- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 12d87eb..a7ca212 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,10 @@ lines of the form `option = value`. Lines starting with a semicolon are treated as comments and ignored. Inline comments are not currently supported. +Configuration files may be included recursively in other configuration files using +the syntax `[include ]`. This will read the referenced configuration file as +if it were inserted at that point. + Example configuration files may be found in [configs/](configs/). ### Backend and instance configuration diff --git a/config.c b/config.c index aef4f87..b5e0747 100644 --- a/config.c +++ b/config.c @@ -2,6 +2,11 @@ #include #include #include +#ifndef _WIN32 +#include +#endif + +#define BACKEND_NAME "core/cfg" #include "midimonster.h" #include "config.h" #include "backend.h" @@ -349,6 +354,10 @@ static int config_line(char* line){ } } } + else if(!strncmp(line, "[include ", 9)){ + line[strlen(line) - 1] = 0; + return config_read(line + 9); + } else if(!strcmp(line, "[map]")){ //mapping configuration parser_state = map; @@ -493,7 +502,7 @@ int config_read(char* cfg_filepath){ char* line_raw = NULL; //create heap copy of file name because original might be in readonly memory - char* source_dir = strdup(cfg_filepath), *source_file = NULL; + char* source_dir = strdup(cfg_filepath), *source_file = NULL, original_dir[PATH_MAX * 2] = ""; #ifdef _WIN32 char path_separator = '\\'; #else @@ -510,6 +519,12 @@ int config_read(char* cfg_filepath){ if(source_file){ *source_file = 0; source_file++; + + if(!getcwd(original_dir, sizeof(original_dir))){ + fprintf(stderr, "Failed to read current working directory: %s\n", strerror(errno)); + goto bail; + } + if(chdir(source_dir)){ fprintf(stderr, "Failed to change to configuration file directory %s: %s\n", source_dir, strerror(errno)); goto bail; @@ -519,10 +534,11 @@ int config_read(char* cfg_filepath){ source_file = source_dir; } + fprintf(stderr, "Reading configuration file %s\n", cfg_filepath); source = fopen(source_file, "r"); if(!source){ - fprintf(stderr, "Failed to open configuration file for reading\n"); + fprintf(stderr, "Failed to open %s for reading\n", cfg_filepath); goto bail; } @@ -536,6 +552,11 @@ int config_read(char* cfg_filepath){ rv = 0; bail: + //change back to previous directory to allow recursive configuration file parsing + if(source_file && source_dir != source_file){ + chdir(original_dir); + } + free(source_dir); if(source){ fclose(source); diff --git a/midimonster.c b/midimonster.c index 8100607..1065e5c 100644 --- a/midimonster.c +++ b/midimonster.c @@ -482,7 +482,7 @@ int main(int argc, char** argv){ //read config if(config_read(cfg_file)){ - fprintf(stderr, "Failed to read configuration file %s\n", cfg_file); + fprintf(stderr, "Failed to parse master configuration file %s\n", cfg_file); backends_stop(); routing_cleanup(); fds_free(); -- cgit v1.2.3 From bc275e10defe27e6d288ccf9125fe9b915168240 Mon Sep 17 00:00:00 2001 From: cbdev Date: Tue, 21 Apr 2020 00:20:23 +0200 Subject: Do not load lua backend automatically on Windows --- .travis-ci.sh | 3 +++ .travis.yml | 2 +- backends/Makefile | 2 +- backends/libmmbackend.c | 7 +++++++ backends/lua.md | 5 ++--- backends/rtpmidi.c | 2 +- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index c832f2c..5272fde 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -71,7 +71,9 @@ elif [ "$TASK" = "windows" ]; then if make windows; then exit "$?" fi + # Build the lua backend but disable it by default to avoid scary error messages make -C backends lua.dll + mv backends/lua.dll backends/lua.dll.disabled travis_fold end "make_windows" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then travis_fold start "deploy_windows" @@ -80,6 +82,7 @@ elif [ "$TASK" = "windows" ]; then mkdir ./deployment/docs cp ./midimonster.exe ./deployment/ cp ./backends/*.dll ./deployment/backends/ + cp ./backends/*.dll.disabled ./deployment/backends/ cp ./monster.cfg ./deployment/monster.cfg cp ./backends/*.md ./deployment/docs/ cp -r ./configs ./deployment/ diff --git a/.travis.yml b/.travis.yml index d7c25b6..8cf9e82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -152,7 +152,7 @@ before_install: #OS X uses something other than $CXX variable - if [ "$TRAVIS_OS_NAME" == "linux" -a \( "$TASK" = "compile" -o "$TASK" = "sanitize" \) ]; then $CXX --version; fi # Download libraries to link with on Windows - - if [ "$TASK" == "windows" ]; then mkdir libs; wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; mv lua53.dll libs; fi + - if [ "$TASK" == "windows" ]; then wget "https://downloads.sourceforge.net/project/luabinaries/5.3.5/Windows%20Libraries/Dynamic/lua-5.3.5_Win64_dllw6_lib.zip" -O lua53.zip; unzip lua53.zip lua53.dll; fi notifications: irc: diff --git a/backends/Makefile b/backends/Makefile index 1e66995..700c9b3 100644 --- a/backends/Makefile +++ b/backends/Makefile @@ -65,7 +65,7 @@ ola.so: CPPFLAGS += -Wno-write-strings lua.so: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.so: LDLIBS += $(shell pkg-config --libs lua53 || pkg-config --libs lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") lua.dll: CFLAGS += $(shell pkg-config --cflags lua53 || pkg-config --cflags lua5.3 || echo "-DBUILD_ERROR=\"Missing pkg-config data for lua53\"") -lua.dll: LDLIBS += -L../libs -llua53 +lua.dll: LDLIBS += -L../ -llua53 python.so: CFLAGS += $(shell pkg-config --cflags python3 || pkg-config --cflags python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") python.so: CFLAGS += $(shell pkg-config --libs python3 || pkg-config --libs python || echo "-DBUILD_ERROR=\"Missing pkg-config data for python3\"") diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 2bbc226..92adc3c 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -20,8 +20,15 @@ int mmbackend_strdup(char** dest, char* src){ char* mmbackend_socket_strerror(int err_no){ #ifdef _WIN32 static char error[2048] = ""; + ssize_t u; FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, WSAGetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), error, sizeof(error), NULL); + //remove trailing newline that for some reason is included in most of these... + for(u = strlen(error) - 1; u > 0; u--){ + if(!isprint(error[u])){ + error[u] = 0; + } + } return error; #else return strerror(err_no); diff --git a/backends/lua.md b/backends/lua.md index e59e513..0a31dce 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -101,7 +101,6 @@ the MIDIMonster project can not provide this file within this repository. You will need to acquire a copy of `lua53.dll`, for example by downloading it from the [luabinaries project](http://luabinaries.sourceforge.net/download.html). -To build the `lua` backend for Windows, place `lua53.dll` in a subdirectory `libs/` in the project root -and run `make lua.dll` inside the `backends/` directory. - +Place this file in the project root directory and run `make lua.dll` inside the `backends/` directory +to build the backend. At runtime, Windows searches for the file in the same directory as `midimonster.exe`. diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 52cb0c5..7df8563 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -525,7 +525,7 @@ static int rtpmidi_applecommand(instance* inst, struct sockaddr* dest, socklen_t //FIXME should we match sending/receiving ports? if the reference does this, it should be documented bytes = sendto(control ? data->control_fd : data->fd, frame, sizeof(apple_command) + strlen(inst->name) + 1, 0, dest, dest_len); if(bytes != sizeof(apple_command) + strlen(inst->name) + 1){ - LOGPF("Failed to transmit session command on %s", inst->name); + LOGPF("Failed to transmit session command on %s: %s", inst->name, mmbackend_socket_strerror(errno)); return 1; } return 0; -- cgit v1.2.3 From a8be76f20b3eaa20b53e1cda7686845a08cef32b Mon Sep 17 00:00:00 2001 From: cbdev Date: Thu, 23 Apr 2020 23:38:36 +0200 Subject: Split mDNS socket into v4/v6 --- backends/artnet.c | 2 +- backends/libmmbackend.c | 13 ++++++++---- backends/libmmbackend.h | 2 +- backends/maweb.c | 2 +- backends/openpixelcontrol.c | 4 ++-- backends/osc.c | 2 +- backends/rtpmidi.c | 49 +++++++++++++++++++++++++++------------------ backends/rtpmidi.h | 1 + backends/sacn.c | 2 +- backends/winmidi.c | 2 +- 10 files changed, 47 insertions(+), 32 deletions(-) diff --git a/backends/artnet.c b/backends/artnet.c index 76962b4..e07ea52 100644 --- a/backends/artnet.c +++ b/backends/artnet.c @@ -21,7 +21,7 @@ static int artnet_listener(char* host, char* port){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 92adc3c..186cc66 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -138,7 +138,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad return 0; } -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast){ +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack){ int fd = -1, status, yes = 1; struct addrinfo hints = { .ai_family = AF_UNSPEC, @@ -162,18 +162,23 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin //set required socket options yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to enable SO_REUSEADDR on socket: %s", mmbackend_socket_strerror(errno)); } + yes = dualstack ? 0 : 1; + if(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ + LOGPF("Failed to %s dualstack operations on socket: %s", dualstack ? "enable" : "disable", mmbackend_socket_strerror(errno)); + } + if(mcast){ yes = 1; - if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, SOL_SOCKET, SO_BROADCAST, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to enable SO_BROADCAST on socket: %s", mmbackend_socket_strerror(errno)); } yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*)&yes, sizeof(yes)) < 0){ + if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } diff --git a/backends/libmmbackend.h b/backends/libmmbackend.h index 08f03aa..1f0b4d7 100644 --- a/backends/libmmbackend.h +++ b/backends/libmmbackend.h @@ -72,7 +72,7 @@ int mmbackend_parse_sockaddr(char* host, char* port, struct sockaddr_storage* ad * Create a socket of given type and mode for a bind / connect host. * Returns -1 on failure, a valid file descriptor for the socket on success. */ -int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast); +int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uint8_t mcast, uint8_t dualstack); /* * Send arbitrary data over multiple writes if necessary diff --git a/backends/maweb.c b/backends/maweb.c index 2804508..c2f6311 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -639,7 +639,7 @@ static int maweb_connect(instance* inst){ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); } - data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0); + data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0, 1); if(data->fd < 0){ return 1; } diff --git a/backends/openpixelcontrol.c b/backends/openpixelcontrol.c index 1f8d46a..f2dde23 100644 --- a/backends/openpixelcontrol.c +++ b/backends/openpixelcontrol.c @@ -48,7 +48,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu return 1; } - data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0); + data->dest_fd = mmbackend_socket(host, port, SOCK_STREAM, 0, 0, 1); if(data->dest_fd >= 0){ return 0; } @@ -62,7 +62,7 @@ static int openpixel_configure_instance(instance* inst, char* option, char* valu return 1; } - data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0); + data->listen_fd = mmbackend_socket(host, port, SOCK_STREAM, 1, 0, 1); if(data->listen_fd >= 0 && !listen(data->listen_fd, SOMAXCONN)){ return 0; } diff --git a/backends/osc.c b/backends/osc.c index 8b552b9..5887a50 100644 --- a/backends/osc.c +++ b/backends/osc.c @@ -525,7 +525,7 @@ static int osc_configure_instance(instance* inst, char* option, char* value){ } //this requests a socket with SO_BROADCAST set, whether this is useful functionality for OSC is up for debate - data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + data->fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(data->fd < 0){ LOGPF("Failed to bind for instance %s", inst->name); return 1; diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index 7df8563..eb08e8d 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -//#define DEBUG +#define DEBUG #include #include @@ -25,7 +25,6 @@ //TODO for some reason, the announce packet generates an exception in the wireshark dns dissector //TODO rename and document most functions //TODO timeout non-responsive peers (connected = 0) to allow discovery to reconnect them -//TODO ipv6-mapped-ipv4 creates problems when connecting on a ipv4-bound instance /* * CAVEAT EMPTOR: This is one of the largest backends yet, due to the @@ -42,7 +41,10 @@ */ static struct /*_rtpmidi_global*/ { + //mdns is split into v6 and v4 to avoid having to translate ipv6-mapped-ipv4 source addresses int mdns_fd; + int mdns4_fd; + char* mdns_name; char* mdns_interface; @@ -56,6 +58,7 @@ static struct /*_rtpmidi_global*/ { rtpmidi_invite* invite; } cfg = { .mdns_fd = -1, + .mdns4_fd = -1, .mdns_name = NULL, .mdns_interface = NULL, @@ -370,7 +373,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch char control_port[32]; //bind to random port if none supplied - data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0); + data->fd = mmbackend_socket(host, port ? port : "0", SOCK_DGRAM, 1, 0, 1); if(data->fd < 0){ return 1; } @@ -384,7 +387,7 @@ static int rtpmidi_bind_instance(instance* inst, rtpmidi_instance_data* data, ch if(data->mode == apple){ data->control_port = be16toh(((struct sockaddr_in*) &sock_addr)->sin_port) - 1; snprintf(control_port, sizeof(control_port), "%d", data->control_port); - data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0); + data->control_fd = mmbackend_socket(host, control_port, SOCK_DGRAM, 1, 0, 1); if(data->control_fd < 0){ LOGPF("Failed to bind control port %s for instance %s", control_port, inst->name); return 1; @@ -1135,9 +1138,8 @@ static int rtpmidi_mdns_broadcast(uint8_t* frame, size_t len){ }; //send to ipv4 and ipv6 mcasts - //FIXME much as it pains me, this should probably be split into two descriptors, one for ipv4 and one for ipv6 sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast6, sizeof(mcast6)); - sendto(cfg.mdns_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); + sendto(cfg.mdns4_fd, frame, len, 0, (struct sockaddr*) &mcast, sizeof(mcast)); return 0; } @@ -1329,7 +1331,7 @@ static int rtpmidi_service(){ if(data->mode == apple){ //mdns discovery - if(cfg.mdns_fd >= 0 + if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && (!data->last_announce || mm_timestamp() - data->last_announce > RTPMIDI_ANNOUNCE_INTERVAL)){ rtpmidi_mdns_announce(inst[u]); } @@ -1513,7 +1515,7 @@ static int rtpmidi_parse_announce(uint8_t* buffer, size_t length, dns_header* hd return 0; } -static int rtpmidi_handle_mdns(){ +static int rtpmidi_handle_mdns(int fd){ uint8_t buffer[RTPMIDI_PACKET_BUFFER]; dns_header* hdr = (dns_header*) buffer; dns_name name = { @@ -1523,9 +1525,9 @@ static int rtpmidi_handle_mdns(){ struct sockaddr_storage peer_addr; socklen_t peer_len = sizeof(peer_addr); - for(bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); + for(bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); bytes > 0; - bytes = recvfrom(cfg.mdns_fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ + bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len)){ if(bytes < sizeof(dns_header)){ continue; } @@ -1540,7 +1542,10 @@ static int rtpmidi_handle_mdns(){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - DBGPF("%" PRIsize_t " bytes, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", bytes, hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", hdr->questions, hdr->answers, hdr->servers, hdr->additional); + DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", + bytes, (fd == cfg.mdns_fd ? '6' : '4'), hdr->id, + DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", + hdr->questions, hdr->answers, hdr->servers, hdr->additional); rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); @@ -1578,7 +1583,7 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ for(u = 0; u < num; u++){ if(!fds[u].impl){ //handle mDNS discovery input - rtpmidi_handle_mdns(); + rtpmidi_handle_mdns(fds[u].fd); } else{ //handle rtp/control input @@ -1619,24 +1624,25 @@ static int rtpmidi_start_mdns(){ } //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps - cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1); - if(cfg.mdns_fd < 0){ - LOG("Failed to create requested mDNS descriptor"); + cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); + cfg.mdns4_fd = mmbackend_socket(RTPMIDI_DEFAULT4_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); + if(cfg.mdns_fd < 0 && cfg.mdns4_fd < 0){ + LOG("Failed to create requested mDNS descriptors"); return 1; } //join ipv4 multicast group - if(setsockopt(cfg.mdns_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ + if(cfg.mdns4_fd >= 0 && setsockopt(cfg.mdns4_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, (uint8_t*) &mcast_req, sizeof(mcast_req))){ LOGPF("Failed to join IPv4 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //join ipv6 multicast group - if(setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ + if(cfg.mdns_fd >= 0 && setsockopt(cfg.mdns_fd, IPPROTO_IPV6, IPV6_ADD_MEMBERSHIP, (uint8_t*) &mcast6_req, sizeof(mcast6_req))){ LOGPF("Failed to join IPv6 multicast group for mDNS, discovery may be impaired: %s", mmbackend_socket_strerror(errno)); } //register mdns fd to core - return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL); + return mm_manage_fd(cfg.mdns_fd, BACKEND_NAME, 1, NULL) | mm_manage_fd(cfg.mdns4_fd, BACKEND_NAME, 1, NULL); } static int rtpmidi_start(size_t n, instance** inst){ @@ -1686,7 +1692,7 @@ static int rtpmidi_start(size_t n, instance** inst){ LOG("Failed to set up mDNS discovery, instances may not show up on remote hosts and may not find remote peers"); } else if(mdns_requested){ - fds++; + fds += 2; } LOGPF("Registered %" PRIsize_t " descriptors to core", fds); @@ -1700,7 +1706,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ for(u = 0; u < n; u++){ data = (rtpmidi_instance_data*) inst[u]->impl; - if(cfg.mdns_fd >= 0 && data->mode == apple){ + if((cfg.mdns_fd >= 0 || cfg.mdns4_fd >= 0) && data->mode == apple){ rtpmidi_mdns_detach(inst[u]); } @@ -1743,6 +1749,9 @@ static int rtpmidi_shutdown(size_t n, instance** inst){ if(cfg.mdns_fd >= 0){ close(cfg.mdns_fd); } + if(cfg.mdns4_fd >= 0){ + close(cfg.mdns4_fd); + } LOG("Backend shut down"); return 0; diff --git a/backends/rtpmidi.h b/backends/rtpmidi.h index 9d46911..7e6eccc 100644 --- a/backends/rtpmidi.h +++ b/backends/rtpmidi.h @@ -16,6 +16,7 @@ static int rtpmidi_shutdown(size_t n, instance** inst); #define RTPMIDI_PACKET_BUFFER 8192 #define RTPMIDI_DEFAULT_HOST "::" +#define RTPMIDI_DEFAULT4_HOST "0.0.0.0" #define RTPMIDI_MDNS_PORT "5353" #define RTPMIDI_HEADER_MAGIC 0x80 #define RTPMIDI_HEADER_TYPE 0x61 diff --git a/backends/sacn.c b/backends/sacn.c index 0444949..0c0fd10 100644 --- a/backends/sacn.c +++ b/backends/sacn.c @@ -80,7 +80,7 @@ static int sacn_listener(char* host, char* port, uint8_t flags){ return -1; } - fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1); + fd = mmbackend_socket(host, port, SOCK_DGRAM, 1, 1, 1); if(fd < 0){ return -1; } diff --git a/backends/winmidi.c b/backends/winmidi.c index 753f25c..030062d 100644 --- a/backends/winmidi.c +++ b/backends/winmidi.c @@ -438,7 +438,7 @@ static int winmidi_socket_pair(int* fds){ }; //for some reason the feedback connection fails to work on 'real' windows with ipv6 - fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0); + fds[0] = mmbackend_socket("127.0.0.1", "0", SOCK_DGRAM, 1, 0, 0); if(fds[0] < 0){ LOG("Failed to open feedback socket"); return 1; -- cgit v1.2.3 From ff785c5c5a300d01b404c48335de7f68ad8711a9 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Apr 2020 00:18:36 +0200 Subject: Update lua documentation, keep last value for handler --- backends/lua.c | 3 ++- backends/lua.md | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backends/lua.c b/backends/lua.c index d7f2643..98ce369 100644 --- a/backends/lua.c +++ b/backends/lua.c @@ -517,7 +517,6 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ //handle all incoming events for(n = 0; n < num; n++){ ident = c[n]->ident; - data->channel[ident].in = v[n].normalised; //call lua channel handlers if present if(data->channel[ident].reference != LUA_NOREF){ //push the channel name @@ -532,6 +531,8 @@ static int lua_set(instance* inst, size_t num, channel** c, channel_value* v){ lua_pop(data->interpreter, 1); } } + //update the channel input value after the handler call, so we can use both values there + data->channel[ident].in = v[n].normalised; } //clear the channel name diff --git a/backends/lua.md b/backends/lua.md index 0a31dce..4e58ded 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -3,18 +3,21 @@ The `lua` backend provides a flexible programming environment, allowing users to route, generate and manipulate events using the Lua scripting language. -Every instance has its own interpreter state which can be loaded with custom handler scripts. +Every instance has its own interpreter state which can be loaded with custom scripts. To process incoming channel events, the MIDIMonster calls corresponding Lua functions (if they exist) with the value (as a Lua `number` type) as parameter. Alternatively, a designated default channel handler -may be supplied in the configuration. +which will receive events for all incoming channels may be supplied in the configuration. + +The backend can also call Lua functions repeatedly using a timer, allowing users to implement time-based +functionality (such as evaluating a fixed mathematical function or outputting periodic updates). The following functions are provided within the Lua interpreter for interaction with the MIDIMonster | Function | Usage example | Description | |-------------------------------|-------------------------------|---------------------------------------| | `output(string, number)` | `output("foo", 0.75)` | Output a value event to a channel on this instance | -| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function | +| `interval(function, number)` | `interval(update, 100)` | Register a function to be called periodically. Intervals are milliseconds (rounded to the nearest 10 ms). Calling `interval` on a Lua function multiple times updates the interval. Specifying `0` as interval stops periodic calls to the function. Do not call this function from within a Lua thread. | | `cleanup_handler(function)` | `cleanup_handler(shutdown)` | Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | | `input_value(string)` | `input_value("foo")` | Get the last input value on a channel on this instance | | `output_value(string)` | `output_value("bar")` | Get the last output value on a channel on this instance | @@ -23,18 +26,27 @@ The following functions are provided within the Lua interpreter for interaction | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | +While a channel handler executes, calling `input_value` for that channel returns the previous value. Once +the handler returns, the internal buffer is updated. + Example script: ```lua +-- This function is called when there are incoming events on input channel `bar` +-- It outputs half the input value on the channel `foo` function bar(value) output("foo", value / 2); end +-- This function is registered below to execute every second +-- It toggles output channel `bar` every time it is called by storing the next state in the variable `step` step = 0 function toggle() output("bar", step * 1.0); step = (step + 1) % 2; end +-- This function is registered below to run as a Lua thread +-- It loops infinitely and toggles the output channel `narf` every second function run_show() while(true) do sleep(1000); @@ -44,10 +56,12 @@ function run_show() end end +-- This function is registered below to be called when the MIDIMonster shuts down function save_values() -- Store state to a file, for example end +-- Register the functions interval(toggle, 1000) thread(run_show) cleanup_handler(save_values) -- cgit v1.2.3 From 4a575cd3c466abfccf4516f5e9f6eacd534f4dc1 Mon Sep 17 00:00:00 2001 From: cbdev Date: Fri, 24 Apr 2020 23:28:24 +0200 Subject: Set MCAST_LOOP/IPV6_V6ONLY only on supported sockets, select outbound multicast interfaces properly --- backends/libmmbackend.c | 4 ++-- backends/rtpmidi.c | 44 ++++++++++++++++++++++++++++++++++++++------ 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/backends/libmmbackend.c b/backends/libmmbackend.c index 186cc66..bad048c 100644 --- a/backends/libmmbackend.c +++ b/backends/libmmbackend.c @@ -167,7 +167,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin } yes = dualstack ? 0 : 1; - if(setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ + if(addr_it->ai_family == AF_INET6 && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to %s dualstack operations on socket: %s", dualstack ? "enable" : "disable", mmbackend_socket_strerror(errno)); } @@ -178,7 +178,7 @@ int mmbackend_socket(char* host, char* port, int socktype, uint8_t listener, uin } yes = 0; - if(setsockopt(fd, IPPROTO_IP, IP_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ + if(setsockopt(fd, addr_it->ai_family == AF_INET ? IPPROTO_IP : IPPROTO_IPV6, addr_it->ai_family == AF_INET ? IP_MULTICAST_LOOP : IPV6_MULTICAST_LOOP, (void*) &yes, sizeof(yes)) < 0){ LOGPF("Failed to disable IP_MULTICAST_LOOP on socket: %s", mmbackend_socket_strerror(errno)); } } diff --git a/backends/rtpmidi.c b/backends/rtpmidi.c index eb08e8d..7c5aa69 100644 --- a/backends/rtpmidi.c +++ b/backends/rtpmidi.c @@ -1,5 +1,5 @@ #define BACKEND_NAME "rtpmidi" -#define DEBUG +//#define DEBUG #include #include @@ -15,6 +15,7 @@ #include #else #include +#include #include #include #endif @@ -47,6 +48,10 @@ static struct /*_rtpmidi_global*/ { char* mdns_name; char* mdns_interface; + #ifdef _WIN32 + unsigned mdns_adapter; + unsigned mdns6_adapter; + #endif uint8_t detect; uint64_t last_service; @@ -283,6 +288,13 @@ static int rtpmidi_announce_addrs(){ continue; } + //for exact matches, use exactly this interface for multicasts + if(!strcmp(iface, cfg.mdns_interface)){ + LOGPF("Using interface %s for mDNS discovery", iface); + cfg.mdns_adapter = iter->IfIndex; + cfg.mdns6_adapter = iter->Ipv6IfIndex; + } + for(unicast_addr = (IP_ADAPTER_UNICAST_ADDRESS_LH*) iter->FirstUnicastAddress; unicast_addr; unicast_addr = unicast_addr->Next){ addr.in = unicast_addr->Address.lpSockaddr; #else @@ -1437,7 +1449,7 @@ static int rtpmidi_apple_peermatch(uint8_t* session_raw, struct sockaddr* peer, for(n = 0; n < cfg.invite[u].invites; n++){ if(!strcmp(cfg.invite[u].name[n], "*")){ done = 1; - DBGPF("Peer %.*s implicitly invited on instance %s, converting to explicit invitation", session_name[0], session_name + 1, cfg.invite[u].inst->name); + DBGPF("Peer %s implicitly invited on instance %s, converting to explicit invitation", session_name, cfg.invite[u].inst->name); if(rtpmidi_push_invite(cfg.invite[u].inst, session_name)){ return 1; } @@ -1524,6 +1536,9 @@ static int rtpmidi_handle_mdns(int fd){ ssize_t bytes = 0; struct sockaddr_storage peer_addr; socklen_t peer_len = sizeof(peer_addr); + #ifdef DEBUG + char peer_name[INET6_ADDRSTRLEN + 1]; + #endif for(bytes = recvfrom(fd, buffer, sizeof(buffer), 0, (struct sockaddr*) &peer_addr, &peer_len); bytes > 0; @@ -1542,10 +1557,11 @@ static int rtpmidi_handle_mdns(int fd){ //rfc6762 18.3: opcode != 0 -> ignore //rfc6762 18.11: response code != 0 -> ignore - DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional", + DBGPF("%" PRIsize_t " bytes on v%c, ID %d, Opcode %d, %s, %d questions, %d answers, %d servers, %d additional, src %s", bytes, (fd == cfg.mdns_fd ? '6' : '4'), hdr->id, DNS_OPCODE(hdr->flags[0]), DNS_RESPONSE(hdr->flags[0]) ? "response" : "query", - hdr->questions, hdr->answers, hdr->servers, hdr->additional); + hdr->questions, hdr->answers, hdr->servers, hdr->additional, + mmbackend_sockaddr_ntop((struct sockaddr*) &peer_addr, peer_name, sizeof(peer_name))); rtpmidi_parse_announce(buffer, bytes, hdr, &name, &host, (struct sockaddr*) &peer_addr, peer_len); peer_len = sizeof(peer_addr); @@ -1605,9 +1621,15 @@ static int rtpmidi_handle(size_t num, managed_fd* fds){ } static int rtpmidi_start_mdns(){ + //use ip_mreqn where possible, but that renames the interface member + #ifdef _WIN32 struct ip_mreq mcast_req = { - .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)), - .imr_interface.s_addr = INADDR_ANY + .imr_interface.s_addr = INADDR_ANY, + #else + struct ip_mreqn mcast_req = { + .imr_address.s_addr = INADDR_ANY, + #endif + .imr_multiaddr.s_addr = htobe32(((uint32_t) 0xe00000fb)) }; struct ipv6_mreq mcast6_req = { @@ -1623,6 +1645,16 @@ static int rtpmidi_start_mdns(){ return 0; } + if(cfg.mdns_interface){ + #ifdef _WIN32 + mcast6_req.ipv6mr_interface = cfg.mdns6_adapter; + mcast_req.imr_interface.s_addr = htobe32(cfg.mdns_adapter); + #else + mcast6_req.ipv6mr_interface = if_nametoindex(cfg.mdns_interface); + mcast_req.imr_ifindex = if_nametoindex(cfg.mdns_interface); + #endif + } + //FIXME might try passing NULL as host here to work around possible windows ipv6 handicaps cfg.mdns_fd = mmbackend_socket(RTPMIDI_DEFAULT_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); cfg.mdns4_fd = mmbackend_socket(RTPMIDI_DEFAULT4_HOST, RTPMIDI_MDNS_PORT, SOCK_DGRAM, 1, 1, 0); -- cgit v1.2.3 From ee086b47e5171698ed9c221e704120522f6abb73 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Apr 2020 00:23:57 +0200 Subject: Add rtpmidi to README, add example config --- README.md | 3 +++ backends/rtpmidi.md | 4 ++-- configs/print.lua | 5 +++++ configs/rtpmidi.cfg | 22 ++++++++++++++++++++++ 4 files changed, 32 insertions(+), 2 deletions(-) create mode 100644 configs/print.lua create mode 100644 configs/rtpmidi.cfg diff --git a/README.md b/README.md index a7ca212..2eda183 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Currently, the MIDIMonster supports the following protocols: | ArtNet | Linux, Windows, OSX | Version 4 | [`artnet`](backends/artnet.md) | | Streaming ACN (sACN / E1.31) | Linux, Windows, OSX | | [`sacn`](backends/sacn.md) | | OpenSoundControl (OSC) | Linux, Windows, OSX | | [`osc`](backends/osc.md) | +| RTP-MIDI | Linux, Windows, OSX | AppleMIDI sessions supported | [`rtpmidi`](backends/rtpmidi.md) | | OpenPixelControl | Linux, Windows, OSX | 8 Bit & 16 Bit modes | [`openpixelcontrol`](backends/openpixelcontrol.md) | | evdev input devices | Linux | Virtual output supported | [`evdev`](backends/evdev.md) | | Open Lighting Architecture | Linux, OSX | | [`ola`](backends/ola.md) | @@ -36,6 +37,7 @@ one protocol into channel(s) on any other (or the same) supported protocol, for * Use an OSC app as a simple lighting controller via ArtNet or sACN * Visualize ArtNet data using OSC tools * Control lighting fixtures or DAWs using gamepad controllers, trackballs, etc ([Example configuration](configs/evdev.cfg)) +* Connect a device speaking RTP MIDI (for example, an iPad) to your computer or lighting console ([Example configuration](configs/rtpmidi.cfg)) * Play games, type, or control your mouse using MIDI controllers ([Example configuration](configs/midi-mouse.cfg)) If you encounter a bug or suspect a problem with a protocol implementation, please @@ -145,6 +147,7 @@ special information. These documentation files are located in the `backends/` di * [`winmidi` backend documentation](backends/winmidi.md) * [`artnet` backend documentation](backends/artnet.md) * [`sacn` backend documentation](backends/sacn.md) +* [`rtpmidi` backend documentation](backends/rtpmidi.md) * [`evdev` backend documentation](backends/evdev.md) * [`loopback` backend documentation](backends/loopback.md) * [`ola` backend documentation](backends/ola.md) diff --git a/backends/rtpmidi.md b/backends/rtpmidi.md index 13fd1f6..82548bf 100644 --- a/backends/rtpmidi.md +++ b/backends/rtpmidi.md @@ -27,7 +27,7 @@ stream, which may lead to inconsistencies during playback. |-----------------------|-----------------------|-----------------------|-----------------------| | `detect` | `on` | `off` | Output channel specifications for any events coming in on configured instances to help with configuration. | | `mdns-name` | `computer1` | none | mDNS hostname to announce (`.local`). Apple-mode instances will be announced via mDNS if set. | -| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. | +| `mdns-interface` | `wlan0` | none | Limit addresses announced via mDNS to this interface. On Windows, this is prefix-matched against the user-editable "friendly" interface name. If this name matches an interface exactly, discovery uses exactly this device. | #### Instance configuration @@ -84,7 +84,7 @@ rmidi1.ch0.pitch > rmidi2.ch1.pitch #### Known bugs / problems -This backend is currently still a work in progress, and is not mentioned on the main README yet for that reason. +This backend has been in development for a long time due to its complexity. There may still be bugs hidden in there. Critical feedback and tests across multiple devices are very welcome. The mDNS and DNS-SD implementations in this backend are extremely terse, to the point of violating the diff --git a/configs/print.lua b/configs/print.lua new file mode 100644 index 0000000..c6391d3 --- /dev/null +++ b/configs/print.lua @@ -0,0 +1,5 @@ +-- This function prints the name of the channel it handles and it's value +-- It can be used for a simple debug output with the `default-handler` configuration option +function printchannel(value) + print(input_channel() .. " @ " .. value) +end diff --git a/configs/rtpmidi.cfg b/configs/rtpmidi.cfg new file mode 100644 index 0000000..4128274 --- /dev/null +++ b/configs/rtpmidi.cfg @@ -0,0 +1,22 @@ +; Simple RTP MIDI example configuration + +[backend rtpmidi] +; This causes the backend itself to output channel values +detect = on +; When connecting multiple MIDIMonster hosts via RTP MIDI, set this to something else on each computer +mdns-name = midimonster-host + +[rtpmidi rtp] +mode = apple +; Invite everyone we see on the network +invite = * + +; This instance just sends all incoming events to the `printchannel` function +[lua print] +script = print.lua +default-handler = printchannel + +; Map all notes and CC's coming in to the Lua instance +[map] +rtp.ch{0..15}.cc{0..127} > print.ch{0..15}.cc{0..127} +rtp.ch{0..15}.note{0..127} > print.ch{0..15}.cnote{0..127} -- cgit v1.2.3 From ceefbdb7eddfdccd7b1b6cb452f920382cdae4b0 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Apr 2020 00:33:57 +0200 Subject: Fix spelling --- configs/print.lua | 2 +- configs/rtpmidi.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/configs/print.lua b/configs/print.lua index c6391d3..dc28bb6 100644 --- a/configs/print.lua +++ b/configs/print.lua @@ -1,4 +1,4 @@ --- This function prints the name of the channel it handles and it's value +-- This function prints the name of the channel it handles and its current value -- It can be used for a simple debug output with the `default-handler` configuration option function printchannel(value) print(input_channel() .. " @ " .. value) diff --git a/configs/rtpmidi.cfg b/configs/rtpmidi.cfg index 4128274..3223bd7 100644 --- a/configs/rtpmidi.cfg +++ b/configs/rtpmidi.cfg @@ -1,9 +1,9 @@ ; Simple RTP MIDI example configuration [backend rtpmidi] -; This causes the backend itself to output channel values +; This causes the backend itself to print channel values as they come in detect = on -; When connecting multiple MIDIMonster hosts via RTP MIDI, set this to something else on each computer +; When connecting multiple MIDIMonster hosts via RTP MIDI, set this to something different on each computer mdns-name = midimonster-host [rtpmidi rtp] -- cgit v1.2.3 From 3691faaaf6da4d479c31b0246949be2f4a02b8b5 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sat, 25 Apr 2020 23:33:54 +0200 Subject: Implement maweb fallback connections --- backends/maweb.c | 109 +++++++++++++++++++++++++++++++++++++++++------------- backends/maweb.h | 7 +++- backends/maweb.md | 12 +++--- 3 files changed, 95 insertions(+), 33 deletions(-) diff --git a/backends/maweb.c b/backends/maweb.c index c2f6311..192c69e 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -163,19 +163,29 @@ static int maweb_configure_instance(instance* inst, char* option, char* value){ LOGPF("Invalid host specified for instance %s", inst->name); return 1; } - free(data->host); - data->host = strdup(host); - free(data->port); - data->port = NULL; - if(port){ - data->port = strdup(port); + + data->host = realloc(data->host, (data->hosts + 1) * sizeof(char*)); + data->port = realloc(data->port, (data->hosts + 1) * sizeof(char*)); + + if(!data->host || !data->port){ + LOG("Failed to allocate memory"); + return 1; + } + + data->host[data->hosts] = strdup(host); + data->port[data->hosts] = port ? strdup(port) : NULL; + if(!data->host[data->hosts] || (port && !data->port[data->hosts])){ + LOG("Failed to allocate memory"); + free(data->host[data->hosts]); + free(data->port[data->hosts]); + return 1; } + + data->hosts++; return 0; } else if(!strcmp(option, "user")){ - free(data->user); - data->user = strdup(value); - return 0; + return mmbackend_strdup(&data->user, value); } else if(!strcmp(option, "password")){ #ifndef MAWEB_NO_LIBSSL @@ -629,9 +639,11 @@ static int maweb_handle_message(instance* inst, char* payload, size_t payload_le } static int maweb_connect(instance* inst){ + int rv = 1; maweb_instance_data* data = (maweb_instance_data*) inst->impl; - if(!data->host){ - return 1; + if(!data->host || !data->host[data->next_host]){ + LOGPF("Invalid host configuration on instance %s, host %" PRIsize_t, inst->name, data->next_host + 1); + goto bail; } //unregister old fd from core @@ -639,9 +651,14 @@ static int maweb_connect(instance* inst){ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); } - data->fd = mmbackend_socket(data->host, data->port ? data->port : MAWEB_DEFAULT_PORT, SOCK_STREAM, 0, 0, 1); + LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name); + + data->fd = mmbackend_socket(data->host[data->next_host], + data->port[data->next_host] ? data->port[data->next_host] : MAWEB_DEFAULT_PORT, + SOCK_STREAM, 0, 0, 1); + if(data->fd < 0){ - return 1; + goto bail; } data->state = ws_new; @@ -654,15 +671,20 @@ static int maweb_connect(instance* inst){ || mmbackend_send_str(data->fd, "Sec-WebSocket-Key: rbEQrXMEvCm4ZUjkj6juBQ==\r\n") || mmbackend_send_str(data->fd, "\r\n")){ LOG("Failed to communicate with peer"); - return 1; + goto bail; } //register new fd if(mm_manage_fd(data->fd, BACKEND_NAME, 1, (void*) inst)){ LOG("Failed to register FD"); - return 1; + goto bail; } - return 0; + + rv = 0; +bail: + data->next_host++; + data->next_host %= data->hosts; + return rv; } static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ @@ -693,6 +715,21 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ return data->offset + begin; } +static int maweb_establish(instance* inst){ + maweb_instance_data* data = (maweb_instance_data*) inst->impl; + uint8_t connected = 0; + size_t start = data->next_host; + + do{ + if(!maweb_connect(inst)){ + connected = 1; + break; + } + } while(data->next_host != start); + + return connected ? 0 : 1; +} + static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; size_t header_length = 2; @@ -775,12 +812,18 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); - //TODO close, reopen - return 1; + if(maweb_establish(inst)){ + LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); + return 1; + } + return 0; } else if(bytes_read == 0){ - //client closed connection - //TODO try to reopen + //client closed connection, try to reopen the connection + if(maweb_establish(inst)){ + LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); + return 1; + } return 0; } @@ -1003,8 +1046,13 @@ static int maweb_start(size_t n, instance** inst){ maweb_instance_data* data = NULL; for(u = 0; u < n; u++){ - //sort channels data = (maweb_instance_data*) inst[u]->impl; + if(!data->hosts){ + LOGPF("No hosts configured on instance %s", inst[u]->name); + return 1; + } + + //sort channels qsort(data->channel, data->channels, sizeof(maweb_channel_data), channel_comparator); //re-set channel identifiers @@ -1012,9 +1060,9 @@ static int maweb_start(size_t n, instance** inst){ data->channel[p].chan->ident = p; } - if(maweb_connect(inst[u])){ - LOGPF("Failed to open connection for instance %s", inst[u]->name); - free(inst); + //try to connect to any available host + if(maweb_establish(inst[u])){ + LOGPF("Failed to connect to any host configured on instance %s", inst[u]->name); return 1; } } @@ -1027,15 +1075,26 @@ static int maweb_start(size_t n, instance** inst){ } static int maweb_shutdown(size_t n, instance** inst){ - size_t u; + size_t u, p; maweb_instance_data* data = NULL; for(u = 0; u < n; u++){ data = (maweb_instance_data*) inst[u]->impl; + + for(p = 0; p < data->hosts; p++){ + //one of these might have failed to allocate + if(data->host){ + free(data->host[p]); + } + if(data->port){ + free(data->port[p]); + } + } free(data->host); data->host = NULL; free(data->port); data->port = NULL; + free(data->user); data->user = NULL; free(data->pass); diff --git a/backends/maweb.h b/backends/maweb.h index 80835d9..85ca09d 100644 --- a/backends/maweb.h +++ b/backends/maweb.h @@ -79,8 +79,11 @@ typedef struct /*_maweb_channel*/ { } maweb_channel_data; typedef struct /*_maweb_instance_data*/ { - char* host; - char* port; + size_t next_host; + size_t hosts; + char** host; + char** port; + char* user; char* pass; diff --git a/backends/maweb.md b/backends/maweb.md index eddf1a5..1547919 100644 --- a/backends/maweb.md +++ b/backends/maweb.md @@ -18,17 +18,17 @@ Web Remote. Set a web remote password using the option below the activation sett | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------------------------------| -| `interval` | `100` | `50` | Query interval for input data polling (in msec) | -| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts | +| `interval` | `100` | `50` | Query interval for input data polling (in msec). | +| `quiet` | `1` | `0` | Turn off some warning messages, for use by experts. | #### Instance configuration | Option | Example value | Default value | Description | |---------------|-----------------------|-----------------------|---------------------------------------------------------------| -| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote | -| `user` | `midimonster` | none | User for the remote session (GrandMA2) | -| `password` | `midimonster` | `midimonster` | Password for the remote session | -| `cmdline` | `console` | `remote` | Commandline key handling mode (see below) | +| `host` | `10.23.42.21 80` | none | Host address (and optional port) of the MA Web Remote. When specified multiple times, the instance will connect the next address when the current connection fails. | +| `user` | `midimonster` | none | User for the remote session (GrandMA2). | +| `password` | `midimonster` | `midimonster` | Password for the remote session. | +| `cmdline` | `console` | `remote` | Commandline key handling mode (see below). | The per-instance command line mode may be one of `remote`, `console` or `downgrade`. The first option handles command keys with a "virtual" commandline belonging to the Web Remote connection. Any commands entered are -- cgit v1.2.3 From 7693ed34ab5e549b8b7fb8ca395bbf15616d6915 Mon Sep 17 00:00:00 2001 From: Spacelord Date: Sun, 26 Apr 2020 02:28:11 +0200 Subject: Update layering config to use the new default-handler functionality --- configs/layering.cfg | 5 ++-- configs/layering.lua | 82 +++++++--------------------------------------------- 2 files changed, 14 insertions(+), 73 deletions(-) diff --git a/configs/layering.cfg b/configs/layering.cfg index 7adcd6f..b71f9e5 100644 --- a/configs/layering.cfg +++ b/configs/layering.cfg @@ -7,7 +7,7 @@ bind = 0.0.0.0 [midi in] -read = Launch Control +read = BCF [artnet out] destination = 255.255.255.255 @@ -15,9 +15,10 @@ universe = 1 [lua layers] script = layering.lua +default-handler = handler [map] -in.ch0.cc{0..15} > layers.in{0..15} +in.ch1.cc{1..8} > layers.in{0..7} layers.out{0..511} > out.{1..512} in.ch0.note0 > layers.control diff --git a/configs/layering.lua b/configs/layering.lua index 5d9458d..5ddde65 100644 --- a/configs/layering.lua +++ b/configs/layering.lua @@ -3,74 +3,14 @@ -- so we have 32 possible offsets (32 * 16 = 512) current_layer = 0 --- Set the current_layer based on the control input channel -function control(value) - current_layer = math.floor(value * 31.99); -end - --- Handler functions for the input channels --- Calculate the channel offset and just output the value the input channel provides -function in0(value) - output("out"..((current_layer * 16)), value) - print("Output on out"..(current_layer * 16)) -end - -function in1(value) - output("out"..((current_layer * 16) + 1), value) -end - -function in2(value) - output("out"..((current_layer * 16) + 2), value) -end - -function in3(value) - output("out"..((current_layer * 16) + 3), value) -end - -function in4(value) - output("out"..((current_layer * 16) + 4), value) -end - -function in5(value) - output("out"..((current_layer * 16) + 5), value) -end - -function in6(value) - output("out"..((current_layer * 16) + 6), value) -end - -function in7(value) - output("out"..((current_layer * 16) + 7), value) -end - -function in8(value) - output("out"..((current_layer * 16) + 8), value) -end - -function in9(value) - output("out"..((current_layer * 16) + 9), value) -end - -function in10(value) - output("out"..((current_layer * 16) + 10), value) -end - -function in11(value) - output("out"..((current_layer * 16) + 11), value) -end - -function in12(value) - output("out"..((current_layer * 16) + 12), value) -end - -function in13(value) - output("out"..((current_layer * 16) + 13), value) -end - -function in14(value) - output("out"..((current_layer * 16) + 14), value) -end - -function in15(value) - output("out"..((current_layer * 16) + 15), value) -end +function handler(value) + if(input_channel() == "control") then + -- Set the current_layer based on the control input channel + current_layer = math.floor(value * 31.99); + else + -- Handler functions for the input channels + -- Calculate the channel offset and just output the value the input channel provides + output("out"..((current_layer * 16) + tonumber(input_channel())), value) + print("Output on out"..(current_layer * 16)) + end +end \ No newline at end of file -- cgit v1.2.3 From 5f2808188b71c6eea7440c70321ccfe7b3ccdd55 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 26 Apr 2020 02:31:59 +0200 Subject: Fix channel name for layering config --- DEVELOPMENT.md | 2 ++ configs/layering.cfg | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 495c4f9..79005a9 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -34,6 +34,8 @@ in spirit rather than by the letter. * Prefer to name unsigned loop counters `u` and signed ones `i` * Place comments above the section they are commenting on * Use inline comments sparingly +* Do not omit '{}' brackets, even if optional (e.g. single-statement conditional bodies) +* Opening braces stay on the same line as the condition #### C specific diff --git a/configs/layering.cfg b/configs/layering.cfg index b71f9e5..f312a65 100644 --- a/configs/layering.cfg +++ b/configs/layering.cfg @@ -7,7 +7,7 @@ bind = 0.0.0.0 [midi in] -read = BCF +read = Launch Control [artnet out] destination = 255.255.255.255 @@ -18,7 +18,7 @@ script = layering.lua default-handler = handler [map] -in.ch1.cc{1..8} > layers.in{0..7} +in.ch1.cc{0..15} > layers.{0..15} layers.out{0..511} > out.{1..512} in.ch0.note0 > layers.control -- cgit v1.2.3 From 6503d83dee9c2c7dd30572b84f11b95f832e9309 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 26 Apr 2020 21:50:00 +0200 Subject: Disable stop-the-world windows error messages --- .travis-ci.sh | 1 - midimonster.c | 3 +++ plugin.c | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 5272fde..39652a1 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -73,7 +73,6 @@ elif [ "$TASK" = "windows" ]; then fi # Build the lua backend but disable it by default to avoid scary error messages make -C backends lua.dll - mv backends/lua.dll backends/lua.dll.disabled travis_fold end "make_windows" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then travis_fold start "deploy_windows" diff --git a/midimonster.c b/midimonster.c index 1065e5c..b418711 100644 --- a/midimonster.c +++ b/midimonster.c @@ -284,6 +284,9 @@ static int platform_initialize(){ if(WSAStartup(version, &wsa)){ return 1; } + + unsigned error_mode = SetErrorMode(0); + SetErrorMode(error_mode | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX); #endif return 0; } diff --git a/plugin.c b/plugin.c index 56d6651..e7d8eba 100644 --- a/plugin.c +++ b/plugin.c @@ -51,7 +51,7 @@ static int plugin_attach(char* path, char* file){ char* error = NULL; FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &error, 0, NULL); - fprintf(stderr, "Failed to load plugin %s: %s\n", lib, error); + fprintf(stderr, "Failed to load plugin %s, check that all supporting libraries are present: %s\n", lib, error); LocalFree(error); #else fprintf(stderr, "Failed to load plugin %s: %s\n", lib, dlerror()); -- cgit v1.2.3 From 929026130c7866d9b70be7a6cc820f103ae241b4 Mon Sep 17 00:00:00 2001 From: cbdev Date: Sun, 26 Apr 2020 22:54:17 +0200 Subject: Periodically retry connecting remotes for maweb --- .travis-ci.sh | 1 - backends/maweb.c | 50 ++++++++++++++++++++++++++++++++------------------ 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 39652a1..40beec6 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -71,7 +71,6 @@ elif [ "$TASK" = "windows" ]; then if make windows; then exit "$?" fi - # Build the lua backend but disable it by default to avoid scary error messages make -C backends lua.dll travis_fold end "make_windows" if [ "$(git describe)" == "$(git describe --abbrev=0)" ]; then diff --git a/backends/maweb.c b/backends/maweb.c index 192c69e..5242f36 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -232,6 +232,7 @@ static int maweb_instance(instance* inst){ } data->fd = -1; + data->state = ws_closed; data->buffer = calloc(MAWEB_RECV_CHUNK, sizeof(uint8_t)); if(!data->buffer){ LOG("Failed to allocate memory"); @@ -350,6 +351,9 @@ static int maweb_send_frame(instance* inst, maweb_operation op, uint8_t* payload if(mmbackend_send(data->fd, frame_header, header_bytes) || mmbackend_send(data->fd, payload, len)){ + LOGPF("Failed to send on instance %s, assuming connection failure", inst->name); + data->state = ws_closed; + data->login = 0; return 1; } @@ -649,7 +653,11 @@ static int maweb_connect(instance* inst){ //unregister old fd from core if(data->fd >= 0){ mm_manage_fd(data->fd, BACKEND_NAME, 0, NULL); + close(data->fd); + data->fd = -1; } + data->state = ws_closed; + data->login = 0; LOGPF("Connecting to host %" PRIsize_t " of %" PRIsize_t " on %s", data->next_host + 1, data->hosts, inst->name); @@ -717,17 +725,15 @@ static ssize_t maweb_handle_lines(instance* inst, ssize_t bytes_read){ static int maweb_establish(instance* inst){ maweb_instance_data* data = (maweb_instance_data*) inst->impl; - uint8_t connected = 0; size_t start = data->next_host; do{ if(!maweb_connect(inst)){ - connected = 1; break; } } while(data->next_host != start); - return connected ? 0 : 1; + return data->state != ws_closed ? 0 : 1; } static ssize_t maweb_handle_ws(instance* inst, ssize_t bytes_read){ @@ -803,7 +809,7 @@ static int maweb_handle_fd(instance* inst){ data->buffer = realloc(data->buffer, (data->allocated + MAWEB_RECV_CHUNK) * sizeof(uint8_t)); if(!data->buffer){ LOG("Failed to allocate memory"); - return 1; + return -1; } data->allocated += MAWEB_RECV_CHUNK; bytes_left += MAWEB_RECV_CHUNK; @@ -812,19 +818,11 @@ static int maweb_handle_fd(instance* inst){ bytes_read = recv(data->fd, data->buffer + data->offset, bytes_left - 1, 0); if(bytes_read < 0){ LOGPF("Failed to receive on %s: %s", inst->name, mmbackend_socket_strerror(errno)); - if(maweb_establish(inst)){ - LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); - return 1; - } - return 0; + return 1; } else if(bytes_read == 0){ //client closed connection, try to reopen the connection - if(maweb_establish(inst)){ - LOGPF("Failed to reconnect with any configured host on instance %s", inst->name); - return 1; - } - return 0; + return 1; } do{ @@ -844,7 +842,6 @@ static int maweb_handle_fd(instance* inst){ if(bytes_handled < 0){ bytes_handled = data->offset + bytes_read; data->offset = 0; - //TODO close, reopen LOG("Failed to handle incoming data"); return 1; } @@ -990,6 +987,12 @@ static int maweb_keepalive(){ snprintf(xmit_buffer, sizeof(xmit_buffer), "{\"session\":%" PRIu64 "}", data->session); maweb_send_frame(inst[u], ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); } + else if(data->state == ws_closed){ + //try to reconnect to any remote + if(maweb_establish(inst[u])){ + LOGPF("Failed to reconnect to any host on %s, will retry in %d seconds", inst[u]->name, MAWEB_CONNECTION_KEEPALIVE / 1000); + } + } } free(inst); @@ -1024,7 +1027,18 @@ static int maweb_handle(size_t num, managed_fd* fds){ int rv = 0; for(n = 0; n < num; n++){ - rv |= maweb_handle_fd((instance*) fds[n].impl); + rv = maweb_handle_fd((instance*) fds[n].impl); + //try to reconnect soft failures + if(rv == 1 && maweb_establish((instance*) fds[n].impl)){ + //keepalive will retry periodically + LOGPF("Failed to reconnect with any configured host on instance %s", ((instance*) fds[n].impl)->name); + } + else if(rv){ + //propagate critical failures + return rv; + } + //errors handled + rv = 0; } //FIXME all keepalive processing allocates temporary buffers, this might an optimization target @@ -1062,8 +1076,8 @@ static int maweb_start(size_t n, instance** inst){ //try to connect to any available host if(maweb_establish(inst[u])){ + //do not return failure here, keepalive will periodically try to reconnect LOGPF("Failed to connect to any host configured on instance %s", inst[u]->name); - return 1; } } @@ -1107,7 +1121,7 @@ static int maweb_shutdown(size_t n, instance** inst){ data->buffer = NULL; data->offset = data->allocated = 0; - data->state = ws_new; + data->state = ws_closed; free(data->channel); data->channel = NULL; -- cgit v1.2.3 From f692715444c6ddeb47bf87b53acf46798785290a Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 27 Apr 2020 20:54:02 +0200 Subject: Allow access to previous value in python handlers --- backends/lua.md | 4 ++-- backends/maweb.c | 2 +- backends/python.c | 9 +++++---- backends/python.md | 3 +++ 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/backends/lua.md b/backends/lua.md index 4e58ded..b2f40e0 100644 --- a/backends/lua.md +++ b/backends/lua.md @@ -26,8 +26,8 @@ The following functions are provided within the Lua interpreter for interaction | `thread(function)` | `thread(run_show)` | Run a function as a Lua thread (see below) | | `sleep(number)` | `sleep(100)` | Suspend current thread for time specified in milliseconds | -While a channel handler executes, calling `input_value` for that channel returns the previous value. Once -the handler returns, the internal buffer is updated. +While a channel handler executes, calling `input_value` for that channel returns the previous value. +The stored value is updated once the handler returns. Example script: ```lua diff --git a/backends/maweb.c b/backends/maweb.c index 5242f36..97d4cea 100644 --- a/backends/maweb.c +++ b/backends/maweb.c @@ -570,7 +570,7 @@ static int maweb_request_playbacks(instance* inst){ item_types, view, data->session); - rv |= maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); + maweb_send_frame(inst, ws_text, (uint8_t*) xmit_buffer, strlen(xmit_buffer)); DBGPF("Poll request: %s", xmit_buffer); updates_inflight++; } diff --git a/backends/python.c b/backends/python.c index 28b95a9..bd73a20 100644 --- a/backends/python.c +++ b/backends/python.c @@ -559,18 +559,19 @@ static int python_set(instance* inst, size_t num, channel** c, channel_value* v) for(u = 0; u < num; u++){ chan = data->channel + c[u]->ident; - //update input value buffer - chan->in = v[u].normalised; - //call handler if present if(chan->handler){ DBGPF("Calling handler for %s.%s", inst->name, chan->name); data->current_channel = chan; - result = PyObject_CallFunction(chan->handler, "d", chan->in); + result = PyObject_CallFunction(chan->handler, "d", v[u].normalised); Py_XDECREF(result); data->current_channel = NULL; DBGPF("Done with handler for %s.%s", inst->name, chan->name); } + + //update input value buffer after finishing the handler + chan->in = v[u].normalised; + } //release interpreter diff --git a/backends/python.md b/backends/python.md index ab0fb38..a78d972 100644 --- a/backends/python.md +++ b/backends/python.md @@ -26,6 +26,9 @@ The `midimonster` module provides the following functions: | `manage(function, socket)` | `midimonster.manage(handler, socket)` | Register a (connected/listening) socket to the MIDIMonster core. Calls `function(socket)` when the socket is ready to read. Calling this method with `None` as the function argument unregisters the socket. A socket may only have one associated handler | | `cleanup_handler(function)` | `midimonster.cleanup_handler(save_all)`| Register a function to be called when the instance is destroyed (on MIDIMonster shutdown). One cleanup handler can be registered per instance. Calling this function when the instance already has a cleanup handler registered replaces the handler, returning the old one. | +When a channel handler executes, calling `midimonster.inputvalue()` for that exact channel returns the previous value, +while the argument to the handler is the current value. The stored value is updated after the handler finishes executing. + Example Python module: ```python import socket -- cgit v1.2.3 From 30d7adab904dfb5350ba57f1cc0e2de49a740c01 Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 27 Apr 2020 21:33:10 +0200 Subject: Fix OSX compilation --- config.c | 2 +- portability.h | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/config.c b/config.c index b5e0747..11e8a70 100644 --- a/config.c +++ b/config.c @@ -3,7 +3,7 @@ #include #include #ifndef _WIN32 -#include +#include #endif #define BACKEND_NAME "core/cfg" diff --git a/portability.h b/portability.h index f0bfd07..c249a10 100644 --- a/portability.h +++ b/portability.h @@ -18,6 +18,10 @@ #define htole64(x) OSSwapHostToLittleInt64(x) #define be64toh(x) OSSwapBigToHostInt64(x) #define le64toh(x) OSSwapLittleToHostInt64(x) + + #ifndef IPV6_ADD_MEMBERSHIP + #define IPV6_ADD_MEMBERSHIP IPV6_JOIN_GROUP + #endif #endif #ifdef _WIN32 -- cgit v1.2.3 From c0bb55ff08faf2f89af947090d1c9bc412927d9f Mon Sep 17 00:00:00 2001 From: cbdev Date: Mon, 27 Apr 2020 22:20:19 +0200 Subject: Fix CI script --- .travis-ci.sh | 6 +++--- .travis.yml | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis-ci.sh b/.travis-ci.sh index 40beec6..8f6a5ca 100644 --- a/.travis-ci.sh +++ b/.travis-ci.sh @@ -62,13 +62,13 @@ elif [ "$TASK" = "codesmell" ]; then elif [ "$TASK" = "sanitize" ]; then # Run sanitized compile travis_fold start "make_sanitize" - if make sanitize; then + if ! make sanitize; then exit "$?" fi travis_fold end "make_sanitize" elif [ "$TASK" = "windows" ]; then travis_fold start "make_windows" - if make windows; then + if ! make windows; then exit "$?" fi make -C backends lua.dll @@ -92,7 +92,7 @@ elif [ "$TASK" = "windows" ]; then else # Otherwise compile as normal travis_fold start "make" - if make full; then + if ! make full; then exit "$?" fi travis_fold end "make" diff --git a/.travis.yml b/.travis.yml index 8cf9e82..4e14339 100644 --- a/.travis.yml +++ b/.travis.yml @@ -134,6 +134,7 @@ before_cache: before_install: # Travis clones with --branch, which omits tags. Since we use them for the version string at build time, fetch them - git pull --tags + - printf "This is %s on %s\n" "$(git describe)" "$TRAVIS_OS_NAME" - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update; fi # 'brew install' sometimes returns non-zero for some arcane reason. Executing 'true' resets the exit code and allows Travis to continue building... # Travis seems to have Python 2.7 installed by default, which for some reason prevents pkg-config from reading python3.pc -- cgit v1.2.3