/* * Copyright 2002-2015, Haiku, Inc. All rights reserved. * Copyright 2002-2004, Matthijs Hollemans * Copyright 2021, Panagiotis "Ivory" Vasilopoulos * Distributed under the terms of the MIT License. * * Authors: * Humdinger * Matthijs Hollemans * Oliver Tappe * Panagiotis "Ivory" Vasilopoulos * Philippe Houdoin */ #include "MidiServerApp.h" #include #include #include #include #include #include "debug.h" #include "protocol.h" #include "PortDrivers.h" #include "ServerDefs.h" using std::nothrow; #undef B_TRANSLATION_CONTEXT #define B_TRANSLATION_CONTEXT "midi_server" MidiServerApp::MidiServerApp(status_t& error) : BServer(MIDI_SERVER_SIGNATURE, true, &error) { TRACE(("Running Haiku MIDI server")) fNextID = 1; fDeviceWatcher = new(std::nothrow) DeviceWatcher(); if (fDeviceWatcher != NULL) fDeviceWatcher->Run(); } MidiServerApp::~MidiServerApp() { if (fDeviceWatcher && fDeviceWatcher->Lock()) fDeviceWatcher->Quit(); for (int32 t = 0; t < _CountApps(); ++t) { delete _AppAt(t); } for (int32 t = 0; t < _CountEndpoints(); ++t) { delete _EndpointAt(t); } } void MidiServerApp::AboutRequested() { BAboutWindow* window = new BAboutWindow(B_TRANSLATE_SYSTEM_NAME( "Haiku MIDI Server"), MIDI_SERVER_SIGNATURE); window->AddDescription(B_TRANSLATE( "Notes disguised as bytes\n" "propagating to endpoints-\n" "An aural delight.")); const char* extraCopyrights[] = { "2002-2004 Matthijs Hollemans", "2021 Panagiotis \"Ivory\" Vasilopoulos", NULL }; const char* authors[] = { "Humdinger", "Matthijs Hollemans", "Oliver Tappe", "Panagiotis \"Ivory\" Vasilopoulos", "Philippe Houdoin", NULL }; window->AddCopyright(2021, "Haiku, Inc.", extraCopyrights); window->AddAuthors(authors); window->Show(); } void MidiServerApp::MessageReceived(BMessage* msg) { #ifdef DEBUG printf("IN "); msg->PrintToStream(); #endif switch (msg->what) { case MSG_REGISTER_APP: _OnRegisterApp(msg); break; case MSG_CREATE_ENDPOINT: _OnCreateEndpoint(msg); break; case MSG_DELETE_ENDPOINT: _OnDeleteEndpoint(msg); break; case MSG_PURGE_ENDPOINT: _OnPurgeEndpoint(msg); break; case MSG_CHANGE_ENDPOINT: _OnChangeEndpoint(msg); break; case MSG_CONNECT_ENDPOINTS: _OnConnectDisconnect(msg); break; case MSG_DISCONNECT_ENDPOINTS: _OnConnectDisconnect(msg); break; default: super::MessageReceived(msg); break; } } void MidiServerApp::_OnRegisterApp(BMessage* msg) { TRACE(("MidiServerApp::_OnRegisterApp")) // We only send the "app registered" message upon success, // so if anything goes wrong here, we do not let the app // know about it, and we consider it unregistered. (Most // likely, the app is dead. If not, it freezes forever // in anticipation of a message that will never arrive.) app_t* app = new app_t; if (msg->FindMessenger("midi:messenger", &app->messenger) == B_OK && _SendAllEndpoints(app) && _SendAllConnections(app)) { BMessage reply; reply.what = MSG_APP_REGISTERED; if (_SendNotification(app, &reply)) { fApps.AddItem(app); #ifdef DEBUG _DumpApps(); #endif return; } } delete app; } void MidiServerApp::_OnCreateEndpoint(BMessage* msg) { TRACE(("MidiServerApp::_OnCreateEndpoint")) status_t status; endpoint_t* endpoint = new endpoint_t; endpoint->app = _WhichApp(msg); if (endpoint->app == NULL) { status = B_ERROR; } else { status = B_BAD_VALUE; if (msg->FindBool("midi:consumer", &endpoint->consumer) == B_OK && msg->FindBool("midi:registered", &endpoint->registered) == B_OK && msg->FindString("midi:name", &endpoint->name) == B_OK && msg->FindMessage("midi:properties", &endpoint->properties) == B_OK) { if (endpoint->consumer) { if (msg->FindInt32("midi:port", &endpoint->port) == B_OK && msg->FindInt64("midi:latency", &endpoint->latency) == B_OK) status = B_OK; } else status = B_OK; } } BMessage reply; if (status == B_OK) { endpoint->id = fNextID++; reply.AddInt32("midi:id", endpoint->id); } reply.AddInt32("midi:result", status); if (_SendReply(endpoint->app, msg, &reply) && status == B_OK) _AddEndpoint(msg, endpoint); else delete endpoint; } void MidiServerApp::_OnDeleteEndpoint(BMessage* msg) { TRACE(("MidiServerApp::_OnDeleteEndpoint")) // Clients send the "delete endpoint" message from // the BMidiEndpoint destructor, so there is no point // sending a reply, because the endpoint object will // be destroyed no matter what. app_t* app = _WhichApp(msg); if (app != NULL) { endpoint_t* endpoint = _WhichEndpoint(msg, app); if (endpoint != NULL) _RemoveEndpoint(app, endpoint); } } void MidiServerApp::_OnPurgeEndpoint(BMessage* msg) { TRACE(("MidiServerApp::_OnPurgeEndpoint")) // This performs the same task as OnDeleteEndpoint(), // except that this message was send by the midi_server // itself, so we don't check that the app that made the // request really is the owner of the endpoint. (But we // _do_ check that the message came from the server.) if (!msg->IsSourceRemote()) { int32 id; if (msg->FindInt32("midi:id", &id) == B_OK) { endpoint_t* endpoint = _FindEndpoint(id); if (endpoint != NULL) _RemoveEndpoint(NULL, endpoint); } } } void MidiServerApp::_OnChangeEndpoint(BMessage* msg) { TRACE(("MidiServerApp::_OnChangeEndpoint")) endpoint_t* endpoint = NULL; status_t status; app_t* app = _WhichApp(msg); if (app == NULL) status = B_ERROR; else { endpoint = _WhichEndpoint(msg, app); if (endpoint == NULL) status = B_BAD_VALUE; else status = B_OK; } BMessage reply; reply.AddInt32("midi:result", status); if (_SendReply(app, msg, &reply) && status == B_OK) { TRACE(("Endpoint %" B_PRId32 " (%p) changed", endpoint->id, endpoint)) BMessage notify; notify.what = MSG_ENDPOINT_CHANGED; notify.AddInt32("midi:id", endpoint->id); bool registered; if (msg->FindBool("midi:registered", ®istered) == B_OK) { notify.AddBool("midi:registered", registered); endpoint->registered = registered; } BString name; if (msg->FindString("midi:name", &name) == B_OK) { notify.AddString("midi:name", name); endpoint->name = name; } BMessage properties; if (msg->FindMessage("midi:properties", &properties) == B_OK) { notify.AddMessage("midi:properties", &properties); endpoint->properties = properties; } bigtime_t latency; if (msg->FindInt64("midi:latency", &latency) == B_OK) { notify.AddInt64("midi:latency", latency); endpoint->latency = latency; } _NotifyAll(¬ify, app); #ifdef DEBUG _DumpEndpoints(); #endif } } void MidiServerApp::_OnConnectDisconnect(BMessage* msg) { TRACE(("MidiServerApp::_OnConnectDisconnect")) bool mustConnect = msg->what == MSG_CONNECT_ENDPOINTS; status_t status; endpoint_t* producer = NULL; endpoint_t* consumer = NULL; app_t* app = _WhichApp(msg); if (app == NULL) status = B_ERROR; else { status = B_BAD_VALUE; int32 producerID; int32 consumerID; if (msg->FindInt32("midi:producer", &producerID) == B_OK && msg->FindInt32("midi:consumer", &consumerID) == B_OK) { producer = _FindEndpoint(producerID); consumer = _FindEndpoint(consumerID); if (producer != NULL && !producer->consumer) { if (consumer != NULL && consumer->consumer) { // It is an error to connect two endpoints that // are already connected, or to disconnect two // endpoints that are not connected at all. if (mustConnect == producer->connections.HasItem(consumer)) status = B_ERROR; else status = B_OK; } } } } BMessage reply; reply.AddInt32("midi:result", status); if (_SendReply(app, msg, &reply) && status == B_OK) { if (mustConnect) { TRACE(("Connection made: %" B_PRId32 " ---> %" B_PRId32, producer->id, consumer->id)) producer->connections.AddItem(consumer); } else { TRACE(("Connection broken: %" B_PRId32 " -X-> %" B_PRId32, producer->id, consumer->id)) producer->connections.RemoveItem(consumer); } BMessage notify; _MakeConnectedNotification(¬ify, producer, consumer, mustConnect); _NotifyAll(¬ify, app); #ifdef DEBUG _DumpEndpoints(); #endif } } /*! Sends an app MSG_ENDPOINT_CREATED notifications for all current endpoints. Used when the app registers. */ bool MidiServerApp::_SendAllEndpoints(app_t* app) { ASSERT(app != NULL) BMessage notify; for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* endpoint = _EndpointAt(t); _MakeCreatedNotification(¬ify, endpoint); if (!_SendNotification(app, ¬ify)) return false; } return true; } /*! Sends an app MSG_ENDPOINTS_CONNECTED notifications for all current connections. Used when the app registers. */ bool MidiServerApp::_SendAllConnections(app_t* app) { ASSERT(app != NULL) BMessage notify; for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* producer = _EndpointAt(t); if (!producer->consumer) { for (int32 k = 0; k < _CountConnections(producer); ++k) { endpoint_t* consumer = _ConnectionAt(producer, k); _MakeConnectedNotification(¬ify, producer, consumer, true); if (!_SendNotification(app, ¬ify)) return false; } } } return true; } /*! Adds the specified endpoint to the roster, and notifies all other applications about this event. */ void MidiServerApp::_AddEndpoint(BMessage* msg, endpoint_t* endpoint) { ASSERT(msg != NULL) ASSERT(endpoint != NULL) ASSERT(!fEndpoints.HasItem(endpoint)) TRACE(("Endpoint %" B_PRId32 " (%p) added", endpoint->id, endpoint)) fEndpoints.AddItem(endpoint); BMessage notify; _MakeCreatedNotification(¬ify, endpoint); _NotifyAll(¬ify, endpoint->app); #ifdef DEBUG _DumpEndpoints(); #endif } /*! Removes an endpoint from the roster, and notifies all other apps about this event. "app" is the application that the endpoint belongs to; if it is NULL, the app no longer exists and we're purging the endpoint. */ void MidiServerApp::_RemoveEndpoint(app_t* app, endpoint_t* endpoint) { ASSERT(endpoint != NULL) ASSERT(fEndpoints.HasItem(endpoint)) TRACE(("Endpoint %" B_PRId32 " (%p) removed", endpoint->id, endpoint)) fEndpoints.RemoveItem(endpoint); if (endpoint->consumer) _DisconnectDeadConsumer(endpoint); BMessage notify; notify.what = MSG_ENDPOINT_DELETED; notify.AddInt32("midi:id", endpoint->id); _NotifyAll(¬ify, app); delete endpoint; #ifdef DEBUG _DumpEndpoints(); #endif } /*! Removes a consumer from the list of connections of all the producers it is connected to, just before we remove it from the roster. */ void MidiServerApp::_DisconnectDeadConsumer(endpoint_t* consumer) { ASSERT(consumer != NULL) ASSERT(consumer->consumer) for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* producer = _EndpointAt(t); if (!producer->consumer) producer->connections.RemoveItem(consumer); } } //! Fills up a MSG_ENDPOINT_CREATED message. void MidiServerApp::_MakeCreatedNotification(BMessage* msg, endpoint_t* endpoint) { ASSERT(msg != NULL) ASSERT(endpoint != NULL) msg->MakeEmpty(); msg->what = MSG_ENDPOINT_CREATED; msg->AddInt32("midi:id", endpoint->id); msg->AddBool("midi:consumer", endpoint->consumer); msg->AddBool("midi:registered", endpoint->registered); msg->AddString("midi:name", endpoint->name); msg->AddMessage("midi:properties", &endpoint->properties); if (endpoint->consumer) { msg->AddInt32("midi:port", endpoint->port); msg->AddInt64("midi:latency", endpoint->latency); } } //! Fills up a MSG_ENDPOINTS_(DIS)CONNECTED message. void MidiServerApp::_MakeConnectedNotification(BMessage* msg, endpoint_t* producer, endpoint_t* consumer, bool mustConnect) { ASSERT(msg != NULL) ASSERT(producer != NULL) ASSERT(consumer != NULL) ASSERT(!producer->consumer) ASSERT(consumer->consumer) msg->MakeEmpty(); if (mustConnect) msg->what = MSG_ENDPOINTS_CONNECTED; else msg->what = MSG_ENDPOINTS_DISCONNECTED; msg->AddInt32("midi:producer", producer->id); msg->AddInt32("midi:consumer", consumer->id); } /*! Figures out which application a message came from. Returns NULL if the application is not registered. */ app_t* MidiServerApp::_WhichApp(BMessage* msg) { ASSERT(msg != NULL) BMessenger retadr = msg->ReturnAddress(); for (int32 t = 0; t < _CountApps(); ++t) { app_t* app = _AppAt(t); if (app->messenger.Team() == retadr.Team()) return app; } TRACE(("Application %" B_PRId32 " is not registered", retadr.Team())) return NULL; } /*! Looks at the "midi:id" field from a message, and returns the endpoint object that corresponds to that ID. It also checks whether the application specified by "app" really owns the endpoint. Returns NULL on error. */ endpoint_t* MidiServerApp::_WhichEndpoint(BMessage* msg, app_t* app) { ASSERT(msg != NULL) ASSERT(app != NULL) int32 id; if (msg->FindInt32("midi:id", &id) == B_OK) { endpoint_t* endpoint = _FindEndpoint(id); if (endpoint != NULL && endpoint->app == app) return endpoint; } TRACE(("Endpoint not found or wrong app")) return NULL; } /*! Returns the endpoint with the specified ID, or \c NULL if no such endpoint exists on the roster. */ endpoint_t* MidiServerApp::_FindEndpoint(int32 id) { if (id > 0) { for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* endpoint = _EndpointAt(t); if (endpoint->id == id) return endpoint; } } TRACE(("Endpoint %" B_PRId32 " not found", id)) return NULL; } /*! Sends notification messages to all registered apps, except to the application that triggered the event. The "except" app is allowed to be NULL. */ void MidiServerApp::_NotifyAll(BMessage* msg, app_t* except) { ASSERT(msg != NULL) for (int32 t = _CountApps() - 1; t >= 0; --t) { app_t* app = _AppAt(t); if (app != except && !_SendNotification(app, msg)) { delete (app_t*)fApps.RemoveItem(t); #ifdef DEBUG _DumpApps(); #endif } } } /*! Sends a notification message to an application, which is not necessarily registered yet. Applications never reply to such notification messages. */ bool MidiServerApp::_SendNotification(app_t* app, BMessage* msg) { ASSERT(app != NULL) ASSERT(msg != NULL) status_t status = app->messenger.SendMessage(msg, (BHandler*) NULL, TIMEOUT); if (status != B_OK) _DeliveryError(app); return status == B_OK; } /*! Sends a reply to a request made by an application. If "app" is NULL, the application is not registered (and the reply should contain an error code). */ bool MidiServerApp::_SendReply(app_t* app, BMessage* msg, BMessage* reply) { ASSERT(msg != NULL) ASSERT(reply != NULL) status_t status = msg->SendReply(reply, (BHandler*) NULL, TIMEOUT); if (status != B_OK && app != NULL) { _DeliveryError(app); fApps.RemoveItem(app); delete app; #ifdef DEBUG _DumpApps(); #endif } return status == B_OK; } /*! Removes an app and all of its endpoints from the roster if a reply or notification message cannot be delivered. (Waiting for communications to fail is actually our only way to get rid of stale endpoints.) */ void MidiServerApp::_DeliveryError(app_t* app) { ASSERT(app != NULL) // We cannot communicate with the app, so we assume it's // dead. We need to remove its endpoints from the roster, // but we cannot do that right away; removing endpoints // triggers a bunch of new notifications and we don't want // those to get in the way of the notifications we are // currently sending out. Instead, we consider the death // of an app as a separate event, and pretend that the // now-dead app sent us delete requests for its endpoints. TRACE(("Delivery error; unregistering app (%p)", app)) BMessage msg; for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* endpoint = _EndpointAt(t); if (endpoint->app == app) { msg.MakeEmpty(); msg.what = MSG_PURGE_ENDPOINT; msg.AddInt32("midi:id", endpoint->id); // It is not safe to post a message to your own // looper's message queue, because you risk a // deadlock if the queue is full. The chance of // that happening is fairly small, but just in // case, we catch it with a timeout. Because this // situation is so unlikely, I decided to simply // forget about the whole "purge" message then. if (be_app_messenger.SendMessage(&msg, (BHandler*)NULL, TIMEOUT) != B_OK) { WARN("Could not deliver purge message") } } } } int32 MidiServerApp::_CountApps() { return fApps.CountItems(); } app_t* MidiServerApp::_AppAt(int32 index) { ASSERT(index >= 0 && index < _CountApps()) return (app_t*)fApps.ItemAt(index); } int32 MidiServerApp::_CountEndpoints() { return fEndpoints.CountItems(); } endpoint_t* MidiServerApp::_EndpointAt(int32 index) { ASSERT(index >= 0 && index < _CountEndpoints()) return (endpoint_t*)fEndpoints.ItemAt(index); } int32 MidiServerApp::_CountConnections(endpoint_t* producer) { ASSERT(producer != NULL) ASSERT(!producer->consumer) return producer->connections.CountItems(); } endpoint_t* MidiServerApp::_ConnectionAt(endpoint_t* producer, int32 index) { ASSERT(producer != NULL) ASSERT(!producer->consumer) ASSERT(index >= 0 && index < _CountConnections(producer)) return (endpoint_t*)producer->connections.ItemAt(index); } #ifdef DEBUG void MidiServerApp::_DumpApps() { printf("*** START DumpApps\n"); for (int32 t = 0; t < _CountApps(); ++t) { app_t* app = _AppAt(t); printf("\tapp %" B_PRId32 " (%p): team %" B_PRId32 "\n", t, app, app->messenger.Team()); } printf("*** END DumpApps\n"); } void MidiServerApp::_DumpEndpoints() { printf("*** START DumpEndpoints\n"); for (int32 t = 0; t < _CountEndpoints(); ++t) { endpoint_t* endpoint = _EndpointAt(t); printf("\tendpoint %" B_PRId32 " (%p):\n", t, endpoint); printf("\t\tid %" B_PRId32 ", name '%s', %s, %s, app %p\n", endpoint->id, endpoint->name.String(), endpoint->consumer ? "consumer" : "producer", endpoint->registered ? "registered" : "unregistered", endpoint->app); printf("\t\tproperties: "); endpoint->properties.PrintToStream(); if (endpoint->consumer) printf("\t\tport %" B_PRId32 ", latency %" B_PRIdBIGTIME "\n", endpoint->port, endpoint->latency); else { printf("\t\tconnections:\n"); for (int32 k = 0; k < _CountConnections(endpoint); ++k) { endpoint_t* consumer = _ConnectionAt(endpoint, k); printf("\t\t\tid %" B_PRId32 " (%p)\n", consumer->id, consumer); } } } printf("*** END DumpEndpoints\n"); } #endif // DEBUG // #pragma mark - int main() { status_t status; MidiServerApp app(status); if (status == B_OK) app.Run(); return status == B_OK ? EXIT_SUCCESS : EXIT_FAILURE; }