1/*
2 * Copyright 2002-2015, Haiku, Inc. All rights reserved.
3 * Copyright 2002-2004, Matthijs Hollemans
4 * Copyright 2021, Panagiotis "Ivory" Vasilopoulos <git@n0toose.net>
5 * Distributed under the terms of the MIT License.
6 *
7 * Authors:
8 *		Humdinger
9 *		Matthijs Hollemans
10 *		Oliver Tappe
11 *		Panagiotis "Ivory" Vasilopoulos
12 *		Philippe Houdoin
13 */
14
15
16#include "MidiServerApp.h"
17
18#include <new>
19
20#include <AboutWindow.h>
21#include <Catalog.h>
22#include <Locale.h>
23#include <LocaleRoster.h>
24
25#include "debug.h"
26#include "protocol.h"
27#include "PortDrivers.h"
28#include "ServerDefs.h"
29
30
31using std::nothrow;
32
33
34#undef B_TRANSLATION_CONTEXT
35#define B_TRANSLATION_CONTEXT "midi_server"
36
37
38MidiServerApp::MidiServerApp(status_t& error)
39	:
40	BServer(MIDI_SERVER_SIGNATURE, true, &error)
41{
42	TRACE(("Running Haiku MIDI server"))
43
44	fNextID = 1;
45	fDeviceWatcher = new(std::nothrow) DeviceWatcher();
46	if (fDeviceWatcher != NULL)
47		fDeviceWatcher->Run();
48}
49
50
51MidiServerApp::~MidiServerApp()
52{
53	if (fDeviceWatcher && fDeviceWatcher->Lock())
54		fDeviceWatcher->Quit();
55
56	for (int32 t = 0; t < _CountApps(); ++t) {
57		delete _AppAt(t);
58	}
59
60	for (int32 t = 0; t < _CountEndpoints(); ++t) {
61		delete _EndpointAt(t);
62	}
63}
64
65
66void
67MidiServerApp::AboutRequested()
68{
69	BAboutWindow* window = new BAboutWindow(B_TRANSLATE_SYSTEM_NAME(
70		"Haiku MIDI Server"), MIDI_SERVER_SIGNATURE);
71	window->AddDescription(B_TRANSLATE(
72		"Notes disguised as bytes\n"
73		"propagating to endpoints-\n"
74		"An aural delight."));
75
76	const char* extraCopyrights[] = {
77		"2002-2004 Matthijs Hollemans",
78		"2021 Panagiotis \"Ivory\" Vasilopoulos",
79		NULL
80	};
81
82	const char* authors[] = {
83		"Humdinger",
84		"Matthijs Hollemans",
85		"Oliver Tappe",
86		"Panagiotis \"Ivory\" Vasilopoulos",
87		"Philippe Houdoin",
88		NULL
89	};
90
91	window->AddCopyright(2021, "Haiku, Inc.", extraCopyrights);
92	window->AddAuthors(authors);
93
94	window->Show();
95}
96
97
98void
99MidiServerApp::MessageReceived(BMessage* msg)
100{
101#ifdef DEBUG
102	printf("IN "); msg->PrintToStream();
103#endif
104
105	switch (msg->what) {
106		case MSG_REGISTER_APP:
107			_OnRegisterApp(msg);
108			break;
109		case MSG_CREATE_ENDPOINT:
110			_OnCreateEndpoint(msg);
111			break;
112		case MSG_DELETE_ENDPOINT:
113			_OnDeleteEndpoint(msg);
114			break;
115		case MSG_PURGE_ENDPOINT:
116			_OnPurgeEndpoint(msg);
117			break;
118		case MSG_CHANGE_ENDPOINT:
119			_OnChangeEndpoint(msg);
120			break;
121		case MSG_CONNECT_ENDPOINTS:
122			_OnConnectDisconnect(msg);
123			break;
124		case MSG_DISCONNECT_ENDPOINTS:
125			_OnConnectDisconnect(msg);
126			break;
127
128		default:
129			super::MessageReceived(msg);
130			break;
131	}
132}
133
134
135void
136MidiServerApp::_OnRegisterApp(BMessage* msg)
137{
138	TRACE(("MidiServerApp::_OnRegisterApp"))
139
140	// We only send the "app registered" message upon success,
141	// so if anything goes wrong here, we do not let the app
142	// know about it, and we consider it unregistered. (Most
143	// likely, the app is dead. If not, it freezes forever
144	// in anticipation of a message that will never arrive.)
145
146	app_t* app = new app_t;
147
148	if (msg->FindMessenger("midi:messenger", &app->messenger) == B_OK
149		&& _SendAllEndpoints(app)
150		&& _SendAllConnections(app)) {
151		BMessage reply;
152		reply.what = MSG_APP_REGISTERED;
153
154		if (_SendNotification(app, &reply)) {
155			fApps.AddItem(app);
156#ifdef DEBUG
157			_DumpApps();
158#endif
159			return;
160		}
161	}
162
163	delete app;
164}
165
166
167void
168MidiServerApp::_OnCreateEndpoint(BMessage* msg)
169{
170	TRACE(("MidiServerApp::_OnCreateEndpoint"))
171
172	status_t status;
173	endpoint_t* endpoint = new endpoint_t;
174
175	endpoint->app = _WhichApp(msg);
176	if (endpoint->app == NULL) {
177		status = B_ERROR;
178	} else {
179		status = B_BAD_VALUE;
180
181		if (msg->FindBool("midi:consumer", &endpoint->consumer) == B_OK
182			&& msg->FindBool("midi:registered", &endpoint->registered) == B_OK
183			&& msg->FindString("midi:name", &endpoint->name) == B_OK
184			&& msg->FindMessage("midi:properties", &endpoint->properties)
185					== B_OK) {
186			if (endpoint->consumer) {
187				if (msg->FindInt32("midi:port", &endpoint->port) == B_OK
188					&& msg->FindInt64("midi:latency", &endpoint->latency)
189							== B_OK)
190					status = B_OK;
191			} else
192				status = B_OK;
193		}
194	}
195
196	BMessage reply;
197
198	if (status == B_OK) {
199		endpoint->id = fNextID++;
200		reply.AddInt32("midi:id", endpoint->id);
201	}
202
203	reply.AddInt32("midi:result", status);
204
205	if (_SendReply(endpoint->app, msg, &reply) && status == B_OK)
206		_AddEndpoint(msg, endpoint);
207	else
208		delete endpoint;
209}
210
211
212void
213MidiServerApp::_OnDeleteEndpoint(BMessage* msg)
214{
215	TRACE(("MidiServerApp::_OnDeleteEndpoint"))
216
217	// Clients send the "delete endpoint" message from
218	// the BMidiEndpoint destructor, so there is no point
219	// sending a reply, because the endpoint object will
220	// be destroyed no matter what.
221
222	app_t* app = _WhichApp(msg);
223	if (app != NULL) {
224		endpoint_t* endpoint = _WhichEndpoint(msg, app);
225		if (endpoint != NULL)
226			_RemoveEndpoint(app, endpoint);
227	}
228}
229
230
231void
232MidiServerApp::_OnPurgeEndpoint(BMessage* msg)
233{
234	TRACE(("MidiServerApp::_OnPurgeEndpoint"))
235
236	// This performs the same task as OnDeleteEndpoint(),
237	// except that this message was send by the midi_server
238	// itself, so we don't check that the app that made the
239	// request really is the owner of the endpoint. (But we
240	// _do_ check that the message came from the server.)
241
242	if (!msg->IsSourceRemote()) {
243		int32 id;
244		if (msg->FindInt32("midi:id", &id) == B_OK) {
245			endpoint_t* endpoint = _FindEndpoint(id);
246			if (endpoint != NULL)
247				_RemoveEndpoint(NULL, endpoint);
248		}
249	}
250}
251
252
253void
254MidiServerApp::_OnChangeEndpoint(BMessage* msg)
255{
256	TRACE(("MidiServerApp::_OnChangeEndpoint"))
257
258	endpoint_t* endpoint = NULL;
259	status_t status;
260
261	app_t* app = _WhichApp(msg);
262	if (app == NULL)
263		status = B_ERROR;
264	else {
265		endpoint = _WhichEndpoint(msg, app);
266		if (endpoint == NULL)
267			status = B_BAD_VALUE;
268		else
269			status = B_OK;
270	}
271
272	BMessage reply;
273	reply.AddInt32("midi:result", status);
274
275	if (_SendReply(app, msg, &reply) && status == B_OK) {
276		TRACE(("Endpoint %" B_PRId32 " (%p) changed", endpoint->id, endpoint))
277
278		BMessage notify;
279		notify.what = MSG_ENDPOINT_CHANGED;
280		notify.AddInt32("midi:id", endpoint->id);
281
282		bool registered;
283		if (msg->FindBool("midi:registered", &registered) == B_OK) {
284			notify.AddBool("midi:registered", registered);
285			endpoint->registered = registered;
286		}
287
288		BString name;
289		if (msg->FindString("midi:name", &name) == B_OK) {
290			notify.AddString("midi:name", name);
291			endpoint->name = name;
292		}
293
294		BMessage properties;
295		if (msg->FindMessage("midi:properties", &properties) == B_OK) {
296			notify.AddMessage("midi:properties", &properties);
297			endpoint->properties = properties;
298		}
299
300		bigtime_t latency;
301		if (msg->FindInt64("midi:latency", &latency) == B_OK) {
302			notify.AddInt64("midi:latency", latency);
303			endpoint->latency = latency;
304		}
305
306		_NotifyAll(&notify, app);
307
308#ifdef DEBUG
309		_DumpEndpoints();
310#endif
311	}
312}
313
314
315void
316MidiServerApp::_OnConnectDisconnect(BMessage* msg)
317{
318	TRACE(("MidiServerApp::_OnConnectDisconnect"))
319
320	bool mustConnect = msg->what == MSG_CONNECT_ENDPOINTS;
321
322	status_t status;
323	endpoint_t* producer = NULL;
324	endpoint_t* consumer = NULL;
325
326	app_t* app = _WhichApp(msg);
327	if (app == NULL)
328		status = B_ERROR;
329	else {
330		status = B_BAD_VALUE;
331
332		int32 producerID;
333		int32 consumerID;
334		if (msg->FindInt32("midi:producer", &producerID) == B_OK
335			&& msg->FindInt32("midi:consumer", &consumerID) == B_OK) {
336			producer = _FindEndpoint(producerID);
337			consumer = _FindEndpoint(consumerID);
338
339			if (producer != NULL && !producer->consumer) {
340				if (consumer != NULL && consumer->consumer) {
341					// It is an error to connect two endpoints that
342					// are already connected, or to disconnect two
343					// endpoints that are not connected at all.
344
345					if (mustConnect == producer->connections.HasItem(consumer))
346						status = B_ERROR;
347					else
348						status = B_OK;
349				}
350			}
351		}
352	}
353
354	BMessage reply;
355	reply.AddInt32("midi:result", status);
356
357	if (_SendReply(app, msg, &reply) && status == B_OK) {
358		if (mustConnect) {
359			TRACE(("Connection made: %" B_PRId32 " ---> %" B_PRId32,
360				producer->id, consumer->id))
361
362			producer->connections.AddItem(consumer);
363		} else {
364			TRACE(("Connection broken: %" B_PRId32 " -X-> %" B_PRId32,
365				producer->id, consumer->id))
366
367			producer->connections.RemoveItem(consumer);
368		}
369
370		BMessage notify;
371		_MakeConnectedNotification(&notify, producer, consumer, mustConnect);
372		_NotifyAll(&notify, app);
373
374#ifdef DEBUG
375		_DumpEndpoints();
376#endif
377	}
378}
379
380
381/*!	Sends an app MSG_ENDPOINT_CREATED notifications for
382	all current endpoints. Used when the app registers.
383*/
384bool
385MidiServerApp::_SendAllEndpoints(app_t* app)
386{
387	ASSERT(app != NULL)
388
389	BMessage notify;
390
391	for (int32 t = 0; t < _CountEndpoints(); ++t) {
392		endpoint_t* endpoint = _EndpointAt(t);
393
394		_MakeCreatedNotification(&notify, endpoint);
395
396		if (!_SendNotification(app, &notify))
397			return false;
398	}
399
400	return true;
401}
402
403
404/*!	Sends an app MSG_ENDPOINTS_CONNECTED notifications for
405	all current connections. Used when the app registers.
406*/
407bool
408MidiServerApp::_SendAllConnections(app_t* app)
409{
410	ASSERT(app != NULL)
411
412	BMessage notify;
413
414	for (int32 t = 0; t < _CountEndpoints(); ++t) {
415		endpoint_t* producer = _EndpointAt(t);
416		if (!producer->consumer) {
417			for (int32 k = 0; k < _CountConnections(producer); ++k) {
418				endpoint_t* consumer = _ConnectionAt(producer, k);
419
420				_MakeConnectedNotification(&notify, producer, consumer, true);
421
422				if (!_SendNotification(app, &notify))
423					return false;
424			}
425		}
426	}
427
428	return true;
429}
430
431
432/*!	Adds the specified endpoint to the roster, and notifies
433	all other applications about this event.
434*/
435void
436MidiServerApp::_AddEndpoint(BMessage* msg, endpoint_t* endpoint)
437{
438	ASSERT(msg != NULL)
439	ASSERT(endpoint != NULL)
440	ASSERT(!fEndpoints.HasItem(endpoint))
441
442	TRACE(("Endpoint %" B_PRId32 " (%p) added", endpoint->id, endpoint))
443
444	fEndpoints.AddItem(endpoint);
445
446	BMessage notify;
447	_MakeCreatedNotification(&notify, endpoint);
448	_NotifyAll(&notify, endpoint->app);
449
450#ifdef DEBUG
451	_DumpEndpoints();
452#endif
453}
454
455
456/*!	Removes an endpoint from the roster, and notifies all
457	other apps about this event. "app" is the application
458	that the endpoint belongs to; if it is NULL, the app
459	no longer exists and we're purging the endpoint.
460*/
461void
462MidiServerApp::_RemoveEndpoint(app_t* app, endpoint_t* endpoint)
463{
464	ASSERT(endpoint != NULL)
465	ASSERT(fEndpoints.HasItem(endpoint))
466
467	TRACE(("Endpoint %" B_PRId32 " (%p) removed", endpoint->id, endpoint))
468
469	fEndpoints.RemoveItem(endpoint);
470
471	if (endpoint->consumer)
472		_DisconnectDeadConsumer(endpoint);
473
474	BMessage notify;
475	notify.what = MSG_ENDPOINT_DELETED;
476	notify.AddInt32("midi:id", endpoint->id);
477	_NotifyAll(&notify, app);
478
479	delete endpoint;
480
481#ifdef DEBUG
482	_DumpEndpoints();
483#endif
484}
485
486
487/*!	Removes a consumer from the list of connections of
488	all the producers it is connected to, just before
489	we remove it from the roster.
490*/
491void
492MidiServerApp::_DisconnectDeadConsumer(endpoint_t* consumer)
493{
494	ASSERT(consumer != NULL)
495	ASSERT(consumer->consumer)
496
497	for (int32 t = 0; t < _CountEndpoints(); ++t) {
498		endpoint_t* producer = _EndpointAt(t);
499		if (!producer->consumer)
500			producer->connections.RemoveItem(consumer);
501	}
502}
503
504
505//! Fills up a MSG_ENDPOINT_CREATED message.
506void
507MidiServerApp::_MakeCreatedNotification(BMessage* msg, endpoint_t* endpoint)
508{
509	ASSERT(msg != NULL)
510	ASSERT(endpoint != NULL)
511
512	msg->MakeEmpty();
513	msg->what = MSG_ENDPOINT_CREATED;
514	msg->AddInt32("midi:id", endpoint->id);
515	msg->AddBool("midi:consumer", endpoint->consumer);
516	msg->AddBool("midi:registered", endpoint->registered);
517	msg->AddString("midi:name", endpoint->name);
518	msg->AddMessage("midi:properties", &endpoint->properties);
519
520	if (endpoint->consumer) {
521		msg->AddInt32("midi:port", endpoint->port);
522		msg->AddInt64("midi:latency", endpoint->latency);
523	}
524}
525
526
527//! Fills up a MSG_ENDPOINTS_(DIS)CONNECTED message.
528void
529MidiServerApp::_MakeConnectedNotification(BMessage* msg, endpoint_t* producer,
530	endpoint_t* consumer, bool mustConnect)
531{
532	ASSERT(msg != NULL)
533	ASSERT(producer != NULL)
534	ASSERT(consumer != NULL)
535	ASSERT(!producer->consumer)
536	ASSERT(consumer->consumer)
537
538	msg->MakeEmpty();
539
540	if (mustConnect)
541		msg->what = MSG_ENDPOINTS_CONNECTED;
542	else
543		msg->what = MSG_ENDPOINTS_DISCONNECTED;
544
545	msg->AddInt32("midi:producer", producer->id);
546	msg->AddInt32("midi:consumer", consumer->id);
547}
548
549
550/*!	Figures out which application a message came from.
551	Returns NULL if the application is not registered.
552*/
553app_t*
554MidiServerApp::_WhichApp(BMessage* msg)
555{
556	ASSERT(msg != NULL)
557
558	BMessenger retadr = msg->ReturnAddress();
559
560	for (int32 t = 0; t < _CountApps(); ++t) {
561		app_t* app = _AppAt(t);
562		if (app->messenger.Team() == retadr.Team())
563			return app;
564	}
565
566	TRACE(("Application %" B_PRId32 " is not registered", retadr.Team()))
567
568	return NULL;
569}
570
571
572/*!	Looks at the "midi:id" field from a message, and returns
573	the endpoint object that corresponds to that ID. It also
574	checks whether the application specified by "app" really
575	owns the endpoint. Returns NULL on error.
576*/
577endpoint_t*
578MidiServerApp::_WhichEndpoint(BMessage* msg, app_t* app)
579{
580	ASSERT(msg != NULL)
581	ASSERT(app != NULL)
582
583	int32 id;
584	if (msg->FindInt32("midi:id", &id) == B_OK) {
585		endpoint_t* endpoint = _FindEndpoint(id);
586		if (endpoint != NULL && endpoint->app == app)
587			return endpoint;
588	}
589
590	TRACE(("Endpoint not found or wrong app"))
591	return NULL;
592}
593
594
595/*!	Returns the endpoint with the specified ID, or
596	\c NULL if no such endpoint exists on the roster.
597*/
598endpoint_t*
599MidiServerApp::_FindEndpoint(int32 id)
600{
601	if (id > 0) {
602		for (int32 t = 0; t < _CountEndpoints(); ++t) {
603			endpoint_t* endpoint = _EndpointAt(t);
604			if (endpoint->id == id)
605				return endpoint;
606		}
607	}
608
609	TRACE(("Endpoint %" B_PRId32 " not found", id))
610	return NULL;
611}
612
613
614/*!	Sends notification messages to all registered apps,
615	except to the application that triggered the event.
616	The "except" app is allowed to be NULL.
617*/
618void
619MidiServerApp::_NotifyAll(BMessage* msg, app_t* except)
620{
621	ASSERT(msg != NULL)
622
623	for (int32 t = _CountApps() - 1; t >= 0; --t) {
624		app_t* app = _AppAt(t);
625		if (app != except && !_SendNotification(app, msg)) {
626			delete (app_t*)fApps.RemoveItem(t);
627#ifdef DEBUG
628			_DumpApps();
629#endif
630		}
631	}
632}
633
634
635/*!	Sends a notification message to an application, which is
636	not necessarily registered yet. Applications never reply
637	to such notification messages.
638*/
639bool
640MidiServerApp::_SendNotification(app_t* app, BMessage* msg)
641{
642	ASSERT(app != NULL)
643	ASSERT(msg != NULL)
644
645	status_t status = app->messenger.SendMessage(msg, (BHandler*) NULL,
646		TIMEOUT);
647	if (status != B_OK)
648		_DeliveryError(app);
649
650	return status == B_OK;
651}
652
653
654/*!	Sends a reply to a request made by an application.
655	If "app" is NULL, the application is not registered
656	(and the reply should contain an error code).
657*/
658bool
659MidiServerApp::_SendReply(app_t* app, BMessage* msg, BMessage* reply)
660{
661	ASSERT(msg != NULL)
662	ASSERT(reply != NULL)
663
664	status_t status = msg->SendReply(reply, (BHandler*) NULL, TIMEOUT);
665	if (status != B_OK && app != NULL) {
666		_DeliveryError(app);
667		fApps.RemoveItem(app);
668		delete app;
669
670#ifdef DEBUG
671		_DumpApps();
672#endif
673	}
674
675	return status == B_OK;
676}
677
678
679/*!	Removes an app and all of its endpoints from the roster
680	if a reply or notification message cannot be delivered.
681	(Waiting for communications to fail is actually our only
682	way to get rid of stale endpoints.)
683*/
684void
685MidiServerApp::_DeliveryError(app_t* app)
686{
687	ASSERT(app != NULL)
688
689	// We cannot communicate with the app, so we assume it's
690	// dead. We need to remove its endpoints from the roster,
691	// but we cannot do that right away; removing endpoints
692	// triggers a bunch of new notifications and we don't want
693	// those to get in the way of the notifications we are
694	// currently sending out. Instead, we consider the death
695	// of an app as a separate event, and pretend that the
696	// now-dead app sent us delete requests for its endpoints.
697
698	TRACE(("Delivery error; unregistering app (%p)", app))
699
700	BMessage msg;
701
702	for (int32 t = 0; t < _CountEndpoints(); ++t) {
703		endpoint_t* endpoint = _EndpointAt(t);
704		if (endpoint->app == app) {
705			msg.MakeEmpty();
706			msg.what = MSG_PURGE_ENDPOINT;
707			msg.AddInt32("midi:id", endpoint->id);
708
709			// It is not safe to post a message to your own
710			// looper's message queue, because you risk a
711			// deadlock if the queue is full. The chance of
712			// that happening is fairly small, but just in
713			// case, we catch it with a timeout. Because this
714			// situation is so unlikely, I decided to simply
715			// forget about the whole "purge" message then.
716
717			if (be_app_messenger.SendMessage(&msg, (BHandler*)NULL,
718					TIMEOUT) != B_OK) {
719				WARN("Could not deliver purge message")
720			}
721		}
722	}
723}
724
725
726int32
727MidiServerApp::_CountApps()
728{
729	return fApps.CountItems();
730}
731
732
733app_t*
734MidiServerApp::_AppAt(int32 index)
735{
736	ASSERT(index >= 0 && index < _CountApps())
737
738	return (app_t*)fApps.ItemAt(index);
739}
740
741
742int32
743MidiServerApp::_CountEndpoints()
744{
745	return fEndpoints.CountItems();
746}
747
748
749endpoint_t*
750MidiServerApp::_EndpointAt(int32 index)
751{
752	ASSERT(index >= 0 && index < _CountEndpoints())
753
754	return (endpoint_t*)fEndpoints.ItemAt(index);
755}
756
757
758int32
759MidiServerApp::_CountConnections(endpoint_t* producer)
760{
761	ASSERT(producer != NULL)
762	ASSERT(!producer->consumer)
763
764	return producer->connections.CountItems();
765}
766
767
768endpoint_t*
769MidiServerApp::_ConnectionAt(endpoint_t* producer, int32 index)
770{
771	ASSERT(producer != NULL)
772	ASSERT(!producer->consumer)
773	ASSERT(index >= 0 && index < _CountConnections(producer))
774
775	return (endpoint_t*)producer->connections.ItemAt(index);
776}
777
778
779#ifdef DEBUG
780void
781MidiServerApp::_DumpApps()
782{
783	printf("*** START DumpApps\n");
784
785	for (int32 t = 0; t < _CountApps(); ++t) {
786		app_t* app = _AppAt(t);
787
788		printf("\tapp %" B_PRId32 " (%p): team %" B_PRId32 "\n", t, app,
789			app->messenger.Team());
790	}
791
792	printf("*** END DumpApps\n");
793}
794
795
796void
797MidiServerApp::_DumpEndpoints()
798{
799	printf("*** START DumpEndpoints\n");
800
801	for (int32 t = 0; t < _CountEndpoints(); ++t) {
802		endpoint_t* endpoint = _EndpointAt(t);
803
804		printf("\tendpoint %" B_PRId32 " (%p):\n", t, endpoint);
805		printf("\t\tid %" B_PRId32 ", name '%s', %s, %s, app %p\n",
806			endpoint->id, endpoint->name.String(),
807			endpoint->consumer ? "consumer" : "producer",
808			endpoint->registered ? "registered" : "unregistered",
809			endpoint->app);
810		printf("\t\tproperties: "); endpoint->properties.PrintToStream();
811
812		if (endpoint->consumer)
813			printf("\t\tport %" B_PRId32 ", latency %" B_PRIdBIGTIME "\n",
814				endpoint->port, endpoint->latency);
815		else {
816			printf("\t\tconnections:\n");
817			for (int32 k = 0; k < _CountConnections(endpoint); ++k) {
818				endpoint_t* consumer = _ConnectionAt(endpoint, k);
819				printf("\t\t\tid %" B_PRId32 " (%p)\n", consumer->id, consumer);
820			}
821		}
822	}
823
824	printf("*** END DumpEndpoints\n");
825}
826#endif	// DEBUG
827
828
829//	#pragma mark -
830
831
832int
833main()
834{
835	status_t status;
836	MidiServerApp app(status);
837
838	if (status == B_OK)
839		app.Run();
840
841	return status == B_OK ? EXIT_SUCCESS : EXIT_FAILURE;
842}
843