UI-less Linux applications

For Linux specific issues

UI-less Linux applications

Postby jpo » Tue Jul 07, 2009 1:11 pm

Hi Jules,

The Juce messaging stuff is used at many places in JUCE code. Even in modules that are not related to graphical user interface (all the audio stuff for example). The problem on linux is that the messaging uses the X server to transmit internal juce messages, all none of this stuff works when you run it without a X server. I believe this is quite easily fixable by rewriting the juce_linux_Messaging.cpp file, in order to replace the XSendEvent stuff for internal juce messages by a custom message queue. The "juce_dispatchNextMessageOnSystemQueue" would then poll events from that queue, and from the X queue (when "display" is not null). That way we would be able to use more Juce classes in command-line apps.

This is something that I may try to do in the future, but I wanted to share my thoughts about this first.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby kraken » Wed Jul 08, 2009 12:23 am

i would like to see how this can be done properly cause it will be more than useful ! and still you can use the juce power in the command line...
Image
User avatar
kraken
JUCE UberWeenie
 
Posts: 1063
Joined: Wed Feb 09, 2005 10:31 am
Location: Venice, Italy

Postby jpo » Sat Jul 11, 2009 12:18 pm

I do agree !

I wrote it yesterday, and so far it seems to work perfectly (no regressions, only improvements). Here is the drop-in replacement for juce_linux_Messaging.cpp

Code: Select all
/*
  ==============================================================================

   This file is part of the JUCE library - "Jules' Utility Class Extensions"
   Copyright 2004-7 by Raw Material Software ltd.

  ------------------------------------------------------------------------------

   JUCE can be redistributed and/or modified under the terms of the
   GNU General Public License, as published by the Free Software Foundation;
   either version 2 of the License, or (at your option) any later version.

   JUCE is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with JUCE; if not, visit www.gnu.org/licenses or write to the
   Free Software Foundation, Inc., 59 Temple Place, Suite 330,
   Boston, MA 02111-1307 USA

  ------------------------------------------------------------------------------

   If you'd like to release a closed-source product which uses JUCE, commercial
   licenses are also available: visit www.rawmaterialsoftware.com/juce for
   more information.

  ==============================================================================
*/

#include "../../../juce_Config.h"

#include "linuxincludes.h"
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xresource.h>
#include <X11/Xutil.h>
#include "../../../src/juce_core/basics/juce_StandardHeader.h"

BEGIN_JUCE_NAMESPACE

#include "../../../src/juce_appframework/events/juce_MessageManager.h"
#include "../../../src/juce_core/threads/juce_WaitableEvent.h"
#include "../../../src/juce_core/threads/juce_Process.h"
#include "../../../src/juce_core/threads/juce_ScopedLock.h"
#include "../../../src/juce_core/containers/juce_OwnedArray.h"

#ifdef JUCE_DEBUG
  #define JUCE_DEBUG_XERRORS 1
#endif

Display* display = 0;     // This is also referenced from WindowDriver.cpp
Window juce_messageWindowHandle = None;

#define SpecialAtom         "JUCESpecialAtom"
#define BroadcastAtom       "JUCEBroadcastAtom"
#define SpecialCallbackAtom "JUCESpecialCallbackAtom"

static Atom specialId;
static Atom broadcastId;
static Atom specialCallbackId;

// This is referenced from WindowDriver.cpp
XContext improbableNumber;

// Defined in WindowDriver.cpp
extern void juce_windowMessageReceive (XEvent* event);

struct MessageThreadFuncCall
{
    MessageCallbackFunction* func;
    void* parameter;
    void* result;
    CriticalSection lock;
    WaitableEvent event;
};

struct InternalMessageQueue {
  CriticalSection mutex;
  OwnedArray<Message> queue;
  int fd[2];
public:
  InternalMessageQueue() {
    int ret = ::socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
    jassert(ret == 0);
  }
  ~InternalMessageQueue() {
    close(fd[0]);
    close(fd[1]);
  }
  void postMessage(Message *msg) {
    ScopedLock lock(mutex); queue.add(msg);
    unsigned char x = 0xff;
    write(fd[0], &x, 1);
  }
  bool isEmpty() { ScopedLock lock(mutex); return queue.size() == 0; }
  Message *popMessage() {
    ScopedLock lock(mutex);
    Message *m = 0;
    if (queue.size()) {
      unsigned char x;
      read(fd[1], &x, 1);
      m = queue.getUnchecked(0);
      queue.remove(0, false /* deleteObject */);
    }
    return m;
  }
  int getWaitHandle() { return fd[1]; }
};

static InternalMessageQueue *internal_message_queue;

static bool errorCondition = false;
static XErrorHandler oldErrorHandler = (XErrorHandler) 0;
static XIOErrorHandler oldIOErrorHandler = (XIOErrorHandler) 0;

// (defined in another file to avoid problems including certain headers in this one)
extern bool juce_isRunningAsApplication();
extern void juce_x11_handleSelectionRequest(XSelectionRequestEvent &evt);

// Usually happens when client-server connection is broken
static int ioErrorHandler (Display* display)
{
    DBG (T("ERROR: connection to X server broken.. terminating."));

    errorCondition = true;

    if (juce_isRunningAsApplication())
        Process::terminate();

    return 0;
}

// A protocol error has occurred
static int errorHandler (Display* display, XErrorEvent* event)
{
#ifdef JUCE_DEBUG_XERRORS
    char errorStr[64] = { 0 };
    char requestStr[64] = { 0 };

    XGetErrorText (display, event->error_code, errorStr, 64);

    XGetErrorDatabaseText (display,
                           "XRequest",
                           (const char*) String (event->request_code),
                           "Unknown",
                           requestStr,
                           64);

    DBG (T("ERROR: X returned ") + String (errorStr) + T(" for operation ") + String (requestStr));
#endif

    return 0;
}

static bool breakIn = false;

// Breakin from keyboard
static void sig_handler (int sig)
{
    if (sig == SIGINT)
    {
        breakIn = true;
        return;
    }

    static bool reentrant = false;

    if (reentrant == false)
    {
        reentrant = true;

        // Illegal instruction
        fflush (stdout);
        Logger::outputDebugString ("ERROR: Program executed illegal instruction.. terminating");

        errorCondition = true;

        if (juce_isRunningAsApplication())
            Process::terminate();
    }
    else
    {
        if (juce_isRunningAsApplication())
            exit(0);
    }
}

//==============================================================================
void MessageManager::doPlatformSpecificInitialisation()
{
    // Initialise xlib for multiple thread support
    static bool initThreadCalled = false;

    if (! initThreadCalled)
    {
        if (! XInitThreads())
        {
            // This is fatal!  Print error and closedown
            Logger::outputDebugString ("Failed to initialise xlib thread support.");

            if (juce_isRunningAsApplication())
                Process::terminate();

            return;
        }

        initThreadCalled = true;
    }

    // This is called if the client/server connection is broken
    oldIOErrorHandler = XSetIOErrorHandler (ioErrorHandler);

    // This is called if a protocol error occurs
    oldErrorHandler = XSetErrorHandler (errorHandler);

    // Install signal handler for break-in
    struct sigaction saction;
    sigset_t maskSet;
    sigemptyset (&maskSet);
    saction.sa_handler = sig_handler;
    saction.sa_mask = maskSet;
    saction.sa_flags = 0;
    sigaction (SIGINT, &saction, NULL);

#ifndef _DEBUG
    // Setup signal handlers for various fatal errors
    sigaction (SIGILL, &saction, NULL);
    sigaction (SIGBUS, &saction, NULL);
    sigaction (SIGFPE, &saction, NULL);
    sigaction (SIGSEGV, &saction, NULL);
    sigaction (SIGSYS, &saction, NULL);
#endif

    internal_message_queue = new InternalMessageQueue();

    String displayName (getenv ("DISPLAY"));
    if (displayName.isEmpty())
        displayName = T(":0.0");

    display = XOpenDisplay (displayName);

    if (display == 0)
    {
        // This is no more fatal! check if display was opened with Desktop::isAvailable()
        /*Logger::outputDebugString ("Failed to open the X display.");

        if (juce_isRunningAsApplication())
            Process::terminate();*/

        return;
    }

    // Get defaults for various properties
    int screen = DefaultScreen (display);
    Window root = RootWindow (display, screen);
    Visual* visual = DefaultVisual (display, screen);

    // Create atoms for our ClientMessages (these cannot be deleted)
    specialId = XInternAtom (display, SpecialAtom, false);
    broadcastId = XInternAtom (display, BroadcastAtom, false);
    specialCallbackId = XInternAtom (display, SpecialCallbackAtom, false);

    // Create a context to store user data associated with Windows we
    // create in WindowDriver
    improbableNumber = XUniqueContext();

    // We're only interested in client messages for this window
    // which are always sent
    XSetWindowAttributes swa;
    swa.event_mask = NoEventMask;

    // Create our message window (this will never be mapped)
    juce_messageWindowHandle = XCreateWindow (display, root,
                                              0, 0, 1, 1, 0, 0, InputOnly,
                                              visual, CWEventMask, &swa);
}

void MessageManager::doPlatformSpecificShutdown()
{
    if (internal_message_queue) {
      deleteAndZero(internal_message_queue);
    }
    if (errorCondition == false && display)
    {
        XDestroyWindow (display, juce_messageWindowHandle);
        XCloseDisplay (display);

        // reset pointers
        juce_messageWindowHandle = 0;
        display = 0;

        // Restore original error handlers
        XSetIOErrorHandler (oldIOErrorHandler);
        oldIOErrorHandler = 0;
        XSetErrorHandler (oldErrorHandler);
        oldErrorHandler = 0;
    }
}

bool juce_postMessageToSystemQueue (void* message)
{
    if (errorCondition)
        return false;

    internal_message_queue->postMessage((Message*)message);
    return true;
}

bool juce_postMessageToX11Queue(void *message)
{
    XClientMessageEvent clientMsg;
    clientMsg.display = display;
    clientMsg.window = juce_messageWindowHandle;
    clientMsg.type = ClientMessage;
    clientMsg.format = 32;
    clientMsg.message_type = specialId;
#if JUCE_64BIT
    clientMsg.data.l[0] = (long) (0x00000000ffffffff & (((uint64) message) >> 32));
    clientMsg.data.l[1] = (long) (0x00000000ffffffff & (long) message);
#else
    clientMsg.data.l[0] = (long) message;
#endif

    XSendEvent (display, juce_messageWindowHandle, false,
                NoEventMask, (XEvent*) &clientMsg);

    XFlush (display); // This is necessary to ensure the event is delivered
    return true;
}

void MessageManager::broadcastMessage (const String& value) throw()
{
    /* TODO */
}

struct InternalCallFunctionMessage : public Message {
    MessageThreadFuncCall *ctx;
    InternalCallFunctionMessage(MessageThreadFuncCall *ctx_) : ctx(ctx_) {
        intParameter1 = 0x73774623;
    }
};

void* MessageManager::callFunctionOnMessageThread (MessageCallbackFunction* func,
                                                   void* parameter)
{
    void* retVal = 0;

    if (! errorCondition)
    {
        if (! isThisTheMessageThread())
        {
            static MessageThreadFuncCall messageFuncCallContext;

            const ScopedLock sl (messageFuncCallContext.lock);

            messageFuncCallContext.func = func;
            messageFuncCallContext.parameter = parameter;
            internal_message_queue->postMessage(new InternalCallFunctionMessage(&messageFuncCallContext));

            // Wait for it to complete before continuing
            messageFuncCallContext.event.wait();

            retVal = messageFuncCallContext.result;
        }
        else
        {
            // Just call the function directly
            retVal = func (parameter);
        }
    }

    return retVal;
}

/* wait for an event (either XEvent, or an internal Message) */
static bool juce_sleepUntilEvent(int timeout_ms)
{
    if (display && XPending(display)) return true;
    if (!internal_message_queue->isEmpty()) return true;

    struct timeval tv;
    tv.tv_sec=0;
    tv.tv_usec=timeout_ms * 1000;
    int fd0   = internal_message_queue->getWaitHandle();
    int fdmax = fd0;

    fd_set readset;
    FD_ZERO(&readset);
    FD_SET(fd0, &readset);
    if (display) {
      int fd1 = XConnectionNumber(display);
      FD_SET(fd1, &readset);
      fdmax = jmax(fd0, fd1);
    }
    int ret = select( fdmax+1, &readset, 0, 0, &tv );
   
    return (ret > 0); // ret <= 0 if error or timeout
}

/* handle next XEvent (if any) */
static bool juce_dispatchNextXEvent()
{
    if (!display || !XPending(display)) return false;
    XEvent evt;
    XNextEvent(display, &evt);   
    /* requires my version of juce_linux_Clipboard.cpp */
    //if (evt.type == SelectionRequest && evt.xany.window == juce_messageWindowHandle) {
    //  juce_x11_handleSelectionRequest(evt.xselectionrequest);
    //} else if (evt.type == SelectionClear && evt.xany.window == juce_messageWindowHandle) {   
    //  /* another window just grabbed the selection -- we just don't care */
    //} else
    if (evt.xany.window != juce_messageWindowHandle) {
      juce_windowMessageReceive (&evt);
    }
    return true;
}

/* handle next internal Message (if any) */
static bool juce_dispatchNextInternalMessage()
{
    if (internal_message_queue->isEmpty()) return false;
    Message *m = internal_message_queue->popMessage();
    InternalCallFunctionMessage *cfm = 0;
    /* handle function call */
    if (m->intParameter1 == 0x73774623 && (cfm = dynamic_cast<InternalCallFunctionMessage*>(m))) {
      MessageThreadFuncCall* const call = cfm->ctx;
      MessageCallbackFunction* func = call->func;
      call->result = (*func) (call->parameter);
      call->event.signal();
      deleteAndZero(m);
    } else {
      /* handle "normal" messages */
      MessageManager::getInstance()->deliverMessage(m);
    }
    return true;
}

/* this function expects that it will NEVER be called simultaneously for two concurrent threads */
bool juce_dispatchNextMessageOnSystemQueue (bool returnIfNoPendingMessages)
{
    while (1) {
      if (errorCondition)
        return false;
     
      if (breakIn) {
        errorCondition = true;
       
        if (juce_isRunningAsApplication())
          Process::terminate();
       
        return false;
      }
     
      static int event_cnt=0;
      ++event_cnt;
      /* the purpose here is to give either priority to XEvents or
         to internal messages This is necessary to keep a "good"
         behaviour when the cpu is overloaded
      */
      if ((event_cnt & 1)) {
        if (juce_dispatchNextXEvent() || juce_dispatchNextInternalMessage()) return true;
      } else {
        if (juce_dispatchNextInternalMessage() || juce_dispatchNextXEvent()) return true;
      }
     
      if (returnIfNoPendingMessages) // early exit
        return false;
     
      juce_sleepUntilEvent(4000 /* milliseconds */); // the timeout is to be on the safe side, but it does not seem to be useful
    }
    return true;
}

END_JUCE_NAMESPACE


(I don't claim any copyright on anything in this file, so anyone is free to do whatever they want with my changes)

As you can see , it is really simple indeed. Just an OwnedArray<Message> for the queue of internal juce messages, a pair of sockets for notifying the event loop. The juce_dispatchNextMessageOnSystemQueue now waits for 2 types of events instead of only XEvents. The only significant change, is that the order of events is not the same as it used to be since there are two events queues instead of one, and no queue should starve the other when they are both flooded with events (see the comment about the "event_cnt" variable). I have built the juce demo, the juce plugin host, and my own application and did not notice any regression.

A side effect is that is fixes the issue that I mentionned here:
juceforum/viewtopic.php?t=4000 (I have spent days on this one)

(As a side note, I now believe that this issue was due to the non-threadsafe use of X11 functions -- as far as I understand, when more that one thread may simultaneously use the same 'display*' object, each X function call should be protected by XLockDisplay()/XUnlockDisplay() . By default these function do nothing, except when XInitThreads has been called before XOpenDisplay)


Other small changes that I had to do to allow my app to run "headless", is to add a function Desktop::isAvailable() function which always return juce_isRunningAsApplication() on windows and mac, and also tests if display != 0 on linux:

Code: Select all
bool Desktop::isAvailable() throw() {
  return juce_isRunningAsApplication() && display != 0;
}


And to allow the app to startup when no DISPLAY is available, I had to add "if (!display) return;" at the beginning of juce_updateMultiMonitorInfo .

(this is not the part i am most proud of , to be honest)


Jules: when you have time, (and after a few weeks in order to let me test this a bit more), I hope that you will review this change to the juce_linux_Messaging.cpp file. I think that fixing the thread-safety problem of the original juce_postMessageToSystemQueue is enough to justify it.
Last edited by jpo on Tue Jul 21, 2009 2:30 pm, edited 1 time in total.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby jules » Sat Jul 11, 2009 3:45 pm

Jules: when you have time, (and after a few weeks in order to let me test this a bit more), I hope that you will review this change to the juce_linux_Messaging.cpp file. I think that fixing the thread-safety problem of the original juce_postMessageToSystemQueue is enough to justify it.


Cool - thanks, keep me posted if you make any changes!
User avatar
jules
Fearless Leader
 
Posts: 17204
Joined: Mon Sep 06, 2004 9:03 am
Location: London, UK

Postby jpo » Mon Jul 20, 2009 5:16 pm

It looks like there is an issue with repaints, some elements of the gui are sometimes repainted at the wrong locations. There is probably something between the LinuxRepaintManager and the LinuxComponentPeer that assumes a relative ordering between X11 messages and juce messages. Too bad...
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby jpo » Tue Jul 21, 2009 2:25 pm

Now I'm understanding the issue with repaints. It happens only when the XSHM extension is used. The LinuxRepaintManager is then using a shared memory ximage to hold the stuff that is repainted. What happens is that XShmPutImage does not display the content of the image immediately. So if one modifies the content of the image buffer just *after* XShmPutImage has been called, then it is the modified content that will be drawn !

Since the LinuxRepaintManager::performAnyPendingRepaintsNow() re-uses the same xshm image to draw various parts of the window, sometimes (on slow computers, with a slow Xorg) it happens that one overwrites the content of the image before it has been displayed with another content.

I see two solutions:
- (solution 1) the current test that determines if one can re-use the current ximage checks only that the drawing area can fit in the image:
Code: Select all
if (image == 0 || image->getWidth() < totalArea.getWidth()
                        || image->getHeight() < totalArea.getHeight()) {
   //delete image and create a fresh one
   ...
}


if could be replaced by a check that the drawing area lies *inside* the area affected to the image:
Code: Select all
if (image == 0 || !Rectangle(image_x, image_y, image->getWidth(), image->getHeight()).contains(totalArea)) {
   //delete image and create a fresh one
  ...
  image_x = totalArea.getX();
  image_y = totalArea.getY();
}


that way the race condition will still occur, but no part of the image will get drawn at the wrong place.

- (solution 2) do not allow performAnyPendingRepaintsNow to execute until the XShmPutImage has completed. That is done by setting to True the last argument of the call to XShmPutImage , and check for the completion event in the window event handler:


Code: Select all
   void handleWindowMessage (XEvent* event)
    {
        int CompletionNotify = XShmGetEventBase(display);

        switch (event->xany.type)
        {
           /* ...(snip).... */

            default:
              if (event->xany.type == CompletionNotify) {
                repainter->shmCompleted();
              }
                break;
        }


I believe this later approach is better from a performance point of view. This is the one I am testing right now.

I don't know why this bug does not show up with the regular the juce_linux_Messaging.cpp , I believe it should also happen.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby valley » Tue Jul 21, 2009 2:36 pm

jpo wrote:
I don't know why this bug does not show up with the regular the juce_linux_Messaging.cpp , I believe it should also happen.


I actually think I may have seen it happen, but it was on an old Mandrake box now retired. At the time it was one of those "I've never seen this happen before, and if I never see it happen again then I'm going to pretend I didn't see it happen here" moments.

Since it only ever happened once, I chose not to care. IIRC, and this was a while ago, a list box was partially drawn at some arbitrary location on screen. Causing a repaint made it go away, and that was end of it.

<shrug>
valley
JUCE UberWeenie
 
Posts: 678
Joined: Sat Sep 04, 2004 4:32 am
Location: Chicago

Postby jpo » Tue Jul 21, 2009 2:59 pm

Ah, good to know that it does happen :)
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby jules » Tue Jul 21, 2009 3:07 pm

Thanks - let me know if your fix seems to be stable, and I'll add it.
User avatar
jules
Fearless Leader
 
Posts: 17204
Joined: Mon Sep 06, 2004 9:03 am
Location: London, UK

Postby jpo » Wed Jul 22, 2009 6:09 pm

As a side note, I now believe that this issue was due to the non-threadsafe use of X11 functions -- as far as I understand, when more that one thread may simultaneously use the same 'display*' object, each X function call should be protected by XLockDisplay()/XUnlockDisplay() . By default these function do nothing, except when XInitThreads has been called before XOpenDisplay


Mmmm now I changed my mind, it looks like, in a perfect world, calling XInitThread *is* sufficient, and XLockDisplay / XUnlockDisplay should be used only when one need to execute atomically a block of X calls. The locking issues I am seeing on some machines (a debian lenny for example) are due to bugs in XCB 1.1, which is unfortunately the version used by debian lenny, and ubuntu 8.04 ..

https://bugs.freedesktop.org/show_bug.cgi?id=16617
http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=463159

Sometimes it is also causing a crash, just like in:
https://bugs.launchpad.net/ubuntu/+sour ... comments/3

(in case of crash (very very hard to reproduce indeed, I have seen it two or three times only so far), the backtrace shows that it is due to the call to MessageManager::inactivityCheckCallback(); in InternalTimerThread, which ends up calling XQueryPointer ).

I don't understand why debian lenny and ubuntu LTQ 8.04 are still using xcb 1.1 since it is so much broken.
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby X-Ryl669 » Mon Nov 16, 2009 2:33 pm

So finally, does the code on the first post works ?
X-Ryl669
X-Ryl669
JUCE UberWeenie
 
Posts: 1124
Joined: Sun Apr 24, 2005 5:30 pm

Postby jpo » Mon Nov 16, 2009 3:29 pm

Yes I've been using it since then, and got no issue (except the bugs in xcb)
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby X-Ryl669 » Mon Nov 16, 2009 4:32 pm

Ok.

I still don't understand how you use the "non X" code.
Do I have to start an instance of JUCEApplication, or do I need to call initialiseJUCE_nonGUI followed by a MessageManager::getInstance() ?

A quick example will help me a lot.
X-Ryl669
X-Ryl669
JUCE UberWeenie
 
Posts: 1124
Joined: Sun Apr 24, 2005 5:30 pm

Postby jpo » Mon Nov 16, 2009 9:04 pm

Yes both methods should work. I'm using it inside a JUCEApplication instance (so the initialiseJUCE_GUI() is being called)
jpo
JUCE UberWeenie
 
Posts: 336
Joined: Thu Mar 20, 2008 2:45 pm

Postby kraken » Tue Nov 17, 2009 10:01 am

i'm interested in this too. i'll take a look and see if i can drop in replacement this with ease.

is the problems with XSHM and redraws still here ?
Image
User avatar
kraken
JUCE UberWeenie
 
Posts: 1063
Joined: Wed Feb 09, 2005 10:31 am
Location: Venice, Italy

Next

Return to Linux

Who is online

Users browsing this forum: No registered users and 0 guests