summaryrefslogtreecommitdiff
path: root/indra/llcommon/llleap.cpp
blob: 84d2a12f657364de4c170673bdeda1bb2b88a209 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
/**
 * @file   llleap.cpp
 * @author Nat Goodspeed
 * @date   2012-02-20
 * @brief  Implementation for llleap.
 * 
 * $LicenseInfo:firstyear=2012&license=viewerlgpl$
 * Copyright (c) 2012, Linden Research, Inc.
 * $/LicenseInfo$
 */

// Precompiled header
#include "linden_common.h"
// associated header
#include "llleap.h"
// STL headers
#include <sstream>
#include <algorithm>
// std headers
// external library headers
#include <boost/bind.hpp>
#include <boost/scoped_ptr.hpp>
#include <boost/tokenizer.hpp>
// other Linden headers
#include "llerror.h"
#include "llstring.h"
#include "llprocess.h"
#include "llevents.h"
#include "stringize.h"
#include "llsdutil.h"
#include "llsdserialize.h"
#include "llerrorcontrol.h"
#include "lltimer.h"
#include "lluuid.h"
#include "llleaplistener.h"

#if LL_MSVC
#pragma warning (disable : 4355) // 'this' used in initializer list: yes, intentionally
#endif

LLLeap::LLLeap() {}
LLLeap::~LLLeap() {}

class LLLeapImpl: public LLLeap
{
    LOG_CLASS(LLLeap);
public:
    // Called only by LLLeap::create()
    LLLeapImpl(const std::string& desc, const std::vector<std::string>& plugin):
        // We might reassign mDesc in the constructor body if it's empty here.
        mDesc(desc),
        // We expect multiple LLLeapImpl instances. Definitely tweak
        // mDonePump's name for uniqueness.
        mDonePump("LLLeap", true),
        // Troubling thought: what if one plugin intentionally messes with
        // another plugin? LLEventPump names are in a single global namespace.
        // Try to make that more difficult by generating a UUID for the reply-
        // pump name -- so it should NOT need tweaking for uniqueness.
        mReplyPump(LLUUID::generateNewID().asString()),
        mExpect(0),
        mPrevFatalFunction(LLError::getFatalFunction()),
        // Instantiate a distinct LLLeapListener for this plugin. (Every
        // plugin will want its own collection of managed listeners, etc.)
        // Pass it a callback to our connect() method, so it can send events
        // from a particular LLEventPump to the plugin without having to know
        // this class or method name.
        mListener(new LLLeapListener(boost::bind(&LLLeapImpl::connect, this, _1, _2)))
    {
        // Rule out empty vector
        if (plugin.empty())
        {
            throw Error("no plugin command");
        }

        // Don't leave desc empty either, but in this case, if we weren't
        // given one, we'll fake one.
        if (desc.empty())
        {
            mDesc = LLProcess::basename(plugin[0]);
            // how about a toLower() variant that returns the transformed string?!
            std::string desclower(mDesc);
            LLStringUtil::toLower(desclower);
            // If we're running a Python script, use the script name for the
            // desc instead of just 'python'. Arguably we should check for
            // more different interpreters as well, but there's a reason to
            // notice Python specially: we provide Python LLSD serialization
            // support, so there's a pretty good reason to implement plugins
            // in that language.
            if (plugin.size() >= 2 && (desclower == "python" || desclower == "python.exe"))
            {
                mDesc = LLProcess::basename(plugin[1]);
            }
        }

        // Listen for child "termination" right away to catch launch errors.
        mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::bad_launch, this, _1));

        // Okay, launch child.
        LLProcess::Params params;
        params.desc = mDesc;
        std::vector<std::string>::const_iterator pi(plugin.begin()), pend(plugin.end());
        params.executable = *pi++;
        for ( ; pi != pend; ++pi)
        {
            params.args.add(*pi);
        }
        params.files.add(LLProcess::FileParam("pipe")); // stdin
        params.files.add(LLProcess::FileParam("pipe")); // stdout
        params.files.add(LLProcess::FileParam("pipe")); // stderr
        params.postend = mDonePump.getName();
        mChild = LLProcess::create(params);
        // If that didn't work, no point in keeping this LLLeap object.
        if (! mChild)
        {
            throw Error(STRINGIZE("failed to run " << mDesc));
        }

        // Okay, launch apparently worked. Change our mDonePump listener.
        mDonePump.stopListening("LLLeap");
        mDonePump.listen("LLLeap", boost::bind(&LLLeapImpl::done, this, _1));

        // Child might pump large volumes of data through either stdout or
        // stderr. Don't bother copying all that data into notification event.
        LLProcess::ReadPipe
            &childout(mChild->getReadPipe(LLProcess::STDOUT)),
            &childerr(mChild->getReadPipe(LLProcess::STDERR));
        childout.setLimit(20);
        childerr.setLimit(20);

        // Serialize any event received on mReplyPump to our child's stdin.
        mStdinConnection = connect(mReplyPump, "LLLeap");

        // Listening on stdout is stateful. In general, we're either waiting
        // for the length prefix or waiting for the specified length of data.
        // We address that with two different listener methods -- one of which
        // is blocked at any given time.
        mStdoutConnection = childout.getPump()
            .listen("prefix", boost::bind(&LLLeapImpl::rstdout, this, _1));
        mStdoutDataConnection = childout.getPump()
            .listen("data",   boost::bind(&LLLeapImpl::rstdoutData, this, _1));
        mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));

        // Log anything sent up through stderr. When a typical program
        // encounters an error, it writes its error message to stderr and
        // terminates with nonzero exit code. In particular, the Python
        // interpreter behaves that way. More generally, though, a plugin
        // author can log whatever s/he wants to the viewer log using stderr.
        mStderrConnection = childerr.getPump()
            .listen("LLLeap", boost::bind(&LLLeapImpl::rstderr, this, _1));

        // For our lifespan, intercept any LL_ERRS so we can notify plugin
        LLError::setFatalFunction(boost::bind(&LLLeapImpl::fatalFunction, this, _1));

        // Send child a preliminary event reporting our own reply-pump name --
        // which would otherwise be pretty tricky to guess!
        wstdin(mReplyPump.getName(),
               LLSDMap
               ("command", mListener->getName())
               // Include LLLeap features -- this may be important for child to
               // construct (or recognize) current protocol.
               ("features", LLLeapListener::getFeatures()));
    }

    // Normally we'd expect to arrive here only via done()
    virtual ~LLLeapImpl()
    {
        LL_DEBUGS("LLLeap") << "destroying LLLeap(\"" << mDesc << "\")" << LL_ENDL;
        // Restore original FatalFunction
        LLError::setFatalFunction(mPrevFatalFunction);
    }

    // Listener for failed launch attempt
    bool bad_launch(const LLSD& data)
    {
        LL_WARNS("LLLeap") << data["string"].asString() << LL_ENDL;
        return false;
    }

    // Listener for child-process termination
    bool done(const LLSD& data)
    {
        // Log the termination
        LL_INFOS("LLLeap") << data["string"].asString() << LL_ENDL;

        // Any leftover data at this moment are because protocol was not
        // satisfied. Possibly the child was interrupted in the middle of
        // sending a message, possibly the child didn't flush stdout before
        // terminating, possibly it's just garbage. Log its existence but
        // discard it.
        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
        if (childout.size())
        {
            LLProcess::ReadPipe::size_type
                peeklen((std::min)(LLProcess::ReadPipe::size_type(50), childout.size()));
            LL_WARNS("LLLeap") << "Discarding final " << childout.size() << " bytes: "
                               << childout.peek(0, peeklen) << "..." << LL_ENDL;
        }

        // Kill this instance. MUST BE LAST before return!
        delete this;
        return false;
    }

    // Listener for events on mReplyPump: send to child stdin
    bool wstdin(const std::string& pump, const LLSD& data)
    {
        LLSD packet(LLSDMap("pump", pump)("data", data));

        std::ostringstream buffer;
        buffer << LLSDNotationStreamer(packet);

/*==========================================================================*|
        // DEBUGGING ONLY: don't copy str() if we can avoid it.
        std::string strdata(buffer.str());
        if (std::size_t(buffer.tellp()) != strdata.length())
        {
            LL_ERRS("LLLeap") << "tellp() -> " << buffer.tellp() << " != "
                              << "str().length() -> " << strdata.length() << LL_ENDL;
        }
        // DEBUGGING ONLY: reading back is terribly inefficient.
        std::istringstream readback(strdata);
        LLSD echo;
        LLPointer<LLSDParser> parser(new LLSDNotationParser());
        S32 parse_status(parser->parse(readback, echo, strdata.length()));
        if (parse_status == LLSDParser::PARSE_FAILURE)
        {
            LL_ERRS("LLLeap") << "LLSDNotationParser() cannot parse output of "
                              << "LLSDNotationStreamer()" << LL_ENDL;
        }
        if (! llsd_equals(echo, packet))
        {
            LL_ERRS("LLLeap") << "LLSDNotationParser() produced different LLSD "
                              << "than passed to LLSDNotationStreamer()" << LL_ENDL;
        }
|*==========================================================================*/

        LL_DEBUGS("EventHost") << "Sending: " << buffer.tellp() << ':';
        std::string::size_type truncate(80);
        if (buffer.tellp() <= truncate)
        {
            LL_CONT << buffer.str();
        }
        else
        {
            LL_CONT << buffer.str().substr(0, truncate) << "...";
        }
        LL_CONT << LL_ENDL;

        LLProcess::WritePipe& childin(mChild->getWritePipe(LLProcess::STDIN));
        childin.get_ostream() << buffer.tellp() << ':' << buffer.str() << std::flush;
        return false;
    }

    // Initial state of stateful listening on child stdout: wait for a length
    // prefix, followed by ':'.
    bool rstdout(const LLSD& data)
    {
        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
        // It's possible we got notified of a couple digit characters without
        // seeing the ':' -- unlikely, but still. Until we see ':', keep
        // waiting.
        if (childout.contains(':'))
        {
            std::istream& childstream(childout.get_istream());
            // Saw ':', read length prefix and store in mExpect.
            size_t expect;
            childstream >> expect;
            int colon(childstream.get());
            if (colon != ':')
            {
                // Protocol failure. Clear out the rest of the pending data in
                // childout (well, up to a max length) to log what was wrong.
                LLProcess::ReadPipe::size_type
                    readlen((std::min)(childout.size(), LLProcess::ReadPipe::size_type(80)));
                bad_protocol(STRINGIZE(expect << char(colon) << childout.read(readlen)));
            }
            else
            {
                // Saw length prefix, saw colon, life is good. Now wait for
                // that length of data to arrive.
                mExpect = expect;
                LL_DEBUGS("LLLeap") << "got length, waiting for "
                                    << mExpect << " bytes of data" << LL_ENDL;
                // Block calls to this method; resetting mBlocker unblocks
                // calls to the other method.
                mBlocker.reset(new LLEventPump::Blocker(mStdoutConnection));
                // Go check if we've already received all the advertised data.
                if (childout.size())
                {
                    LLSD updata(data);
                    updata["len"] = LLSD::Integer(childout.size());
                    rstdoutData(updata);
                }
            }
        }
        else if (childout.contains('\n'))
        {
            // Since this is the initial listening state, this is where we'd
            // arrive if the child isn't following protocol at all -- say
            // because the user specified 'ls' or some darn thing.
            bad_protocol(childout.getline());
        }
        return false;
    }

    // State in which we listen on stdout for the specified length of data to
    // arrive.
    bool rstdoutData(const LLSD& data)
    {
        LLProcess::ReadPipe& childout(mChild->getReadPipe(LLProcess::STDOUT));
        // Until we've accumulated the promised length of data, keep waiting.
        if (childout.size() >= mExpect)
        {
            // Ready to rock and roll.
            LL_DEBUGS("LLLeap") << "needed " << mExpect << " bytes, got "
                                << childout.size() << ", parsing LLSD" << LL_ENDL;
            LLSD data;
            LLPointer<LLSDParser> parser(new LLSDNotationParser());
            S32 parse_status(parser->parse(childout.get_istream(), data, mExpect));
            if (parse_status == LLSDParser::PARSE_FAILURE)
            {
                bad_protocol("unparseable LLSD data");
            }
            else if (! (data.isMap() && data["pump"].isString() && data.has("data")))
            {
                // we got an LLSD object, but it lacks required keys
                bad_protocol("missing 'pump' or 'data'");
            }
            else
            {
                // The LLSD object we got from our stream contains the keys we
                // need.
                LLEventPumps::instance().obtain(data["pump"]).post(data["data"]);
                // Block calls to this method; resetting mBlocker unblocks calls
                // to the other method.
                mBlocker.reset(new LLEventPump::Blocker(mStdoutDataConnection));
                // Go check for any more pending events in the buffer.
                if (childout.size())
                {
                    LLSD updata(data);
                    data["len"] = LLSD::Integer(childout.size());
                    rstdout(updata);
                }
            }
        }
        return false;
    }

    void bad_protocol(const std::string& data)
    {
        LL_WARNS("LLLeap") << mDesc << ": invalid protocol: " << data << LL_ENDL;
        // No point in continuing to run this child.
        mChild->kill();
    }

    // Listen on child stderr and log everything that arrives
    bool rstderr(const LLSD& data)
    {
        LLProcess::ReadPipe& childerr(mChild->getReadPipe(LLProcess::STDERR));
        // We might have gotten a notification involving only a partial line
        // -- or multiple lines. Read all complete lines; stop when there's
        // only a partial line left.
        while (childerr.contains('\n'))
        {
            // DO NOT make calls with side effects in a logging statement! If
            // that log level is suppressed, your side effects WON'T HAPPEN.
            std::string line(childerr.getline());
            // Log the received line. Prefix it with the desc so we know which
            // plugin it's from. This method name rstderr() is intentionally
            // chosen to further qualify the log output.
            LL_INFOS("LLLeap") << mDesc << ": " << line << LL_ENDL;
        }
        // What if child writes a final partial line to stderr?
        if (data["eof"].asBoolean() && childerr.size())
        {
            std::string rest(childerr.read(childerr.size()));
            // Read all remaining bytes and log.
            LL_INFOS("LLLeap") << mDesc << ": " << rest << LL_ENDL;
        }
        return false;
    }

    void fatalFunction(const std::string& error)
    {
        // Notify plugin
        LLSD event;
        event["type"] = "error";
        event["error"] = error;
        mReplyPump.post(event);

        // All the above really accomplished was to buffer the serialized
        // event in our WritePipe. Have to pump mainloop a couple times to
        // really write it out there... but time out in case we can't write.
        LLProcess::WritePipe& childin(mChild->getWritePipe(LLProcess::STDIN));
        LLEventPump& mainloop(LLEventPumps::instance().obtain("mainloop"));
        LLSD nop;
        F64 until = (LLTimer::getElapsedSeconds() + 2).value();
        while (childin.size() && LLTimer::getElapsedSeconds() < until)
        {
            mainloop.post(nop);
        }

        // forward the call to the previous FatalFunction
        mPrevFatalFunction(error);
    }

private:
    /// We always want to listen on mReplyPump with wstdin(); under some
    /// circumstances we'll also echo other LLEventPumps to the plugin.
    LLBoundListener connect(LLEventPump& pump, const std::string& listener)
    {
        // Serialize any event received on the specified LLEventPump to our
        // child's stdin, suitably enriched with the pump name on which it was
        // received.
        return pump.listen(listener,
                           boost::bind(&LLLeapImpl::wstdin, this, pump.getName(), _1));
    }

    std::string mDesc;
    LLEventStream mDonePump;
    LLEventStream mReplyPump;
    LLProcessPtr mChild;
    LLTempBoundListener
        mStdinConnection, mStdoutConnection, mStdoutDataConnection, mStderrConnection;
    boost::scoped_ptr<LLEventPump::Blocker> mBlocker;
    LLProcess::ReadPipe::size_type mExpect;
    LLError::FatalFunction mPrevFatalFunction;
    boost::scoped_ptr<LLLeapListener> mListener;
};

// This must follow the declaration of LLLeapImpl, so it may as well be last.
LLLeap* LLLeap::create(const std::string& desc, const std::vector<std::string>& plugin, bool exc)
{
    // If caller is willing to permit exceptions, just instantiate.
    if (exc)
        return new LLLeapImpl(desc, plugin);

    // Caller insists on suppressing LLLeap::Error. Very well, catch it.
    try
    {
        return new LLLeapImpl(desc, plugin);
    }
    catch (const LLLeap::Error&)
    {
        return NULL;
    }
}

LLLeap* LLLeap::create(const std::string& desc, const std::string& plugin, bool exc)
{
    // Use LLStringUtil::getTokens() to parse the command line
    return create(desc,
                  LLStringUtil::getTokens(plugin,
                                          " \t\r\n", // drop_delims
                                          "",        // no keep_delims
                                          "\"'",     // either kind of quotes
                                          "\\"),     // backslash escape
                  exc);
}