diff --git a/engine/src/flutter/tools/debugger/prompt/BUILD.gn b/engine/src/flutter/tools/debugger/prompt/BUILD.gn index a66d0e0cfa..578d976b67 100644 --- a/engine/src/flutter/tools/debugger/prompt/BUILD.gn +++ b/engine/src/flutter/tools/debugger/prompt/BUILD.gn @@ -16,6 +16,8 @@ mojo_native_application("prompt") { "//mojo/application", "//mojo/public/cpp/bindings", "//mojo/public/cpp/utility", + "//net", + "//net:http_server", "//services/tracing:bindings", "//sky/tools/debugger:bindings", "//sky/viewer:bindings", diff --git a/engine/src/flutter/tools/debugger/prompt/prompt.cc b/engine/src/flutter/tools/debugger/prompt/prompt.cc index fc137b5337..4a69c0c192 100644 --- a/engine/src/flutter/tools/debugger/prompt/prompt.cc +++ b/engine/src/flutter/tools/debugger/prompt/prompt.cc @@ -8,29 +8,17 @@ #include "mojo/public/c/system/main.h" #include "mojo/public/cpp/application/application_delegate.h" #include "mojo/public/cpp/application/application_impl.h" +#include "net/server/http_server.h" +#include "net/server/http_server_request_info.h" +#include "net/socket/tcp_server_socket.h" #include "services/tracing/tracing.mojom.h" #include "sky/tools/debugger/debugger.mojom.h" #include namespace sky { namespace debugger { -namespace { -std::string GetCommand() { - std::cout << "(skydb) "; - std::cout.flush(); - - std::string command; - std::getline(std::cin, command); - // Any errors (including eof) just quit the debugger: - if (!std::cin.good()) - command = 'q'; - return command; -} - -} - -class Prompt : public mojo::ApplicationDelegate { +class Prompt : public mojo::ApplicationDelegate, public net::HttpServer::Delegate { public: Prompt() : is_tracing_(false), @@ -49,6 +37,12 @@ class Prompt : public mojo::ApplicationDelegate { url_ = "https://raw.githubusercontent.com/domokit/mojo/master/sky/" "examples/home.sky"; } + scoped_ptr server_socket( + new net::TCPServerSocket(NULL, net::NetLog::Source())); + // FIXME: This port needs to be configurable, as-is we can only run + // one copy of mojo_shell with sky at a time! + server_socket->ListenWithAddressAndPort("0.0.0.0", 7777, 1); + web_server_.reset(new net::HttpServer(server_socket.Pass(), this)); } virtual bool ConfigureIncomingConnection( @@ -56,93 +50,86 @@ class Prompt : public mojo::ApplicationDelegate { connection->ConnectToService(&debugger_); std::cout << "Loading " << url_ << std::endl; Reload(); -#if !defined(OS_ANDROID) - // FIXME: To support device-centric development we need to re-write - // prompt.cc to just be a server and have all the command handling move - // to python (skydb). prompt.cc would just run until told to quit. - // If we don't comment this out then prompt.cc just quits when run headless - // as it immediately recieves EOF which it treats as quit. - ScheduleWaitForInput(); -#endif return true; } - bool ExecuteCommand(const std::string& command) { - if (command == "help" || command == "h") { - PrintHelp(); - return true; - } - if (command == "trace") { - ToggleTracing(); - return true; - } - if (command == "reload" || command == "r") { - Reload(); - return true; - } - if (command == "inspect") { - Inspect(); - return true; - } - if (command == "quit" || command == "q") { - Quit(); - return true; - } - if (command.size() == 1) { - std::cout << "Unknown command: " << command << std::endl; - return true; - } - return false; + // net::HttpServer::Delegate + void OnConnect(int connection_id) override { } - void WaitForInput() { - std::string command = GetCommand(); + void OnClose(int connection_id) override { + } - if (!ExecuteCommand(command)) { - if (command.size() > 0) { - url_ = command; - Reload(); - } + void OnHttpRequest( + int connection_id, const net::HttpServerRequestInfo& info) override { + + // FIXME: We should use use a fancier lookup system more like what + // services/http_server/http_server.cc does with AddHandler. + if (info.path == "/trace") + ToggleTracing(connection_id); + else if (info.path == "/reload") + Load(connection_id, url_); + else if (info.path == "/inspect") + Inspect(connection_id); + else if (info.path == "/quit") + Quit(connection_id); + else if (info.path == "/load") + Load(connection_id, info.data); + else { + Help(info.path, connection_id); } - - ScheduleWaitForInput(); } - void ScheduleWaitForInput() { - base::MessageLoop::current()->PostTask(FROM_HERE, - base::Bind(&Prompt::WaitForInput, weak_ptr_factory_.GetWeakPtr())); + void OnWebSocketRequest( + int connection_id, const net::HttpServerRequestInfo& info) override { + web_server_->Send500(connection_id, "http only"); } - void PrintHelp() { - std::cout - << "Sky Debugger" << std::endl - << "============" << std::endl - << "Type a URL to load in the debugger, enter to reload." << std::endl - << "Commands: help -- Help" << std::endl - << " trace -- Capture a trace" << std::endl - << " reload -- Reload the current page" << std::endl - << " inspect -- Inspect the current page" << std::endl - << " quit -- Quit" << std::endl; + void OnWebSocketMessage( + int connection_id, const std::string& data) override { + web_server_->Send500(connection_id, "http only"); + } + + void Respond(int connection_id, std::string response) { + web_server_->Send200(connection_id, response, "text/plain"); + } + + void Help(std::string path, int connection_id) { + std::string help = "Sky Debugger\n" + "Supported URLs:\n" + "/toggle_tracing -- Start/stop tracing\n" + "/reload -- Reload the current page\n" + "/inspect -- Start inspector server for current page\n" + "/quit -- Quit\n" + "/load -- Load a new URL, url in POST body.\n"; + if (path != "/") + help = "Unknown path: " + path + "\n\n" + help; + Respond(connection_id, help); + } + + void Load(int connection_id, std::string url) { + url_ = url; + Reload(); + Respond(connection_id, "OK\n"); } void Reload() { debugger_->NavigateToURL(url_); } - void Inspect() { + void Inspect(int connection_id) { debugger_->InjectInspector(); - std::cout - << "Open the following URL in Chrome:" << std::endl - << "chrome-devtools://devtools/bundled/devtools.html?ws=localhost:9898" - << std::endl; + Respond(connection_id, + "Open the following URL in Chrome:\n" + "chrome-devtools://devtools/bundled/devtools.html?ws=localhost:9898\n"); } - void Quit() { + void Quit(int connection_id) { std::cout << "quitting" << std::endl; debugger_->Shutdown(); } - void ToggleTracing() { + void ToggleTracing(int connection_id) { if (is_tracing_) { std::cout << "Stopping trace (writing to sky_viewer.trace)" << std::endl; tracing_->StopAndFlush(); @@ -151,6 +138,7 @@ class Prompt : public mojo::ApplicationDelegate { tracing_->Start(mojo::String("sky_viewer"), mojo::String("*")); } is_tracing_ = !is_tracing_; + Respond(connection_id, "OK\n"); } bool is_tracing_; @@ -158,6 +146,7 @@ class Prompt : public mojo::ApplicationDelegate { tracing::TraceCoordinatorPtr tracing_; std::string url_; base::WeakPtrFactory weak_ptr_factory_; + scoped_ptr web_server_; DISALLOW_COPY_AND_ASSIGN(Prompt); }; @@ -167,5 +156,6 @@ class Prompt : public mojo::ApplicationDelegate { MojoResult MojoMain(MojoHandle shell_handle) { mojo::ApplicationRunnerChromium runner(new sky::debugger::Prompt); + runner.set_message_loop_type(base::MessageLoop::TYPE_IO); return runner.Run(shell_handle); } diff --git a/engine/src/flutter/tools/skydb b/engine/src/flutter/tools/skydb index c810c7df46..07248c9b1d 100755 --- a/engine/src/flutter/tools/skydb +++ b/engine/src/flutter/tools/skydb @@ -6,8 +6,11 @@ from skypy.paths import Paths from skypy.skyserver import SkyServer import argparse +import json import logging import os +import requests +import signal import skypy.configuration as configuration import subprocess import urlparse @@ -19,13 +22,17 @@ SUPPORTED_MIME_TYPES = [ 'text/plain', ] - HTTP_PORT = 9999 +PID_FILE_PATH = "/tmp/skydb.pids" class SkyDebugger(object): def __init__(self): self.paths = None + self.pids = {} + # FIXME: This is not android aware nor aware of the port + # skyserver is listening on. + self.base_url = 'http://localhost:7777' def _server_root_and_url_from_path_arg(self, url_or_path): # This is already a valid url we don't need a local server. @@ -46,19 +53,8 @@ class SkyDebugger(object): def _in_chromoting(self): return os.environ.get('CHROME_REMOTE_DESKTOP_SESSION', False) - def main(self): - logging.basicConfig(level=logging.INFO) - - parser = argparse.ArgumentParser(description='Sky launcher/debugger') - parser.add_argument('--gdb', action='store_true') - parser.add_argument('--use-osmesa', action='store_true', - default=self._in_chromoting()) - parser.add_argument('url_or_path', nargs='?', type=str) - parser.add_argument('--show-command', action='store_true', - help='Display the shell command and exit') - configuration.add_arguments(parser) - args = parser.parse_args() - + def _build_mojo_shell_command(self, args): + sky_server = None self.paths = Paths(os.path.join('out', args.configuration)) content_handlers = ['%s,%s' % (mime_type, 'mojo:sky_viewer') @@ -73,33 +69,126 @@ class SkyDebugger(object): if args.use_osmesa: shell_command.append('--args-for=mojo:native_viewport_service --use-osmesa') - server_root = None - if args.url_or_path: # Check if we need a local server for the url/path arg: server_root, url = \ self._server_root_and_url_from_path_arg(args.url_or_path) + sky_server = SkyServer(self.paths, HTTP_PORT, args.configuration, + server_root) + prompt_args = '--args-for=mojo:sky_debugger_prompt %s' % url shell_command.append(prompt_args) if args.gdb: shell_command = ['gdb', '--args'] + shell_command - if server_root: - with SkyServer(self.paths, HTTP_PORT, args.configuration, - server_root): - subprocess.check_call(shell_command) - else: - subprocess.check_call(shell_command) + return shell_command, sky_server + + def start_command(self, args): + shell_command, sky_server = self._build_mojo_shell_command(args) if args.show_command: print " ".join(shell_command) - else: - subprocess.check_call(shell_command) + return - def shutdown(self): - print "Quitting" - if self._sky_server: - self._sky_server.terminate() + self.stop_command([]) # Quit any existing process. + + if sky_server: + self.pids['sky_server_pid'] = sky_server.start() + # self.pids['sky_server_port'] = sky_server.port + # self.pids['sky_server_root'] = sky_server.root + self.pids['mojo_shell_pid'] = subprocess.Popen(shell_command).pid + + def _kill_if_exists(self, key, name): + pid = self.pids.pop(key, None) + if not pid: + logging.info('No pid for %s, nothing to do.' % name) + return + logging.info('Killing %s (%s).' % (name, pid)) + try: + os.kill(pid, signal.SIGTERM) + except OSError: + logging.info('%s (%s) already gone.' % (name, pid)) + + def stop_command(self, args): + # FIXME: Send /quit to sky prompt instead of killing. + # self._send_command_to_sky('/quit') + self._kill_if_exists('mojo_shell_pid', 'mojo_shell') + self._kill_if_exists('sky_server_pid', 'sky_server') + + def load_command(self, args): + # Should resolve paths to relative urls like start does. + # self.pids['sky_server_root'] and port should help. + self._send_command_to_sky('/load', args.url_or_path) + + def _send_command_to_sky(self, command_path, payload=None): + url = self.base_url + command_path + if payload: + response = requests.post(url, payload) + else: + response = requests.get(url) + print response.text + + # FIXME: These could be made into a context object with __enter__/__exit__. + def _load_pid_file(self, path): + try: + with open(path, 'r') as pid_file: + return json.load(pid_file) + except: + if os.path.exists(path): + logging.warn('Failed to read pid file: %s' % path) + return {} + + def _write_pid_file(self, path, pids): + try: + with open(path, 'w') as pid_file: + json.dump(pids, pid_file) + except: + logging.warn('Failed to write pid file: %s' % path) + + def _add_basic_command(self, subparsers, name, url_path, help_text): + parser = subparsers.add_parser(name, help=help_text) + command = lambda args: self._send_command_to_sky(url_path) + parser.set_defaults(func=command) + + def main(self): + logging.basicConfig(level=logging.INFO) + + self.pids = self._load_pid_file(PID_FILE_PATH) + + parser = argparse.ArgumentParser(description='Sky launcher/debugger') + subparsers = parser.add_subparsers(help='sub-command help') + + start_parser = subparsers.add_parser('start', + help='launch a new mojo_shell with sky') + configuration.add_arguments(start_parser) + start_parser.add_argument('--gdb', action='store_true') + start_parser.add_argument('--use-osmesa', action='store_true', + default=self._in_chromoting()) + start_parser.add_argument('url_or_path', nargs='?', type=str) + start_parser.add_argument('--show-command', action='store_true', + help='Display the shell command and exit') + start_parser.set_defaults(func=self.start_command) + + stop_parser = subparsers.add_parser('stop', + help=('stop sky (as listed in %s)' % PID_FILE_PATH)) + stop_parser.set_defaults(func=self.stop_command) + + self._add_basic_command(subparsers, 'trace', '/trace', + 'toggle tracing') + self._add_basic_command(subparsers, 'reload', '/reload', + 'reload the current page') + self._add_basic_command(subparsers, 'inspect', '/inspect', + 'stop the running sky instance') + + load_parser = subparsers.add_parser('load', + help='load a new page in the currently running sky') + load_parser.add_argument('url_or_path', type=str) + load_parser.set_defaults(func=self.load_command) + + args = parser.parse_args() + args.func(args) + + self._write_pid_file(PID_FILE_PATH, self.pids) if __name__ == '__main__': diff --git a/engine/src/flutter/tools/skypy/skyserver.py b/engine/src/flutter/tools/skypy/skyserver.py index bddea9e0ab..b90846e445 100644 --- a/engine/src/flutter/tools/skypy/skyserver.py +++ b/engine/src/flutter/tools/skypy/skyserver.py @@ -26,7 +26,7 @@ class SkyServer(object): 'download_sky_server')) return os.path.join(paths.src_root, 'out', 'downloads', 'sky_server') - def __enter__(self): + def start(self): if self._port_in_use(self.port): logging.warn( 'Port %s already in use, assuming custom sky_server started.' % @@ -41,11 +41,18 @@ class SkyServer(object): str(self.port), ] self.server = subprocess.Popen(server_command) + return self.server.pid - def __exit__(self, exc_type, exc_value, traceback): + def stop(self): if self.server: self.server.terminate() + def __enter__(self): + self.start() + + def __exit__(self, exc_type, exc_value, traceback): + self.stop() + def path_as_url(self, path): return self.url_for_path(self.port, self.root, path)