construct/matrix/app.cc

434 lines
6.6 KiB
C++

// The Construct
//
// Copyright (C) The Construct Developers, Authors & Contributors
// Copyright (C) 2016-2020 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
decltype(ircd::m::app::log)
ircd::m::app::log
{
"m.app"
};
decltype(ircd::m::app::path)
ircd::m::app::path
{
{ "name", "ircd.m.app.path" },
{ "default", string_view{} },
{ "persist", false },
};
decltype(ircd::m::app::autorun)
ircd::m::app::autorun
{
{ "name", "ircd.m.app.autorun" },
{ "default", true },
};
decltype(ircd::m::app::enable)
ircd::m::app::enable
{
{ "name", "ircd.m.app.enable" },
{ "default", true },
};
decltype(ircd::m::app::bin)
ircd::m::app::bin;
template<>
decltype(ircd::util::instance_list<ircd::m::app>::allocator)
ircd::util::instance_list<ircd::m::app>::allocator{};
template<>
decltype(ircd::util::instance_list<ircd::m::app>::list)
ircd::util::instance_list<ircd::m::app>::list
{
allocator
};
//
// init
//
void
ircd::m::app::init()
{
if(!enable || !path || ircd::read_only)
return;
std::vector<std::string> files
{
fs::ls(string_view(path))
};
bin = std::set<std::string>
{
begin(files), std::remove_if(begin(files), end(files), []
(const std::string &file)
{
return !fs::is_exec(file);
})
};
log::debug
{
log, "Found %zu executable in `%s'",
bin.size(),
string_view{path},
};
if(!autorun)
{
log::warning
{
log, "Autorun is disabled by the configuration. Apps may still be executed manually.",
};
return;
}
events::type::for_each_in("ircd.app.run.auto", []
(const string_view &, const event::idx &run_event_idx)
{
const m::event::fetch run_event
{
std::nothrow, run_event_idx
};
if(!run_event.valid || !my(run_event))
return true;
const m::room room
{
at<"room_id"_>(run_event)
};
const m::event::idx app_event_idx
{
room.get(std::nothrow, "ircd.app", at<"state_key"_>(run_event))
};
if(!app_event_idx)
return true;
const m::event::fetch app_event
{
std::nothrow, app_event_idx
};
if(!app_event.valid || !my(app_event))
return true;
log::debug
{
log, "Attempting app:%lu run.auto:%lu",
app_event_idx,
run_event_idx,
};
auto app
{
std::make_unique<m::app>(app_event_idx)
};
const auto pid
{
app->child.run()
};
app.release();
return true;
});
}
void
ircd::m::app::fini()
{
std::vector<app *> apps
{
std::begin(list), std::end(list)
};
for(auto *const &app : apps)
{
app->child.join(15);
delete app;
}
}
//
// app::app
//
ircd::m::app::app(const m::event::idx &event_idx)
:event_idx
{
event_idx
}
,feature
{
m::get(event_idx, "content")
}
,config
{
feature
}
,arg
{
config.at("arg")
}
,binpath{[&]
{
if(!path)
throw m::FORBIDDEN
{
"Configure the 'ircd.m.app.path' to permit."
};
if(!enable)
throw m::FORBIDDEN
{
"Configure 'ircd.m.app.enable' to permit."
};
const json::string file
{
arg.at(0)
};
const string_view part[2]
{
path, file
};
const auto ret
{
fs::path_string(path, part)
};
if(!bin.count(ret))
throw m::NOT_FOUND
{
"Executable '%s' not found in bin directory at `%s'",
file,
string_view{path},
};
return ret;
}()}
,argv{[&]
{
std::vector<json::string> ret
{
std::begin(arg), std::end(arg)
};
ret.at(0) = binpath;
return ret;
}()}
,outbuf
{
32_KiB
}
,child
{
argv
}
,user_id
{
m::get(event_idx, "sender")
}
,room_id
{
m::room_id(event_idx)
}
,event_id
{
m::event_id(event_idx)
}
,room_hook
{
std::bind(&app::handle_room_message, this, ph::_1, ph::_2),
{
{ "_site", "vm.eval" },
{ "type", "m.room.message" },
{ "room_id", this->room_id },
}
}
,worker_context
{
"m.app",
512_KiB,
std::bind(&app::worker, this)
}
{
}
ircd::m::app::~app()
noexcept
{
worker_context.interrupt();
}
void
ircd::m::app::worker()
try
{
child.dock.wait([this]() noexcept
{
return child.pid >= 0;
});
log::info
{
log, "app:%lu starting %s in %s for %s @ `%s' id:%lu pid:%ld",
event_idx,
string_view{event_id},
string_view{room_id},
string_view{user_id},
argv.at(0),
child.id,
child.pid,
};
run::barrier<ctx::interrupted>{}; do
{
if(!handle_stdout())
break;
}
while(1);
}
catch(const std::exception &e)
{
log::error
{
log, "app:%lu (%p) worker fatal :%s",
event_idx,
this,
e.what(),
};
const ctx::exception_handler eh;
child.join();
return;
}
bool
ircd::m::app::handle_stdout()
{
mutable_buffer buf(this->outbuf);
consume(buf, copy(buf, "<pre>"_sv));
const string_view output
{
read(this->child, buf)
};
consume(buf, size(output));
if(empty(output))
{
log::debug
{
log, "app:%lu :end of file",
this->event_idx,
};
return false;
}
consume(buf, copy(buf, "</pre>"_sv));
const string_view &content
{
data(this->outbuf), data(buf)
};
const fmt::bsprintf<96> alt
{
"app:%lu wrote %zu bytes to stdout.",
this->event_idx,
size(output),
};
const auto message_id
{
!ircd::read_only && !ircd::maintenance?
m::msghtml(room_id, user_id, content, string_view{alt}, "m.notice"):
m::event::id::buf{}
};
log::debug
{
log, "app:%lu output %zu bytes in %s to %s :%s%s",
event_idx,
size(content),
string_view{message_id},
string_view{room_id},
trunc(content, 64),
size(content) > 64? "...": "",
};
return true;
}
void
ircd::m::app::handle_room_message(const event &event,
vm::eval &)
{
assert(at<"room_id"_>(event) == room_id);
assert(at<"type"_>(event) == "m.room.message");
if(at<"sender"_>(event) == user_id)
return;
const m::room::message msg
{
json::get<"content"_>(event)
};
if(json::get<"msgtype"_>(msg) != "m.text")
return;
if(!startswith(json::get<"body"_>(msg), user_id))
return;
string_view text
{
json::get<"body"_>(msg)
};
text = lstrip(text, user_id);
text = lstrip(text, ':');
text = lstrip(text, ' ');
handle_stdin(event, text);
}
void
ircd::m::app::handle_stdin(const event &event,
const string_view &text)
{
const string_view wrote
{
write(this->child, text)
};
const string_view nl
{
write(this->child, "\n"_sv)
};
log::debug
{
log, "app:%lu input %zu of %zu bytes from %s in %s :%s%s",
event_idx,
size(wrote) + size(nl),
size(text) + 1,
string_view{at<"sender"_>(event)},
string_view{room_id},
trunc(text, 64),
size(text) > 64? "...": "",
};
}