root/src/add-ons/kernel/generic/tty/tty.cpp
/*
 * Copyright 2007-2010, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Copyright 2004-2009, Axel Dörfler, axeld@pinc-software.de.
 * Distributed under the terms of the MIT License.
 */


#include "tty_private.h"

#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <unistd.h>

#include <util/AutoLock.h>
#include <util/kernel_cpp.h>

#include <team.h>

#include <tty.h>


//#define TTY_TRACE
#ifdef TTY_TRACE
#       define TRACE(x) dprintf x
#else
#       define TRACE(x) ;
#endif


/*
        Locking
        -------

        There are three locks involved. If more than one needs to be held at a
        time, they must be acquired in the order they are listed here.

        gTTYCookieLock: Guards the access to the fields
        tty_cookie::{thread_count,closed}, or more precisely makes access to them
        atomic. thread_count is the number of threads currently using the cookie
        (i.e. read(), write(), ioctl() operations in progress). Together with
        blocking_semaphore this serves the purpose to make sure that all pending
        operations are done at a certain point when closing a cookie
        (cf. tty_close_cookie() and TTYReference).

        tty::lock: Guards the access to tty::{input_buffer,settings::{termios,
        window_size,pgrp_id}}. Moreover when held guarantees that tty::open_count
        won't drop to zero. A tty and the tty connected to it (master and slave)
        share the same lock.

        gTTYRequestLock: Guards access to tty::{reader,writer}_queue (most
        RequestQueue methods do the locking themselves (the lock is a
        recursive_lock)), queued Requests and associated RequestOwners.


        Reading/Writing
        ---------------

        Most of the dirty work when dealing with reading/writing is done by the
        {Reader,Writer}Locker classes. Upon construction they lock the tty,
        (tty::lock) create a RequestOwner and queue Requests in the respective
        reader/writer queues (tty::{reader,writer}_queue). The
        Acquire{Reader,Writer}() methods need to be called before being allowed to
        read/write. They ensure that there is actually something to read/space for
        writing -- in blocking mode they wait, if necessary. When destroyed the
        {Reader,Writer}Locker() remove the formerly enqueued Requests and notify
        waiting reader/writer and/or send out select events, whatever is appropiate.

        Acquire{Reader,Writer}() never return without an actual event being
        occurred. Either an error has occurred (return value) -- in this case the
        caller should terminate -- or bytes are available for reading/space for
        writing (cf. AvailableBytes()).
*/


static void tty_notify_select_event(struct tty* tty, uint8 event);
static void tty_notify_if_available(struct tty* tty, struct tty* otherTTY,
        bool notifySelect);


class AbstractLocker {
public:
        AbstractLocker(tty_cookie* cookie)
                :
                fCookie(cookie),
                fBytes(0)
        {
        }

        size_t AvailableBytes() const
                { return fBytes; }

protected:
        void Lock()
                { recursive_lock_lock(fCookie->tty->lock); }
        void Unlock()
                { recursive_lock_unlock(fCookie->tty->lock); }

        tty_cookie*     fCookie;
        size_t          fBytes;
};


class WriterLocker : public AbstractLocker {
public:
                                                                WriterLocker(tty_cookie* sourceCookie);
                                                                ~WriterLocker();

                        status_t                        AcquireWriter(bool dontBlock,
                                                                        size_t bytesNeeded);

private:
                        size_t                          _CheckAvailableBytes() const;
                        status_t                        _CheckBackgroundWrite() const;

                        struct tty*                     fSource;
                        struct tty*                     fTarget;
                        RequestOwner            fRequestOwner;
                        bool                            fEcho;
};


class ReaderLocker : public AbstractLocker {
public:
                                                                ReaderLocker(tty_cookie* cookie);
                                                                ~ReaderLocker();

                        status_t                        AcquireReader(bigtime_t timeout,
                                                                        size_t bytesNeeded);

private:
                        size_t                          _CheckAvailableBytes() const;
                        status_t                        _CheckBackgroundRead() const;

                        struct tty*                     fTTY;
                        RequestOwner            fRequestOwner;
};


class TTYReferenceLocking {
public:
        inline bool Lock(tty_cookie* cookie)
        {
                MutexLocker _(gTTYCookieLock);

                if (cookie->closed)
                        return false;

                cookie->thread_count++;

                return true;
        }

        inline void Unlock(tty_cookie* cookie)
        {
                MutexLocker locker(gTTYCookieLock);

                sem_id semaphore = -1;
                if (--cookie->thread_count == 0 && cookie->closed)
                        semaphore = cookie->blocking_semaphore;

                locker.Unlock();

                if (semaphore >= 0) {
                        TRACE(("TTYReference: cookie %p closed, last operation done, "
                                "releasing blocking sem %" B_PRId32 "\n", cookie, semaphore));

                        release_sem(semaphore);
                }
        }
};

typedef AutoLocker<tty_cookie, TTYReferenceLocking> TTYReference;


// #pragma mark -


Request::Request()
        :
        fOwner(NULL),
        fCookie(NULL),
        fBytesNeeded(0),
        fNotified(false),
        fError(false)
{
}


void
Request::Init(RequestOwner* owner, tty_cookie* cookie, size_t bytesNeeded)
{
        fOwner = owner;
        fCookie = cookie;
        fBytesNeeded = bytesNeeded;
        fNotified = false;
        fError = false;
}


void
Request::Notify(size_t bytesAvailable)
{
        if (!fNotified && bytesAvailable >= fBytesNeeded && fOwner) {
                fOwner->Notify(this);
                fNotified = true;
        }
}


void
Request::NotifyError(status_t error)
{
        if (!fError && fOwner) {
                fOwner->NotifyError(this, error);
                fError = true;
                fNotified = true;
        }
}


void
Request::Dump(const char* prefix)
{
        kprintf("%srequest: %p\n", prefix, this);
        kprintf("%s  owner:        %p\n", prefix, fOwner);
        kprintf("%s  cookie:       %p\n", prefix, fCookie);
        kprintf("%s  bytes needed: %lu\n", prefix, fBytesNeeded);
        kprintf("%s  notified:     %s\n", prefix, fNotified ? "true" : "false");
        kprintf("%s  error:        %s\n", prefix, fError ? "true" : "false");
}


// #pragma mark -


RequestQueue::RequestQueue()
        :
        fRequests()
{
}


void
RequestQueue::Add(Request* request)
{
        if (request) {
                RecursiveLocker _(gTTYRequestLock);

                fRequests.Add(request, true);
        }
}


void
RequestQueue::Remove(Request* request)
{
        if (request) {
                RecursiveLocker _(gTTYRequestLock);

                fRequests.Remove(request);
        }
}


void
RequestQueue::NotifyFirst(size_t bytesAvailable)
{
        RecursiveLocker _(gTTYRequestLock);

        if (Request* first = First())
                first->Notify(bytesAvailable);
}


void
RequestQueue::NotifyError(status_t error)
{
        RecursiveLocker _(gTTYRequestLock);

        for (RequestList::Iterator it = fRequests.GetIterator(); it.HasNext();) {
                Request* request = it.Next();
                request->NotifyError(error);
        }
}


void
RequestQueue::NotifyError(tty_cookie* cookie, status_t error)
{
        RecursiveLocker _(gTTYRequestLock);

        for (RequestList::Iterator it = fRequests.GetIterator(); it.HasNext();) {
                Request* request = it.Next();
                if (request->TTYCookie() == cookie)
                        request->NotifyError(error);
        }
}


void
RequestQueue::Dump(const char* prefix)
{
        RequestList::Iterator it = fRequests.GetIterator();
        while (Request* request = it.Next())
                request->Dump(prefix);
}


// #pragma mark -


RequestOwner::RequestOwner()
        :
        fConditionVariable(NULL),
        fCookie(NULL),
        fError(B_OK),
        fBytesNeeded(1)
{
        fRequestQueues[0] = NULL;
        fRequestQueues[1] = NULL;
}


/*!     The caller must already hold the request lock.
*/
void
RequestOwner::Enqueue(tty_cookie* cookie, RequestQueue* queue1,
        RequestQueue* queue2)
{
        TRACE(("%p->RequestOwner::Enqueue(%p, %p, %p)\n", this, cookie, queue1,
                queue2));

        fCookie = cookie;

        fRequestQueues[0] = queue1;
        fRequestQueues[1] = queue2;

        fRequests[0].Init(this, cookie, fBytesNeeded);
        if (queue1)
                queue1->Add(&fRequests[0]);
        else
                fRequests[0].Notify(fBytesNeeded);

        fRequests[1].Init(this, cookie, fBytesNeeded);
        if (queue2)
                queue2->Add(&fRequests[1]);
        else
                fRequests[1].Notify(fBytesNeeded);
}


/*!     The caller must already hold the request lock.
*/
void
RequestOwner::Dequeue()
{
        TRACE(("%p->RequestOwner::Dequeue()\n", this));

        if (fRequestQueues[0])
                fRequestQueues[0]->Remove(&fRequests[0]);
        if (fRequestQueues[1])
                fRequestQueues[1]->Remove(&fRequests[1]);

        fRequestQueues[0] = NULL;
        fRequestQueues[1] = NULL;
}


void
RequestOwner::SetBytesNeeded(size_t bytesNeeded)
{
        if (fRequestQueues[0])
                fRequests[0].Init(this, fCookie, bytesNeeded);

        if (fRequestQueues[1])
                fRequests[1].Init(this, fCookie, bytesNeeded);
}


/*!     The request lock MUST NOT be held!
*/
status_t
RequestOwner::Wait(bool interruptable, bigtime_t timeout)
{
        TRACE(("%p->RequestOwner::Wait(%d)\n", this, interruptable));

        status_t error = B_OK;

        RecursiveLocker locker(gTTYRequestLock);

        // check, if already done
        if (fError == B_OK
                && (!fRequests[0].WasNotified() || !fRequests[1].WasNotified())) {
                // not yet done

                // publish the condition variable
                ConditionVariable conditionVariable;
                conditionVariable.Init(this, "tty request");
                fConditionVariable = &conditionVariable;

                // add an entry to wait on
                ConditionVariableEntry entry;
                conditionVariable.Add(&entry);

                locker.Unlock();

                // wait
                TRACE(("%p->RequestOwner::Wait(): waiting for condition...\n", this));

                error = entry.Wait(
                        (interruptable ? B_CAN_INTERRUPT : 0) | B_RELATIVE_TIMEOUT,
                        timeout);

                TRACE(("%p->RequestOwner::Wait(): condition occurred: %" B_PRIx32 "\n",
                        this, error));

                // remove the condition variable
                locker.Lock();
                fConditionVariable = NULL;
        }

        // get the result
        if (error == B_OK)
                error = fError;

        return error;
}


bool
RequestOwner::IsFirstInQueues()
{
        RecursiveLocker locker(gTTYRequestLock);

        for (int i = 0; i < 2; i++) {
                if (fRequestQueues[i] && fRequestQueues[i]->First() != &fRequests[i])
                        return false;
        }

        return true;
}


void
RequestOwner::Notify(Request* request)
{
        TRACE(("%p->RequestOwner::Notify(%p)\n", this, request));

        if (fError == B_OK && !request->WasNotified()) {
                bool notify = false;

                if (&fRequests[0] == request) {
                        notify = fRequests[1].WasNotified();
                } else if (&fRequests[1] == request) {
                        notify = fRequests[0].WasNotified();
                } else {
                        // spurious call
                }

                if (notify && fConditionVariable)
                        fConditionVariable->NotifyOne();
        }
}


void
RequestOwner::NotifyError(Request* request, status_t error)
{
        TRACE(("%p->RequestOwner::NotifyError(%p, %" B_PRIx32 ")\n", this, request,
                error));

        if (fError == B_OK) {
                fError = error;

                if (!fRequests[0].WasNotified() || !fRequests[1].WasNotified()) {
                        if (fConditionVariable)
                                fConditionVariable->NotifyOne();
                }
        }
}


// #pragma mark -


WriterLocker::WriterLocker(tty_cookie* sourceCookie)
        :
        AbstractLocker(sourceCookie),
        fSource(fCookie->tty),
        fTarget(fCookie->other_tty),
        fRequestOwner(),
        fEcho(false)
{
        Lock();

        // Now that the tty pair is locked, we can check, whether the target is
        // open at all.
        if (fTarget->open_count > 0) {
                // The target tty is open. As soon as we have appended a request to
                // the writer queue of the target, it is guaranteed to remain valid
                // until we have removed the request (and notified the
                // tty_close_cookie() pseudo request).

                // get the echo mode
                fEcho = (fSource->is_master
                        && fSource->settings->termios.c_lflag & ECHO) != 0;

                // enqueue ourselves in the respective request queues
                RecursiveLocker locker(gTTYRequestLock);
                fRequestOwner.Enqueue(fCookie, &fTarget->writer_queue,
                        (fEcho ? &fSource->writer_queue : NULL));
        } else {
                // target is not open: we set it to NULL; all further operations on
                // this locker will fail
                fTarget = NULL;
        }
}


WriterLocker::~WriterLocker()
{
        // dequeue from request queues
        RecursiveLocker locker(gTTYRequestLock);
        fRequestOwner.Dequeue();

        // check the tty queues and notify the next in line, and send out select
        // events
        if (fTarget)
                tty_notify_if_available(fTarget, fSource, true);
        if (fEcho)
                tty_notify_if_available(fSource, fTarget, true);

        locker.Unlock();

        Unlock();
}


size_t
WriterLocker::_CheckAvailableBytes() const
{
        size_t writable = line_buffer_writable(fTarget->input_buffer);
        if (fEcho) {
                // we can only write as much as is available on both ends
                size_t locallyWritable = line_buffer_writable(fSource->input_buffer);
                if (locallyWritable < writable)
                        writable = locallyWritable;
        }
        return writable;
}


status_t
WriterLocker::AcquireWriter(bool dontBlock, size_t bytesNeeded)
{
        if (!fTarget)
                return B_FILE_ERROR;
        if (fEcho && fCookie->closed)
                return B_FILE_ERROR;

        RecursiveLocker requestLocker(gTTYRequestLock);

        // check, if we're first in queue, and if there is space to write
        if (fRequestOwner.IsFirstInQueues()) {
                fBytes = _CheckAvailableBytes();
                if (fBytes >= bytesNeeded)
                        return B_OK;
        }

        // We are not the first in queue or currently there's no space to write:
        // bail out, if we shall not block.
        if (dontBlock)
                return B_WOULD_BLOCK;

        // set the number of bytes we need and notify, just in case we're first in
        // one of the queues (RequestOwner::SetBytesNeeded() resets the notification
        // state)
        fRequestOwner.SetBytesNeeded(bytesNeeded);
        if (fTarget)
                tty_notify_if_available(fTarget, fSource, false);
        if (fEcho)
                tty_notify_if_available(fSource, fTarget, false);

        requestLocker.Unlock();

        // block until something happens
        Unlock();
        status_t status = fRequestOwner.Wait(true);
        Lock();

        // RequestOwner::Wait() returns the error, but to avoid a race condition
        // when closing a tty, we re-get the error with the tty lock being held.
        if (status == B_OK) {
                RecursiveLocker _(gTTYRequestLock);
                status = fRequestOwner.Error();
        }

        if (status == B_OK)
                status = _CheckBackgroundWrite();

        if (status == B_OK)
                fBytes = _CheckAvailableBytes();

        return status;
}


status_t
WriterLocker::_CheckBackgroundWrite() const
{
        // only relevant for the slave end and only when TOSTOP is set
        if (fSource->is_master
                || (fSource->settings->termios.c_lflag & TOSTOP) == 0) {
                return B_OK;
        }

        pid_t processGroup = getpgid(0);
        if (fSource->settings->pgrp_id != 0
                        && processGroup != fSource->settings->pgrp_id) {
                if (team_get_controlling_tty() == fSource)
                        send_signal(-processGroup, SIGTTOU);
        }

        return B_OK;
}


//      #pragma mark -


ReaderLocker::ReaderLocker(tty_cookie* cookie)
        :
        AbstractLocker(cookie),
        fTTY(cookie->tty),
        fRequestOwner()
{
        Lock();

        // enqueue ourselves in the reader request queue
        RecursiveLocker locker(gTTYRequestLock);
        fRequestOwner.Enqueue(fCookie, &fTTY->reader_queue);
}


ReaderLocker::~ReaderLocker()
{
        // dequeue from reader request queue
        RecursiveLocker locker(gTTYRequestLock);
        fRequestOwner.Dequeue();

        // check the tty queues and notify the next in line, and send out select
        // events
        struct tty* otherTTY = fCookie->other_tty;
        tty_notify_if_available(fTTY, (otherTTY->open_count > 0 ? otherTTY : NULL),
                true);

        locker.Unlock();

        Unlock();
}


status_t
ReaderLocker::AcquireReader(bigtime_t timeout, size_t bytesNeeded)
{
        if (fCookie->closed)
                return B_FILE_ERROR;

        status_t status = _CheckBackgroundRead();
        if (status != B_OK)
                return status;

        // check, if we're first in queue, and if there is something to read
        if (fRequestOwner.IsFirstInQueues()) {
                fBytes = _CheckAvailableBytes();
                if (fBytes >= bytesNeeded)
                        return B_OK;
        }

        if (fCookie->other_tty->open_count == 0
                && fCookie->other_tty->opened_count > 0) {
                TRACE(("ReaderLocker::AcquireReader() opened_count %" B_PRId32 "\n",
                        fCookie->other_tty->opened_count));
                return B_FILE_ERROR;
        }

        // We are not the first in queue or currently there's nothing to read:
        // bail out, if we shall not block.
        if (timeout <= 0)
                return B_WOULD_BLOCK;

        // reset the number of bytes we need
        fRequestOwner.SetBytesNeeded(bytesNeeded);

        // block until something happens
        Unlock();
        status = fRequestOwner.Wait(true, timeout);
        Lock();

        if (status == B_OK)
                status = _CheckBackgroundRead();

        fBytes = _CheckAvailableBytes();

        TRACE(("ReaderLocker::AcquireReader() ended status 0x%" B_PRIx32 "\n",
                status));

        return status;
}


size_t
ReaderLocker::_CheckAvailableBytes() const
{
        // Reading from the slave with canonical input processing enabled means
        // that we read at max until hitting a line end or EOF.
        if (!fTTY->is_master && (fTTY->settings->termios.c_lflag & ICANON) != 0) {
                return line_buffer_readable_line(fTTY->input_buffer,
                        fTTY->settings->termios.c_cc[VEOL],
                        fTTY->settings->termios.c_cc[VEOF]);
        }

        return line_buffer_readable(fTTY->input_buffer);
}


status_t
ReaderLocker::_CheckBackgroundRead() const
{
        // only relevant for the slave end
        if (fTTY->is_master)
                return B_OK;

        pid_t processGroup = getpgid(0);
        if (fTTY->settings->pgrp_id != 0
                        && processGroup != fTTY->settings->pgrp_id) {
                if (team_get_controlling_tty() == fTTY)
                        send_signal(-processGroup, SIGTTIN);
        }

        return B_OK;
}


// #pragma mark -


static void
reset_termios(struct termios& termios)
{
        memset(&termios, 0, sizeof(struct termios));

        termios.c_iflag = ICRNL;
        termios.c_oflag = OPOST | ONLCR;
        termios.c_cflag = B19200 | CS8 | CREAD | HUPCL;
                // enable receiver, hang up on last close
        termios.c_lflag = ECHO | ISIG | ICANON;
        termios.c_ispeed = 0;
        termios.c_ospeed = 0;
        termios.c_ispeed_high = 0;
        termios.c_ospeed_high = 0;

        // control characters
        termios.c_cc[VINTR] = CTRL('C');
        termios.c_cc[VQUIT] = CTRL('\\');
        termios.c_cc[VERASE] = 0x7f;
        termios.c_cc[VKILL] = CTRL('U');
        termios.c_cc[VEOF] = CTRL('D');
        termios.c_cc[VEOL] = '\0';
        termios.c_cc[VEOL2] = '\0';
        termios.c_cc[VSTART] = CTRL('S');
        termios.c_cc[VSTOP] = CTRL('Q');
        termios.c_cc[VSUSP] = CTRL('Z');
}


static void
reset_tty_settings(tty_settings& settings)
{
        reset_termios(settings.termios);

        settings.pgrp_id = 0;
                // this value prevents any signal of being sent
        settings.session_id = -1;

        // some initial window size - the TTY in question should set these values
        settings.window_size.ws_col = 80;
        settings.window_size.ws_row = 25;
        settings.window_size.ws_xpixel = settings.window_size.ws_col * 8;
        settings.window_size.ws_ypixel = settings.window_size.ws_row * 8;
}


/*!     Processes the input character and puts it into the TTY's input buffer.
        Depending on the termios flags set, signals may be sent, the input
        character changed or removed, etc.
*/
static void
tty_input_putc_locked(struct tty* tty, int c)
{
        const termios& termios = tty->settings->termios;

        // process signals if needed
        if ((termios.c_lflag & ISIG) != 0) {
                // enable signals, process INTR, QUIT, and SUSP
                int signal = -1;

                if (c == termios.c_cc[VINTR])
                        signal = SIGINT;
                else if (c == termios.c_cc[VQUIT])
                        signal = SIGQUIT;
                else if (c == termios.c_cc[VSUSP])
                        signal = SIGTSTP;

                // do we need to deliver a signal?
                if (signal != -1) {
                        // we may have to flush the input buffer
                        if ((termios.c_lflag & NOFLSH) == 0)
                                clear_line_buffer(tty->input_buffer);

                        if (tty->settings->pgrp_id != 0)
                                send_signal(-tty->settings->pgrp_id, signal);
                        return;
                }
        }

        // process special canonical input characters
        if ((termios.c_lflag & ICANON) != 0) {
                // canonical mode, process ERASE and KILL
                const cc_t* controlChars = termios.c_cc;

                if (c == controlChars[VERASE]) {
                        // erase one character
                        char lastChar;
                        if (line_buffer_tail_getc(tty->input_buffer, &lastChar)) {
                                if (lastChar == controlChars[VEOF]
                                        || lastChar == controlChars[VEOL]
                                        || lastChar == '\n' || lastChar == '\r') {
                                        // EOF or end of line -- put it back
                                        line_buffer_putc(tty->input_buffer, lastChar);
                                }
                        }
                        return;
                } else if (c == controlChars[VKILL]) {
                        // erase line
                        char lastChar;
                        while (line_buffer_tail_getc(tty->input_buffer, &lastChar)) {
                                if (lastChar == controlChars[VEOF]
                                        || lastChar == controlChars[VEOL]
                                        || lastChar == '\n' || lastChar == '\r') {
                                        // EOF or end of line -- put it back
                                        line_buffer_putc(tty->input_buffer, lastChar);
                                        break;
                                }
                        }
                        return;
                } else if (c == controlChars[VEOF]) {
                        // we still write the EOF to the stream -- tty_input_read() needs
                        // to recognize it
                        tty->pending_eof++;
                }
        }

        // Input character conversions have already been done. What reaches this
        // point can directly be written to the line buffer.

        line_buffer_putc(tty->input_buffer, c);
}


static int32
tty_readable(struct tty* tty)
{
        if (!tty->is_master && (tty->settings->termios.c_lflag & ICANON) != 0) {
                return line_buffer_readable_line(tty->input_buffer,
                        tty->settings->termios.c_cc[VEOL],
                        tty->settings->termios.c_cc[VEOF]);
        }

        return line_buffer_readable(tty->input_buffer);
}


/** \brief Notify anyone waiting on events for this TTY.
 *
 * The TTY lock must be held.
 *
 * Otherwise, the select_pool may be modified or deleted (which happens automatically when the last
 * item is removed from it).
 */
static void
tty_notify_select_event(struct tty* tty, uint8 event)
{
        TRACE(("tty_notify_select_event(%p, %u)\n", tty, event));

        if (tty->select_pool)
                notify_select_event_pool(tty->select_pool, event);
}


/*!     \brief Checks whether bytes can be read from/written to the line buffer of
                   the given TTY and notifies the respective queues.

        Also sends out \c B_SELECT_READ and \c B_SELECT_WRITE events as needed.

        The TTY and the request lock must be held.

        \param tty The TTY.
        \param otherTTY The connected TTY.
*/
static void
tty_notify_if_available(struct tty* tty, struct tty* otherTTY,
        bool notifySelect)
{
        if (!tty)
                return;

        // Check, if something is readable (depending on whether canonical input
        // processing is enabled).
        int32 readable = tty_readable(tty);
        if (readable > 0) {
                // if nobody is waiting send select events, otherwise notify the waiter
                if (!tty->reader_queue.IsEmpty())
                        tty->reader_queue.NotifyFirst(readable);
                else if (notifySelect)
                        tty_notify_select_event(tty, B_SELECT_READ);
        }

        int32 writable = line_buffer_writable(tty->input_buffer);
        if (writable > 0) {
                // if nobody is waiting send select events, otherwise notify the waiter
                if (!tty->writer_queue.IsEmpty()) {
                        tty->writer_queue.NotifyFirst(writable);
                } else if (notifySelect) {
                        if (otherTTY && otherTTY->open_count > 0)
                                tty_notify_select_event(otherTTY, B_SELECT_WRITE);
                }
        }
}


/*!     \brief Performs input character conversion and writes the result to
                \a buffer.
        \param tty The master tty.
        \param c The input character.
        \param buffer The buffer to which to write the converted character.
        \param _bytesNeeded The number of bytes needed in the target tty's
                line buffer.
        \return \c true, if the character shall be processed further, \c false, if
                it shall be skipped.
*/
static bool
process_input_char(struct tty* tty, char c, char* buffer,
        size_t* _bytesNeeded)
{
        const termios& termios = tty->settings->termios;
        tcflag_t flags = termios.c_iflag;

        // signals
        if (termios.c_lflag & ISIG) {
                if (c == termios.c_cc[VINTR]
                        || c == termios.c_cc[VQUIT]
                        || c == termios.c_cc[VSUSP]) {
                        *buffer = c;
                        *_bytesNeeded = 0;
                        return true;
                }
        }

        // canonical input characters
        if (termios.c_lflag & ICANON) {
                if (c == termios.c_cc[VERASE]
                        || c == termios.c_cc[VKILL]) {
                        *buffer = c;
                        *_bytesNeeded = 0;
                        return true;
                }
        }

        // convert chars
        if (c == '\r') {
                if (flags & IGNCR)              // ignore CR
                        return false;
                if (flags & ICRNL)              // CR -> NL
                        c = '\n';
        } else if (c == '\n') {
                if (flags & INLCR)              // NL -> CR
                        c = '\r';
        } else if ((flags & ISTRIP)     != 0)   // strip off eighth bit
                c &= 0x7f;

        *buffer = c;
        *_bytesNeeded = 1;
        return true;
}


/*!     \brief Performs output character conversion and writes the result to
                \a buffer.
        \param tty The master tty.
        \param c The output character.
        \param buffer The buffer to which to write the converted character(s).
        \param _bytesWritten The number of bytes written to the output buffer
                (max 3).
        \param echoed \c true if the output char to be processed has been echoed
                from the input.
*/
static void
process_output_char(struct tty* tty, char c, char* buffer,
        size_t* _bytesWritten, bool echoed)
{
        const termios& termios = tty->settings->termios;
        tcflag_t flags = termios.c_oflag;

        if (flags & OPOST) {
                if (echoed && c == termios.c_cc[VERASE]) {
                        if (termios.c_lflag & ECHOE) {
                                // ERASE -> BS SPACE BS
                                buffer[0] = CTRL('H');
                                buffer[1] = ' ';
                                buffer[2] = CTRL('H');;
                                *_bytesWritten = 3;
                                return;
                        }
                } else if (echoed && c == termios.c_cc[VKILL]) {
                        if (!(termios.c_lflag & ECHOK)) {
                                // don't echo KILL
                                *_bytesWritten = 0;
                                return;
                        }
                } else if (echoed && c == termios.c_cc[VEOF]) {
                        // don't echo EOF
                        *_bytesWritten = 0;
                        return;
                } else if (c == '\n') {
                        if (echoed && !(termios.c_lflag & ECHONL)) {
                                // don't echo NL
                                *_bytesWritten = 0;
                                return;
                        }
                        if (flags & ONLCR) {                    // NL -> CR-NL
                                buffer[0] = '\r';
                                buffer[1] = '\n';
                                *_bytesWritten = 2;
                                return;
                        }
                } else if (c == '\r') {
                        if (flags & OCRNL) {                    // CR -> NL
                                c = '\n';
                        } else if (flags & ONLRET) {    // NL also does RET, ignore CR
                                *_bytesWritten = 0;
                                return;
                        } else if (flags & ONOCR) {             // don't output CR at column 0
                                // TODO: We can't decide that here.
                        }
                } else {
                        if (flags & OLCUC)                              // lower case -> upper case
                                c = toupper(c);
                }
        }

        *buffer = c;
        *_bytesWritten = 1;
}


static status_t
tty_write_to_tty_master_unsafe(tty_cookie* sourceCookie, const char* data,
        size_t* _length)
{
        struct tty* source = sourceCookie->tty;
        struct tty* target = sourceCookie->other_tty;
        size_t length = *_length;
        size_t bytesWritten = 0;
        uint32 mode = sourceCookie->open_mode;
        bool dontBlock = (mode & O_NONBLOCK) != 0;

        // bail out, if source is already closed
        TTYReference sourceTTYReference(sourceCookie);
        if (!sourceTTYReference.IsLocked())
                return B_FILE_ERROR;

        if (length == 0)
                return B_OK;

        WriterLocker locker(sourceCookie);

        // if the target is not open, fail now
        if (target->open_count <= 0)
                return B_FILE_ERROR;

        bool echo = (source->settings->termios.c_lflag & ECHO) != 0;

        TRACE(("tty_write_to_tty_master(source = %p, target = %p, "
                "length = %lu%s)\n", source, target, length,
                (echo ? ", echo mode" : "")));

        // Make sure we are first in the writer queue(s) and AvailableBytes() is
        // initialized.
        status_t status = locker.AcquireWriter(dontBlock, 0);
        if (status != B_OK) {
                *_length = 0;
                return status;
        }
        size_t writable = locker.AvailableBytes();
        size_t writtenSinceLastNotify = 0;

        while (bytesWritten < length) {
                // fetch next char and do input processing
                char c;
                size_t bytesNeeded;
                if (!process_input_char(source, *data, &c, &bytesNeeded)) {
                        // input char shall be skipped
                        data++;
                        bytesWritten++;
                        continue;
                }

                // If in echo mode, we do the output conversion and need to update
                // the needed bytes count.
                char echoBuffer[3];
                size_t echoBytes = 0;
                if (echo) {
                        process_output_char(source, c, echoBuffer, &echoBytes, true);
                        if (echoBytes > bytesNeeded)
                                bytesNeeded = echoBytes;
                }

                // If there's not enough space to write what we have, we need to wait
                // until it is available.
                if (writable < bytesNeeded) {
                        if (writtenSinceLastNotify > 0) {
                                tty_notify_if_available(target, source, true);
                                if (echo)
                                        tty_notify_if_available(source, target, true);
                                writtenSinceLastNotify = 0;
                        }

                        status = locker.AcquireWriter(dontBlock, bytesNeeded);
                        if (status != B_OK) {
                                *_length = bytesWritten;
                                return *_length == 0 ? status : B_OK;
                        }

                        writable = locker.AvailableBytes();

                        // XXX: do we need to support VMIN & VTIME for write() ?

                        // We need to restart the loop, since the termios flags might have
                        // changed in the meantime (while we've unlocked the tty). Note,
                        // that we don't re-get "echo" -- maybe we should.
                        continue;
                }

                // write the bytes
                tty_input_putc_locked(target, c);

                if (echo) {
                        for (size_t i = 0; i < echoBytes; i++)
                                line_buffer_putc(source->input_buffer, echoBuffer[i]);
                }

                writable -= bytesNeeded;
                data++;
                bytesWritten++;
                writtenSinceLastNotify++;
        }

        return B_OK;
}


static status_t
tty_write_to_tty_slave_unsafe(tty_cookie* sourceCookie, const char* data,
        size_t* _length)
{
        struct tty* target = sourceCookie->other_tty;
        size_t length = *_length;
        size_t bytesWritten = 0;
        uint32 mode = sourceCookie->open_mode;
        bool dontBlock = (mode & O_NONBLOCK) != 0;

        // bail out, if source is already closed
        TTYReference sourceTTYReference(sourceCookie);
        if (!sourceTTYReference.IsLocked())
                return B_FILE_ERROR;

        if (length == 0)
                return B_OK;

        WriterLocker locker(sourceCookie);

        // if the target is not open, fail now
        if (target->open_count <= 0)
                return B_FILE_ERROR;

        TRACE(("tty_write_to_tty_slave(source = %p, target = %p, length = %lu)\n",
                sourceCookie->tty, target, length));

        // Make sure we are first in the writer queue(s) and AvailableBytes() is
        // initialized.
        status_t status = locker.AcquireWriter(dontBlock, 0);
        if (status != B_OK) {
                *_length = 0;
                return status;
        }
        size_t writable = locker.AvailableBytes();
        size_t writtenSinceLastNotify = 0;

        while (bytesWritten < length) {
                // fetch next char and do output processing
                char buffer[3];
                size_t bytesNeeded;
                process_output_char(target, *data, buffer, &bytesNeeded, false);

                // If there's not enough space to write what we have, we need to wait
                // until it is available.
                if (writable < bytesNeeded) {
                        if (writtenSinceLastNotify > 0) {
                                tty_notify_if_available(target, sourceCookie->tty, true);
                                writtenSinceLastNotify = 0;
                        }

                        status = locker.AcquireWriter(dontBlock, bytesNeeded);
                        if (status != B_OK) {
                                *_length = bytesWritten;
                                return *_length == 0 ? status : B_OK;
                        }

                        writable = locker.AvailableBytes();

                        // We need to restart the loop, since the termios flags might have
                        // changed in the meantime (while we've unlocked the tty).
                        continue;
                }

                // write the bytes
                for (size_t i = 0; i < bytesNeeded; i++)
                        line_buffer_putc(target->input_buffer, buffer[i]);

                writable -= bytesNeeded;
                data++;
                bytesWritten++;
                writtenSinceLastNotify++;
        }

        return B_OK;
}


static status_t
tty_write_to_tty_master(tty_cookie* sourceCookie, const void* _buffer,
        size_t* _length)
{
        const char* buffer = (const char*)_buffer;
        size_t bytesRemaining = *_length;
        *_length = 0;

        while (bytesRemaining > 0) {
                // copy data to stack
                char safeBuffer[256];
                size_t toWrite = min_c(sizeof(safeBuffer), bytesRemaining);
                status_t error = user_memcpy(safeBuffer, buffer, toWrite);
                if (error != B_OK)
                        return error;

                // write them
                size_t written = toWrite;
                error = tty_write_to_tty_master_unsafe(sourceCookie, safeBuffer,
                        &written);
                if (error != B_OK)
                        return error;

                buffer += written;
                bytesRemaining -= written;
                *_length += written;

                if (written < toWrite)
                        return B_OK;
        }

        return B_OK;
}


static status_t
tty_write_to_tty_slave(tty_cookie* sourceCookie, const void* _buffer,
        size_t* _length)
{
        const char* buffer = (const char*)_buffer;
        size_t bytesRemaining = *_length;
        *_length = 0;

        while (bytesRemaining > 0) {
                // copy data to stack
                char safeBuffer[256];
                size_t toWrite = min_c(sizeof(safeBuffer), bytesRemaining);
                status_t status = user_memcpy(safeBuffer, buffer, toWrite);
                if (status != B_OK)
                        return status;

                // write them
                size_t written = toWrite;
                status = tty_write_to_tty_slave_unsafe(sourceCookie, safeBuffer,
                        &written);
                if (status != B_OK)
                        return *_length == 0 ? status : B_OK;

                buffer += written;
                bytesRemaining -= written;
                *_length += written;

                if (written < toWrite)
                        return B_OK;
        }

        return B_OK;
}


// #pragma mark - public API


status_t
tty_create(tty_service_func func, struct tty* master, struct tty** _tty)
{
        struct tty* tty = new(std::nothrow) (struct tty);
        if (tty == NULL)
                return B_NO_MEMORY;

        if (master == NULL) {
                tty->is_master = true;
                tty->lock = new(std::nothrow) recursive_lock;
                tty->settings = new(std::nothrow) tty_settings;
                if (tty->lock == NULL || tty->settings == NULL) {
                        delete tty->lock;
                        delete tty->settings;
                        delete tty;
                        return B_NO_MEMORY;
                }

                recursive_lock_init(tty->lock, "tty lock");
                reset_tty_settings(*tty->settings);
        } else {
                tty->is_master = false;
                tty->lock = master->lock;
                tty->settings = master->settings;
        }

        tty->ref_count = 0;
        tty->open_count = 0;
        tty->opened_count = 0;
        tty->select_pool = NULL;
        tty->pending_eof = 0;
        tty->hardware_bits = 0;
        tty->is_exclusive = false;

        status_t status = init_line_buffer(tty->input_buffer, TTY_BUFFER_SIZE);

        if (status < B_OK) {
                if (tty->is_master) {
                        recursive_lock_destroy(tty->lock);
                        delete tty->lock;
                }
                delete tty;
                return status;
        }

        tty->service_func = func;

        *_tty = tty;

        return B_OK;
}


void
tty_destroy(struct tty* tty)
{
        TRACE(("tty_destroy(%p)\n", tty));
        ASSERT(tty->ref_count == 0);

        uninit_line_buffer(tty->input_buffer);
        delete_select_sync_pool(tty->select_pool);
        if (tty->is_master) {
                recursive_lock_destroy(tty->lock);
                delete tty->lock;
                delete tty->settings;
        }

        delete tty;
}


status_t
tty_create_cookie(struct tty* tty, struct tty* otherTTY, uint32 openMode, tty_cookie** _cookie)
{
        tty_cookie* cookie = new(std::nothrow) tty_cookie;
        if (cookie == NULL)
                return B_NO_MEMORY;

        cookie->blocking_semaphore = create_sem(0, "wait for tty close");
        if (cookie->blocking_semaphore < 0) {
                status_t status = cookie->blocking_semaphore;
                delete cookie;
                return status;
        }

        cookie->tty = tty;
        cookie->other_tty = otherTTY;
        cookie->open_mode = openMode;
        cookie->thread_count = 0;
        cookie->closed = false;


        RecursiveLocker locker(cookie->tty->lock);

        if (tty->is_exclusive && geteuid() != 0) {
                delete_sem(cookie->blocking_semaphore);
                delete cookie;
                return B_BUSY;
        }

        // add to the TTY's cookie list
        tty->cookies.Add(cookie);
        tty->open_count++;
        tty->ref_count++;
        tty->opened_count++;

        *_cookie = cookie;

        return B_OK;
}


void
tty_close_cookie(tty_cookie* cookie)
{
        MutexLocker locker(gTTYCookieLock);

        // Already closed? This can happen for slaves that have been closed when
        // the master was closed.
        if (cookie->closed)
                return;

        // set the cookie's `closed' flag
        cookie->closed = true;
        bool unblock = (cookie->thread_count > 0);

        // unblock blocking threads
        if (unblock) {
                cookie->tty->reader_queue.NotifyError(cookie, B_FILE_ERROR);
                cookie->tty->writer_queue.NotifyError(cookie, B_FILE_ERROR);

                if (cookie->other_tty->open_count > 0) {
                        cookie->other_tty->reader_queue.NotifyError(cookie, B_FILE_ERROR);
                        cookie->other_tty->writer_queue.NotifyError(cookie, B_FILE_ERROR);
                }
        }

        locker.Unlock();

        // wait till all blocking (and now unblocked) threads have left the
        // critical code
        if (unblock) {
                TRACE(("tty_close_cookie(): cookie %p, there're still pending "
                        "operations, acquire blocking sem %" B_PRId32 "\n", cookie,
                        cookie->blocking_semaphore));

                acquire_sem(cookie->blocking_semaphore);
        }

        // For the removal of the cookie acquire the TTY's lock. This ensures, that
        // cookies will not be removed from a TTY (or added -- cf. add_tty_cookie())
        // as long as the TTY's lock is being held.
        RecursiveLocker ttyLocker(cookie->tty->lock);

        // remove the cookie from the TTY's cookie list
        cookie->tty->cookies.Remove(cookie);

        // close the tty, if no longer used
        if (--cookie->tty->open_count == 0) {
                // The last cookie of this tty has been closed. We're going to close
                // the TTY and need to unblock all write requests before. There should
                // be no read requests, since only a cookie of this TTY issues those.
                // We do this by first notifying all queued requests of the error
                // condition. We then clear the line buffer for the TTY and queue
                // an own request.

                // Notify the other TTY first; it doesn't accept any read/writes
                // while there is only one end.
                cookie->other_tty->reader_queue.NotifyError(B_FILE_ERROR);
                cookie->other_tty->writer_queue.NotifyError(B_FILE_ERROR);

                RecursiveLocker requestLocker(gTTYRequestLock);

                // we only need to do all this, if the writer queue is not empty
                if (!cookie->tty->writer_queue.IsEmpty()) {
                        // notify the blocking writers
                        cookie->tty->writer_queue.NotifyError(B_FILE_ERROR);

                        // enqueue our request
                        RequestOwner requestOwner;
                        requestOwner.Enqueue(cookie, &cookie->tty->writer_queue);

                        requestLocker.Unlock();

                        // clear the line buffer
                        clear_line_buffer(cookie->tty->input_buffer);

                        ttyLocker.Unlock();

                        // wait for our turn
                        requestOwner.Wait(false);

                        // re-lock
                        ttyLocker.Lock();
                        requestLocker.Lock();

                        // dequeue our request
                        requestOwner.Dequeue();
                }

                requestLocker.Unlock();

                // notify a select event on the other tty, if we've closed this tty
                tty_notify_select_event(cookie->other_tty, B_SELECT_DISCONNECTED);

                cookie->tty->is_exclusive = false;
        }
}


void
tty_destroy_cookie(tty_cookie* cookie)
{
        RecursiveLocker locker(cookie->tty->lock);
        cookie->tty->ref_count--;
        locker.Unlock();

        if (cookie->blocking_semaphore >= 0)
                delete_sem(cookie->blocking_semaphore);

        delete cookie;
}


status_t
tty_read(tty_cookie* cookie, void* _buffer, size_t* _length)
{
        char* buffer = (char*)_buffer;
        struct tty* tty = cookie->tty;
        uint32 mode = cookie->open_mode;
        bool dontBlock = (mode & O_NONBLOCK) != 0;
        size_t length = *_length;
        bool canon = true;
        bigtime_t timeout = dontBlock ? 0 : B_INFINITE_TIMEOUT;
        bigtime_t interCharTimeout = 0;
        size_t bytesNeeded = 1;

        TRACE(("tty_input_read(tty = %p, length = %lu, mode = %" B_PRIu32 ")\n",
                tty, length, mode));

        if (length == 0)
                return B_OK;

        // bail out, if the TTY is already closed
        TTYReference ttyReference(cookie);
        if (!ttyReference.IsLocked())
                return B_FILE_ERROR;

        ReaderLocker locker(cookie);

        // handle raw mode
        if ((!tty->is_master) && ((tty->settings->termios.c_lflag & ICANON) == 0)) {
                canon = false;
                if (!dontBlock) {
                        // Non-blocking mode. Handle VMIN and VTIME.
                        bytesNeeded = tty->settings->termios.c_cc[VMIN];
                        bigtime_t vtime = tty->settings->termios.c_cc[VTIME] * 100000;
                        TRACE(("tty_input_read: icanon vmin %lu, vtime %" B_PRIdBIGTIME
                                "us\n", bytesNeeded, vtime));

                        if (bytesNeeded == 0) {
                                // In this case VTIME specifies a relative total timeout. We
                                // have no inter-char timeout, though.
                                timeout = vtime;
                        } else {
                                // VTIME specifies the inter-char timeout. 0 is indefinitely.
                                if (vtime == 0)
                                        interCharTimeout = B_INFINITE_TIMEOUT;
                                else
                                        interCharTimeout = vtime;

                                if (bytesNeeded > length)
                                        bytesNeeded = length;
                        }
                }
        }

        status_t status;
        *_length = 0;

        do {
                TRACE(("tty_input_read: AcquireReader(%" B_PRIdBIGTIME "us, %ld)\n",
                        timeout, bytesNeeded));
                status = locker.AcquireReader(timeout, bytesNeeded);
                size_t toRead = locker.AvailableBytes();
                if (status != B_OK && toRead == 0) {
                        TRACE(("tty_input_read() AcquireReader failed\n"));
                        break;
                }

                if (toRead > length)
                        toRead = length;

                bool _hitEOF = false;
                bool* hitEOF = canon && tty->pending_eof > 0 ? &_hitEOF : NULL;

                ssize_t bytesRead = line_buffer_user_read(tty->input_buffer, buffer,
                        toRead, tty->settings->termios.c_cc[VEOF], hitEOF);
                if (bytesRead < 0) {
                        status = bytesRead;
                        break;
                }

                buffer += bytesRead;
                length -= bytesRead;
                *_length += bytesRead;
                bytesNeeded = (size_t)bytesRead > bytesNeeded
                        ? 0 : bytesNeeded - bytesRead;

                // we hit an EOF char -- bail out, whatever amount of data we have
                if (hitEOF && *hitEOF) {
                        tty->pending_eof--;
                        break;
                }

                // Once we have read something reset the timeout to the inter-char
                // timeout, if applicable.
                if (!dontBlock && !canon && *_length > 0)
                        timeout = interCharTimeout;
        } while (bytesNeeded > 0);

        if (status == B_WOULD_BLOCK || status == B_TIMED_OUT) {
                // In non-blocking non-canonical-input-processing mode never return
                // timeout errors. Just return 0, if nothing has been read.
                if (!dontBlock && !canon)
                        status = B_OK;
        }

        TRACE(("tty_input_read() status 0x%" B_PRIx32 "\n", status));

        return *_length == 0 ? status : B_OK;
}


status_t
tty_write(tty_cookie* sourceCookie, const void* _buffer, size_t* _length)
{
        if (sourceCookie->tty->is_master)
                return tty_write_to_tty_master(sourceCookie, _buffer, _length);

        return tty_write_to_tty_slave(sourceCookie, _buffer, _length);
}


status_t
tty_control(tty_cookie* cookie, uint32 op, void* buffer, size_t length)
{
        struct tty* tty = cookie->tty;

        // bail out, if already closed
        TTYReference ttyReference(cookie);
        if (!ttyReference.IsLocked())
                return B_FILE_ERROR;

        TRACE(("tty_ioctl: tty %p, op %" B_PRIu32 ", buffer %p, length %"
                B_PRIuSIZE "\n", tty, op, buffer, length));
        RecursiveLocker locker(tty->lock);

        switch (op) {
                // blocking/non-blocking mode

                case B_SET_BLOCKING_IO:
                        cookie->open_mode &= ~O_NONBLOCK;
                        return B_OK;
                case B_SET_NONBLOCKING_IO:
                        cookie->open_mode |= O_NONBLOCK;
                        return B_OK;

                // get and set TTY attributes

                case TCGETA:
                        TRACE(("tty: get attributes\n"));
                        return user_memcpy(buffer, &tty->settings->termios,
                                sizeof(struct termios));

                case TCSETA:
                case TCSETAW:
                case TCSETAF:
                {
                        TRACE(("tty: set attributes (iflag = %" B_PRIx32 ", oflag = %"
                                B_PRIx32 ", cflag = %" B_PRIx32 ", lflag = %" B_PRIx32 ")\n",
                                tty->settings->termios.c_iflag, tty->settings->termios.c_oflag,
                                tty->settings->termios.c_cflag,
                                tty->settings->termios.c_lflag));

                        status_t status = user_memcpy(&tty->settings->termios, buffer,
                                sizeof(struct termios));
                        if (status != B_OK)
                                return status;

                        tty->service_func(tty, TTYSETMODES, &tty->settings->termios,
                                sizeof(struct termios));
                        return status;
                }

                // get and set process group ID

                case TIOCGPGRP:
                        TRACE(("tty: get pgrp_id\n"));
                        return user_memcpy(buffer, &tty->settings->pgrp_id, sizeof(pid_t));

                case TIOCSPGRP:
                {
                        TRACE(("tty: set pgrp_id\n"));
                        pid_t groupID;

                        if (user_memcpy(&groupID, buffer, sizeof(pid_t)) != B_OK)
                                return B_BAD_ADDRESS;

                        status_t error = team_set_foreground_process_group(tty,
                                groupID);
                        if (error == B_OK)
                                tty->settings->pgrp_id = groupID;
                        return error;
                }

                // become controlling TTY
                case TIOCSCTTY:
                {
                        TRACE(("tty: become controlling tty\n"));
                        pid_t processID = getpid();
                        pid_t sessionID = getsid(processID);
                        // Only session leaders can become controlling tty
                        if (processID != sessionID)
                                return B_NOT_ALLOWED;
                        // Check if already controlling tty
                        if (team_get_controlling_tty() == tty)
                                return B_OK;
                        tty->settings->session_id = sessionID;
                        tty->settings->pgrp_id = sessionID;
                        team_set_controlling_tty(tty);

                        // NOTE: We do not acquire a reference to the TTY for the team, so
                        // consumers of team_get_controlling_tty() must check the TTY is still
                        // valid before actually using it!
                        return B_OK;
                }

                // get session leader process group ID
                case TIOCGSID:
                {
                        TRACE(("tty: get session_id\n"));
                        return user_memcpy(buffer, &tty->settings->session_id,
                                sizeof(pid_t));
                }

                // get and set window size

                case TIOCGWINSZ:
                        TRACE(("tty: get window size\n"));
                        return user_memcpy(buffer, &tty->settings->window_size,
                                sizeof(struct winsize));

                case TIOCSWINSZ:
                {
                        uint16 oldColumns = tty->settings->window_size.ws_col;
                        uint16 oldRows = tty->settings->window_size.ws_row;

                        TRACE(("tty: set window size\n"));
                        if (user_memcpy(&tty->settings->window_size, buffer,
                                        sizeof(struct winsize)) < B_OK) {
                                return B_BAD_ADDRESS;
                        }

                        // send a signal only if the window size has changed
                        if ((oldColumns != tty->settings->window_size.ws_col
                                        || oldRows != tty->settings->window_size.ws_row)
                                && tty->settings->pgrp_id != 0) {
                                send_signal(-tty->settings->pgrp_id, SIGWINCH);
                        }

                        return B_OK;
                }

                case 'ichr':                    // BeOS (int*) (pre- select() support)
                {
                        int wanted;
                        int toRead;

                        // help identify apps using it
                        //dprintf("tty: warning: legacy BeOS opcode 'ichr'\n");

                        if (user_memcpy(&wanted, buffer, sizeof(int)) != B_OK)
                                return B_BAD_ADDRESS;

                        // release the mutex and grab a read lock
                        locker.Unlock();
                        ReaderLocker readLocker(cookie);

                        bigtime_t timeout = wanted == 0 ? 0 : B_INFINITE_TIMEOUT;

                        // TODO: If wanted is > the TTY buffer size, this loop cannot work
                        // correctly. Refactor the read code!
                        do {
                                status_t status = readLocker.AcquireReader(timeout, wanted);
                                if (status != B_OK)
                                        return status;

                                toRead = readLocker.AvailableBytes();
                        } while (toRead < wanted);

                        if (user_memcpy(buffer, &toRead, sizeof(int)) != B_OK)
                                return B_BAD_ADDRESS;

                        return B_OK;
                }

                case FIONREAD:
                {
                        int toRead = 0;

                        // release the mutex and grab a read lock
                        locker.Unlock();
                        ReaderLocker readLocker(cookie);

                        status_t status = readLocker.AcquireReader(0, 1);
                        if (status == B_OK)
                                toRead = readLocker.AvailableBytes();
                        else if (status != B_WOULD_BLOCK)
                                return status;

                        if (user_memcpy(buffer, &toRead, sizeof(int)) != B_OK)
                                return B_BAD_ADDRESS;

                        return B_OK;
                }

                case TIOCOUTQ:
                {
                        int toWrite = 0;

                        // release the mutex and grab a write lock
                        locker.Unlock();
                        WriterLocker writeLocker(cookie);

                        status_t status = writeLocker.AcquireWriter(0, 1);
                        if (status == B_OK)
                                toWrite = line_buffer_readable(tty->input_buffer);
                        else if (status != B_WOULD_BLOCK)
                                return status;

                        if (user_memcpy(buffer, &toWrite, sizeof(int)) != B_OK)
                                return B_BAD_ADDRESS;

                        return B_OK;
                }

                case TCXONC:                    // Unix, but even Linux doesn't handle it
                        //dprintf("tty: unsupported TCXONC\n");
                        break;

                case TCSETDTR:
                case TCSETRTS:
                case TIOCMSET:
                {
                        // control line state setting, we only support DTR and RTS
                        int value;
                        if (user_memcpy(&value, buffer, sizeof(value)) != B_OK)
                                return B_BAD_ADDRESS;

                        bool result = true;
                        bool dtr = (op == TCSETDTR  && value != 0)
                                || (op == TIOCMSET && (value & TIOCM_DTR) != 0);
                        if (op == TCSETDTR || op == TIOCMSET)
                                result &= tty->service_func(tty, TTYSETDTR, &dtr, sizeof(dtr));

                        bool rts = (op == TCSETRTS && value != 0)
                                || (op == TIOCMSET && (value & TIOCM_RTS) != 0);
                        if (op == TCSETRTS || op == TIOCMSET)
                                result &= tty->service_func(tty, TTYSETRTS, &rts, sizeof(rts));

                        return result ? B_OK : B_ERROR;
                }

                case TCGETBITS:
                case TIOCMGET:
                {
                        tty->service_func(tty, TTYGETSIGNALS, NULL, 0);
                        int bits = tty->hardware_bits;
                        return user_memcpy(buffer, &bits, sizeof(bits));
                }

                case TIOCMBIS:
                case TIOCMBIC:
                {
                        // control line state setting, we only support DTR and RTS
                        int value;
                        if (user_memcpy(&value, buffer, sizeof(value)) != B_OK)
                                return B_BAD_ADDRESS;

                        bool result = true;
                        bool dtr = (op == TIOCMBIS);
                        if (value & TIOCM_DTR)
                                result &= tty->service_func(tty, TTYSETDTR, &dtr, sizeof(dtr));

                        bool rts = (op == TIOCMBIS);
                        if (value & TIOCM_RTS)
                                result &= tty->service_func(tty, TTYSETRTS, &rts, sizeof(rts));

                        return result ? B_OK : B_ERROR;
                }

                case TIOCSBRK:
                case TIOCCBRK:
                case TCSBRK:
                {
                        bool set;
                        if (op == TIOCSBRK)
                                set = true;
                        else if (op == TIOCCBRK)
                                set = false;
                        else {
                                int value = (int)(uintptr_t)buffer;
                                set = value != 0;
                        }

                        if (tty->service_func(tty, TTYSETBREAK, &set, sizeof(set)))
                                return B_OK;

                        return B_ERROR;
                }

                case TCFLSH:
                {
                        int value = (int)(uintptr_t)buffer;
                        if (value & TCOFLUSH) {
                                struct tty* otherTTY = cookie->other_tty;
                                if (otherTTY->open_count <= 0)
                                        return B_ERROR;

                                clear_line_buffer(otherTTY->input_buffer);
                        }

                        if (value & TCIFLUSH)
                                clear_line_buffer(tty->input_buffer);

                        if (tty->service_func(tty, TTYFLUSH, &value, sizeof(value)))
                                return B_OK;

                        return B_ERROR;
                }

                case TIOCEXCL:
                {
                        tty->is_exclusive = true;
                        return B_OK;
                }

                case TIOCNXCL:
                {
                        tty->is_exclusive = false;
                        return B_OK;
                }
        }

        TRACE(("tty: unsupported opcode %" B_PRIu32 "\n", op));
        return B_BAD_VALUE;
}


status_t
tty_select(tty_cookie* cookie, uint8 event, uint32 ref, selectsync* sync)
{
        struct tty* tty = cookie->tty;

        TRACE(("tty_select(cookie = %p, event = %u, ref = %" B_PRIu32 ", sync = "
                "%p)\n", cookie, event, ref, sync));

        // we don't support all kinds of events
        if (event < B_SELECT_READ || (event > B_SELECT_ERROR && event != B_SELECT_DISCONNECTED))
                return B_BAD_VALUE;

        // if the TTY is already closed, we notify immediately
        TTYReference ttyReference(cookie);
        if (!ttyReference.IsLocked()) {
                TRACE(("tty_select() done: cookie %p already closed\n", cookie));

                if (event == B_SELECT_DISCONNECTED)
                        notify_select_event(sync, event);
                return B_OK;
        }

        // lock the TTY (allows us to freely access the cookie lists of this and
        // the other TTY)
        RecursiveLocker ttyLocker(tty->lock);

        // get the other TTY -- needed for `write' events
        struct tty* otherTTY = cookie->other_tty;
        if (otherTTY->open_count <= 0)
                otherTTY = NULL;

        // add the event to the TTY's pool
        status_t error = add_select_sync_pool_entry(&tty->select_pool, sync, event);
        if (error != B_OK) {
                TRACE(("tty_select() done: add_select_sync_pool_entry() failed: %"
                        B_PRIx32 "\n", error));

                return error;
        }

        // finally also acquire the request mutex, for access to the reader/writer
        // queues
        RecursiveLocker requestLocker(gTTYRequestLock);

        // check, if the event is already present
        switch (event) {
                case B_SELECT_READ:
                        if (tty->reader_queue.IsEmpty() && tty_readable(tty) > 0)
                                notify_select_event(sync, event);
                        break;

                case B_SELECT_WRITE:
                {
                        // writes go to the other TTY
                        if (otherTTY == NULL)
                                break;

                        // In case input is echoed, we have to check, whether we can
                        // currently can write to our TTY as well.
                        bool echo = (tty->is_master
                                && tty->settings->termios.c_lflag & ECHO);

                        if (otherTTY->writer_queue.IsEmpty()
                                && line_buffer_writable(otherTTY->input_buffer) > 0) {
                                if (!echo
                                        || (tty->writer_queue.IsEmpty()
                                                && line_buffer_writable(tty->input_buffer) > 0)) {
                                        notify_select_event(sync, event);
                                }
                        }
                        break;
                }

                case B_SELECT_DISCONNECTED:
                        if (otherTTY == NULL)
                                notify_select_event(sync, event);
                        break;

                case B_SELECT_ERROR:
                default:
                        break;
        }

        return B_OK;
}


status_t
tty_deselect(tty_cookie* cookie, uint8 event, selectsync* sync)
{
        struct tty* tty = cookie->tty;

        TRACE(("tty_deselect(cookie = %p, event = %u, sync = %p)\n", cookie, event,
                sync));

        // we don't support all kinds of events
        if (event < B_SELECT_READ || (event > B_SELECT_ERROR && event != B_SELECT_DISCONNECTED))
                return B_BAD_VALUE;

        // lock the TTY (guards the select sync pool, among other things)
        RecursiveLocker ttyLocker(tty->lock);

        return remove_select_sync_pool_entry(&tty->select_pool, sync, event);
}


status_t
tty_hardware_signal(tty_cookie* cookie, int signal, bool set)
{
        int bit = 0;

        switch (signal) {
                case TTYHWDCD:
                        bit = TCGB_DCD;
                        break;
                case TTYHWCTS:
                        bit = TCGB_CTS;
                        break;
                case TTYHWDSR:
                        bit = TCGB_DSR;
                        break;
                case TTYHWRI:
                        bit = TCGB_RI;
                        break;

                default:
                        return B_BAD_VALUE;
        }

        if (set)
                cookie->tty->hardware_bits |= bit;
        else
                cookie->tty->hardware_bits &= ~bit;

        return B_ERROR;
}