root/src/apps/expander/ExpanderThread.cpp
/*
 * Copyright 2004-2010, Jérôme Duval. All rights reserved.
 * Distributed under the terms of the MIT License.
 * Original code from ZipOMatic by jonas.sundstrom@kirilla.com
 */
#include "ExpanderThread.h"


#include <errno.h>
#include <image.h>
#include <signal.h>
#include <termios.h>
#include <unistd.h>

#include <Messenger.h>
#include <Path.h>


const char* ExpanderThreadName = "ExpanderThread";


ExpanderThread::ExpanderThread(BMessage* refs_message, BMessenger* messenger)
        :
        GenericThread(ExpanderThreadName, B_NORMAL_PRIORITY, refs_message),
        fWindowMessenger(messenger),
        fThreadId(-1),
        fStdIn(-1),
        fStdOut(-1),
        fStdErr(-1),
        fExpanderOutput(NULL),
        fExpanderError(NULL)
{
        SetDataStore(new BMessage(*refs_message));
                // leak?
        // prevents bug with B_SIMPLE_DATA
        // (drag&drop messages)
}


ExpanderThread::~ExpanderThread()
{
        delete fWindowMessenger;
}


status_t
ExpanderThread::ThreadStartup()
{
        status_t status = B_OK;
        entry_ref srcRef;
        entry_ref destRef;
        BString cmd;

        if ((status = GetDataStore()->FindRef("srcRef", &srcRef)) != B_OK)
                return status;

        if (GetDataStore()->FindRef("destRef", &destRef) == B_OK) {
                BPath path(&destRef);
                chdir(path.Path());
        }

        if ((status = GetDataStore()->FindString("cmd", &cmd)) != B_OK)
                return status;

        BPath path(&srcRef);
        BString pathString(path.Path());
        pathString.CharacterEscape("\\\"$`", '\\');
        pathString.Prepend("\"");
        pathString.Append("\"");
        cmd.ReplaceAll("%s", pathString.String());

        int32 argc = 3;
        const char** argv = new const char * [argc + 1];

        argv[0] = strdup("/bin/sh");
        argv[1] = strdup("-c");
        argv[2] = strdup(cmd.String());
        argv[argc] = NULL;

        fThreadId = PipeCommand(argc, argv, fStdIn, fStdOut, fStdErr);

        delete [] argv;

        if (fThreadId < 0)
                return fThreadId;

        // lower the command priority since it is a background task.
        set_thread_priority(fThreadId, B_LOW_PRIORITY);

        resume_thread(fThreadId);

        int flags = fcntl(fStdOut, F_GETFL, 0);
        flags |= O_NONBLOCK;
        fcntl(fStdOut, F_SETFL, flags);
        flags = fcntl(fStdErr, F_GETFL, 0);
        flags |= O_NONBLOCK;
        fcntl(fStdErr, F_SETFL, flags);

        fExpanderOutput = fdopen(fStdOut, "r");
        fExpanderError = fdopen(fStdErr, "r");

        return B_OK;
}


status_t
ExpanderThread::ExecuteUnit(void)
{
        // read output and error from command
        // send it to window

        BMessage message('outp');
        bool outputAdded = false;
        for (int32 i = 0; i < 50; i++) {
                char* output_string = fgets(fExpanderOutputBuffer , LINE_MAX,
                        fExpanderOutput);
                if (output_string == NULL)
                        break;

                message.AddString("output", output_string);
                outputAdded = true;
        }
        if (outputAdded)
                fWindowMessenger->SendMessage(&message);

        if (feof(fExpanderOutput))
                return EOF;

        char* error_string = fgets(fExpanderOutputBuffer, LINE_MAX,
                fExpanderError);
        if (error_string != NULL && strcmp(error_string, "\n")) {
                BMessage message('errp');
                message.AddString("error", error_string);
                fWindowMessenger->SendMessage(&message);
        }

        // streams are non blocking, sleep every 100ms
        snooze(100000);

        return B_OK;
}


void
ExpanderThread::PushInput(BString text)
{
        text += "\n";
        write(fStdIn, text.String(), text.Length());
}


status_t
ExpanderThread::ThreadShutdown(void)
{
        close(fStdIn);
        close(fStdOut);
        close(fStdErr);

        return B_OK;
}


void
ExpanderThread::ThreadStartupFailed(status_t status)
{
        fprintf(stderr, "ExpanderThread::ThreadStartupFailed() : %s\n",
                strerror(status));

        Quit();
}


void
ExpanderThread::ExecuteUnitFailed(status_t status)
{
        if (status == EOF) {
                // thread has finished, been quit or killed, we don't know
                fWindowMessenger->SendMessage(new BMessage('exit'));
        } else {
                // explicit error - communicate error to Window
                fWindowMessenger->SendMessage(new BMessage('exrr'));
        }

        Quit();
}


void
ExpanderThread::ThreadShutdownFailed(status_t status)
{
        fprintf(stderr, "ExpanderThread::ThreadShutdownFailed() %s\n",
                strerror(status));
}


status_t
ExpanderThread::ProcessRefs(BMessage *msg)
{
        return B_OK;
}


thread_id
ExpanderThread::PipeCommand(int argc, const char** argv, int& in, int& out,
        int& err, const char** envp)
{
        // This function written by Peter Folk <pfolk@uni.uiuc.edu>
        // and published in the BeDevTalk FAQ
        // http://www.abisoft.com/faq/BeDevTalk_FAQ.html#FAQ-209

        // Save current FDs
        int old_out = dup(1);
        int old_err = dup(2);

        int filedes[2];

        // create new pipe FDs as stdout, stderr
        pipe(filedes);  dup2(filedes[1], 1); close(filedes[1]);
        out = filedes[0]; // Read from out, taken from cmd's stdout
        pipe(filedes);  dup2(filedes[1], 2); close(filedes[1]);
        err = filedes[0]; // Read from err, taken from cmd's stderr

        // taken from pty.cpp
        // create a tty for stdin, as utilities don't generally use stdin
        int master = posix_openpt(O_RDWR);
        if (master < 0)
                return -1;

        int slave;
        const char* ttyName;
        if (grantpt(master) != 0 || unlockpt(master) != 0
                || (ttyName = ptsname(master)) == NULL
                || (slave = open(ttyName, O_RDWR | O_NOCTTY)) < 0) {
                close(master);
                return -1;
        }

        int pid = fork();
        if (pid < 0) {
                close(master);
                close(slave);
                return -1;
        }

        // child
        if (pid == 0) {
                close(master);

                setsid();
                if (ioctl(slave, TIOCSCTTY, NULL) != 0) {
                        close(slave);
                        return -1;
                }

                dup2(slave, 0);
                close(slave);

                // "load" command.
                execv(argv[0], (char *const *)argv);

                // shouldn't get here
                _exit(-1);
        }

        // parent
        close (slave);
        in = master;

        // Restore old FDs
        close(1); dup(old_out); close(old_out);
        close(2); dup(old_err); close(old_err);

        // Theoretically I should do loads of error checking, but
        // the calls aren't very likely to fail, and that would
        // muddy up the example quite a bit. YMMV.

        return pid;
}


status_t
ExpanderThread::SuspendExternalExpander()
{
        thread_info info;
        status_t status = get_thread_info(fThreadId, &info);

        if (status == B_OK)
                return send_signal(-fThreadId, SIGSTOP);
        else
                return status;
}


status_t
ExpanderThread::ResumeExternalExpander()
{
        thread_info info;
        status_t status = get_thread_info(fThreadId, &info);

        if (status == B_OK)
                return send_signal(-fThreadId, SIGCONT);
        else
                return status;
}


status_t
ExpanderThread::InterruptExternalExpander()
{
        thread_info info;
        status_t status = get_thread_info(fThreadId, &info);

        if (status == B_OK) {
                status = send_signal(-fThreadId, SIGINT);
                WaitOnExternalExpander();
        }

        return status;
}


status_t
ExpanderThread::WaitOnExternalExpander()
{
        thread_info info;
        status_t status = get_thread_info(fThreadId, &info);

        if (status == B_OK)
                return wait_for_thread(fThreadId, &status);
        else
                return status;
}