diff --git a/.gitmodules b/.gitmodules index 7f3769d..8739090 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,9 @@ [submodule "Shared/ws4py"] path = Shared/ws4py url = https://github.com/robot0nfire/ws4py.git +[submodule "Shared/bottle"] + path = Shared/bottle + url = https://github.com/bottlepy/bottle.git +[submodule "Shared/dashb0ard"] + path = Shared/dashb0ard + url = https://github.com/robot0nfire/dashb0ard.git diff --git a/.no-fl0w b/.no-fl0w deleted file mode 100644 index e69de29..0000000 diff --git a/README.md b/README.md index a7231af..1f18318 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -fl0w logo +fl0w logo # fl0w fl0w is currently in the process of being refactored and revamped. diff --git a/Server/Broadcast.py b/Server/Broadcast.py deleted file mode 100644 index fb7d180..0000000 --- a/Server/Broadcast.py +++ /dev/null @@ -1,47 +0,0 @@ -class Broadcast: - class ChannelError(IndexError): - def __init__(self, channel): - super(Broadcast.ChannelError, self).__init__("channel '%s' does not exist" % channel) - - def __init__(self): - self.channels = {} - - def broadcast(self, data, route, channel, exclude=[]): - if channel in self.channels: - for handler in self.channels[channel]: - if not handler in exclude: - handler.send(data, route) - else: - raise Broadcast.ChannelError(channel) - - def remove(self, handler, channel): - if channel in self.channels: - if handler in self.channels[channel]: - del self.channels[channel][self.channels[channel].index(handler)] - else: - raise Broadcast.ChannelError(channel) - - def add(self, handler, channel): - if channel in self.channels: - if not handler in self.channels[channel]: - self.channels[channel].append(handler) - else: - raise Broadcast.ChannelError(channel) - - def add_channel(self, channel): - self.channels[channel] = [] - - def remove_channel(self, channel): - if channel in self.channels: - del self.channels[channel] - else: - raise Broadcast.ChannelError(channel) - - def __repr__(self): - out = "Channels:\n" - for channel in self.channels: - out += "%s: %d socks\n" % (channel, len(self.channels[channel])) - return out.rstrip("\n") - - def __str__(self): - return self.__repr__() \ No newline at end of file diff --git a/Server/Server.py b/Server/Server.py index e058d29..5e36eb4 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -1,69 +1,35 @@ +from Meh import Config, Option, ExceptionInConfigError +from Highway import Server, Route, DummyPipe import Logging -import Config -from .Broadcast import Broadcast - -import json import os import subprocess import re import pwd import platform -import struct from subprocess import Popen, PIPE +from _thread import start_new_thread from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication -from Highway import Server, Route, DummyPipe +from bottle.bottle import route, run, static_file +@route("/") +def index(): + return static_file("index.html", root="Shared/dashb0ard") + +@route("/static/") +def static(filepath): + return static_file(filepath, root="Shared/dashb0ard/static") + class Info(Route): def run(self, data, handler): handler.send({"routes" : list(handler.routes.keys())}, "info") -class Compile: - HAS_MAIN = re.compile(r"\w*\s*main\(\)\s*(\{|.*)$") - - @staticmethod - def is_valid_c_program(path): - for line in open(path, "r").read().split("\n"): - if Compile.HAS_MAIN.match(line): - return True - return False - - - def __init__(self, source_path, binary_path): - self.source_path = os.path.abspath(source_path) + "/" - self.binary_path = os.path.abspath(binary_path) + "/" - self.wallaby_library_avaliable = os.path.isfile("/usr/local/lib/libaurora.so") and os.path.isfile("/usr/local/lib/libdaylite.so") - if not self.wallaby_library_avaliable: - Logging.warning("Wallaby library not found. All Wallaby functions are unavaliable.") - if platform.machine() != "armv7l": - Logging.warning("Wrong processor architecture! Generated binaries will not run on Wallaby Controllers.") - - - def compile(self, path, relpath, handler=None): - if relpath.endswith(".c") and Compile.is_valid_c_program(path + relpath): - name = "-".join(relpath.split("/")).rstrip(".c") - full_path = self.binary_path + name - if not os.path.exists(full_path): - os.mkdir(full_path) - error = True - command = ["gcc", "-pipe", "-O0", "-lwallaby", "-I%s" % self.source_path, "-o", "%s" % full_path + "/botball_user_program", path + relpath] - if not self.wallaby_library_avaliable: - del command[command.index("-lwallaby")] - p = Popen(command, stdout=PIPE, stderr=PIPE) - error = False if p.wait() == 0 else True - result = "" - for line in p.communicate(): - result += line.decode() - if handler != None: - handler.send({"failed" : error, "returned" : result, "relpath" : relpath}, self.handler.reverse_routes[self]) - - class Subscribe(Route): EDITOR = 1 @@ -76,13 +42,12 @@ class Subscribe(Route): if "channel" in data: if data["channel"] in Subscribe.CHANNELS: handler.channel = data["channel"] - handler.broadcast.add(handler, handler.channel) if handler.debug: Logging.info("'%s:%i' has identified as a %s client." % (handler.address, handler.port, "Editor" if handler.channel == Subscribe.EDITOR else "Controller" if handler.channel == Subscribe.WALLABY else "Web" if handler.channel == Subscribe.WEB else - "Unknown (will not subscribe to broadcast)")) + "Unknown")) if "name" in data: handler.name = data["name"] handler.routes["peers"].push_changes(handler) @@ -90,8 +55,8 @@ class Subscribe(Route): class WhoAmI(Route): def run(self, data, handler): - handler.send({"id" : handler.id_, - "user" : pwd.getpwuid(os.getuid()).pw_name}, + handler.send({"id" : handler.id_, + "user" : pwd.getpwuid(os.getuid()).pw_name}, handler.reverse_routes[self]) @@ -119,7 +84,7 @@ class Peers(Route): for channel in channels: self.subscribe(handler, channel) # Send on channels and on subscribe - self.send_connected_peers(handler, channels) + self.send_connected_peers(handler, channels) def send_connected_peers(self, handler, channels): @@ -169,9 +134,8 @@ class Peers(Route): class Handler(Server): - def setup(self, routes, broadcast, websockets, debug=False): + def setup(self, routes, websockets, debug=False): super().setup(routes, websockets, debug=debug) - self.broadcast = broadcast self.channel = None self.name = "Unknown" @@ -179,11 +143,9 @@ class Handler(Server): def ready(self): if self.debug: Logging.info("Handler for '%s:%d' ready." % (self.address, self.port)) - + def closed(self, code, reason): - if self.channel != None: - self.broadcast.remove(self, self.channel) if self.debug: Logging.info("'%s:%d' disconnected." % (self.address, self.port)) self.routes["peers"].push_changes(self) @@ -200,34 +162,31 @@ def folder_validator(folder): CONFIG_PATH = "server.cfg" -config = Config.Config() -config.add(Config.Option("server_address", ("127.0.0.1", 3077))) -config.add(Config.Option("debug", True, validator=lambda x: True if True or False else False)) -config.add(Config.Option("binary_path", "Binaries", validator=folder_validator)) -config.add(Config.Option("source_path", "Source", validator=folder_validator)) + +config = Config() +config.add(Option("fl0w_address", ("127.0.0.1", 3077))) +config.add(Option("behem0th_address", ("127.0.0.1", 3078))) +config.add(Option("dashb0ard_address", ("127.0.0.1", 8080))) +config.add(Option("debug", True, validator=lambda x: True if True or False else False)) +config.add(Option("path", "Content", validator=folder_validator)) try: - config = config.read_from_file(CONFIG_PATH) -except FileNotFoundError: - config.write_to_file(CONFIG_PATH) - config = config.read_from_file(CONFIG_PATH) + config = config.load(CONFIG_PATH) +except (IOError, ExceptionInConfigError): + config.dump(CONFIG_PATH) + config = config.load(CONFIG_PATH) -broadcast = Broadcast() -# Populating broadcast channels with all channels defined in Subscribe.Channels -for channel in Subscribe.CHANNELS: - broadcast.add_channel(channel) - -compile = Compile(config.source_path, config.binary_path) +#compile = Compile(config.source_path, config.binary_path) -server = make_server(config.server_address[0], config.server_address[1], +server = make_server(config.fl0w_address[0], config.fl0w_address[1], server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=None) server.initialize_websockets_manager() server.set_app(WebSocketWSGIApplication(handler_cls=Handler, - handler_args={"debug" : config.debug, "broadcast" : broadcast, + handler_args={"debug" : config.debug, "websockets" : server.manager.websockets, "routes" : {"info" : Info(), "whoami" : WhoAmI(), @@ -237,16 +196,21 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "peers" : Peers(), "sensor" : DummyPipe(), "identify" : DummyPipe(), - "list_programs" : DummyPipe(), - "run_program" : DummyPipe(), "std_stream" : DummyPipe(), "stop_programs" : DummyPipe(), "shutdown" : DummyPipe(), - "reboot" : DummyPipe()}})) + "reboot" : DummyPipe(), + "output" : DummyPipe()}})) try: Logging.header("Server loop starting.") + start_new_thread(run, (), {"host" : config.dashb0ard_address[0], + "port" : config.dashb0ard_address[1], "quiet" : True}) + Logging.info("Starting dashb0ard on 'http://%s:%d'" % (config.dashb0ard_address[0], + config.dashb0ard_address[1])) + Logging.info("Starting fl0w on 'ws://%s:%d'" % (config.fl0w_address[0], + config.fl0w_address[1])) server.serve_forever() except KeyboardInterrupt: Logging.header("Gracefully shutting down server.") diff --git a/Shared/Config.py b/Shared/Config.py deleted file mode 100644 index 26a509d..0000000 --- a/Shared/Config.py +++ /dev/null @@ -1,118 +0,0 @@ -from imp import load_source -from os.path import isfile -from marshal import dumps, loads -from types import FunctionType - - -class OptionDuplicateError(IndexError): - def __init__(self, name): - super(IndexError, self).__init__("'%s' already exists" % name) - - -class OptionNotFoundError(IndexError): - def __init__(self, name): - super(IndexError, self).__init__("'%s' does not exist" % name) - - -class NameMustBeStringError(Exception): - def __init__(self): - super(Exception, self).__init__("option names have to be strings") - - -def make_value(value): - if type(value) is str: - value = '"%s"' % value - elif type(value) in (list, tuple, dict): - value = str(value) - elif type(value) is FunctionType: - value = dumps(value.__code__) - return value - - -class Option: - def __init__(self, name, default_value, validator=None, comment=""): - if not type(name) is str: - raise NameMustBeStringError() - self.name = name - self.default_value = default_value - self.validator = validator - self.comment = comment - - -class Config: - def __init__(self, options=[], validation_failed=None, override_on_error=False): - if type(options) in (list, tuple): - for option in options: - if not type(option) is Option: - raise TypeError("all options must be of type Option") - else: - raise TypeError("options must be a list or tuple containing options of type Option") - self.options = options - self.validation_failed = validation_failed - self.override_on_error = override_on_error - - - def read_from_file(self, file): - if isfile(file): - config = load_source("config", file) - error = False - for option in self.options: - # Make sure all options are avaliable - if option.name not in dir(config): - setattr(config, option.name, option.default_value) - error = True - else: - # Make sure all validators pass - if option.validator != None: - value = getattr(config, option.name) - if not option.validator(value): - setattr(config, option.name, option.default_value) - if self.validation_failed != None: - self.validation_failed(option.name, value) - error = True - if self.override_on_error: - if error: - self.write_to_file(file) - return config - else: - raise FileNotFoundError() - - - def add(self, new_option): - if type(new_option) is Option: - for option in self.options: - if new_option.name == option.name: - raise OptionDuplicateError(option.name) - self.options.append(new_option) - else: - raise TypeError("invalid type supplied") - - - def remove(self, option): - if option in self.options: - del self.options[self.options.index(option)] - else: - raise OptionNotFoundError(option.name) - - - def write_to_file(self, file): - open(file, "w").write(self.get()) - - - def get(self): - contains_function = False - out = "" - for option in self.options: - value = make_value(option.default_value) - if type(option.default_value) is FunctionType: - if not contains_function: - out = "from marshal import loads; from types import FunctionType\n\n" + out - contains_function = True - value = 'FunctionType(loads(%s), globals(), "%s")' % (value, option.name) - out += "%s = %s%s\n" % (option.name, value, - (" # %s" % option.comment) if option.comment else "") - return out - - - def __repr__(self): - return self.get() \ No newline at end of file diff --git a/Shared/Disc0very.py b/Shared/Disc0very.py deleted file mode 100644 index ed3aff9..0000000 --- a/Shared/Disc0very.py +++ /dev/null @@ -1,97 +0,0 @@ -from socket import * -from random import getrandbits -from random import choice -from time import time -from time import sleep -import Utils -import Logging -from _thread import start_new_thread - - -HEY = "HEY".encode() -NAY = "NAY".encode() -WHAT = "WHAT".encode() - - -class MissingInterfaceError(TypeError): - def __init__(self): - super(TypeError, self).__init__("platform requires interface parameter.") - - -class Disc0very: - def __init__(self, port, interface=None, max_peers=32): - self.port = port - self.sock = socket(AF_INET, SOCK_DGRAM) - self.sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) - self.sock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) - self.sock.bind(("", self.port)) - self.ip_address = Utils.get_ip_address(interface) - self.discovering = False - self.enlisting = False - - - - # Not thread-safe if ran in parallel with __enlist - def discover(self, time_out=1): - self.sock.setblocking(False) - end_time = time() + time_out - servers = [] - self.sock.sendto(HEY, ('255.255.255.255', self.port)) - while time() < end_time: - try: - data, address = self.sock.recvfrom(512) - address = address[0] - # If enlisted peer responds with ip address - if data not in (HEY, NAY) and address != self.ip_address and not data in servers: - servers.append(data) - # If another peer is currently discovering - elif data == HEY and address != self.ip_address: - self.sock.sendto(NAY, ('255.255.255.255', self.port)) - return self.discover(time_out=time_out) - # If another peer gave up - elif data == NAY and address != self.ip_address: - return None - except BlockingIOError: - sleep((choice(range(1, 10)) / 2) / 10) - if len(servers) == 0: - return None - elif len(servers) > 1: - self.sock.sendto(WHAT, ('255.255.255.255', self.port)) - else: - return servers[0] - - - def enlist(self, interface, blocking=False): - if blocking: - self.__enlist(interface) - else: - start_new_thread(self.__enlist, (interface, )) - - - # Not thread-safe if ran in parallel with discover - # Interface should always be provided when using a Wallaby - # because wlan0 and wlan1 have an IP address assigned - def __enlist(self, interface=None): - self.sock.setblocking(True) - data = "" - while True: - try: - data, address = self.sock.recvfrom(512) - except BlockingIOError: - sleep(0.1) - if data == HEY: - self.sock.sendto(self.ip_address.encode(), ('255.255.255.255', self.port)) - elif data == WHAT: - Logging.error("Apparently more than one server is running. " - "Investigating...") - # Discover and if other server is found shutdown - - -if __name__ == "__main__": - disc0very = Disc0very(3077) - server = disc0very.discover() - if not server: - print("enlisting") - disc0very.enlist(None, blocking=True) - else: - print(server) \ No newline at end of file diff --git a/Shared/Highway.py b/Shared/Highway.py index ffc8233..90c679f 100644 --- a/Shared/Highway.py +++ b/Shared/Highway.py @@ -330,10 +330,10 @@ class Shared: data_repr = str(data).replace("\n", " ") if len(data_repr) > 80: data_repr = data_repr[:80] + "..." - Logging.info("Received '%s' on route '%s': %s (%s:%d)" % ( - type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict", - route, data_repr, self.address, - self.port)) + Logging.info("Received '%s' on route '%s': %s (%s:%d)" % ( + type(data).__name__ if not data_type == INDEXED_DICT else "indexed_dict", + route, data_repr, self.address, + self.port)) try: route = self.routes[route] except: diff --git a/Shared/Logging.py b/Shared/Logging.py index 6bd0e4d..52d0ab9 100644 --- a/Shared/Logging.py +++ b/Shared/Logging.py @@ -112,7 +112,6 @@ def success(message): if __name__ == "__main__": - import sys info("Hi!", color=UNDERLINE+BACKGROUND_GREEN+RED) header("This is a header") warning("This is a warning") # > stderr diff --git a/Shared/Meh.py b/Shared/Meh.py new file mode 100644 index 0000000..5d0611d --- /dev/null +++ b/Shared/Meh.py @@ -0,0 +1,328 @@ +from imp import load_source +from os.path import isfile +from sys import version + +class OptionDuplicateError(IndexError): + def __init__(self, name): + super(IndexError, self).__init__("'%s' already exists" % name) + + +class OptionNotFoundError(IndexError): + def __init__(self, name): + super(IndexError, self).__init__("'%s' does not exist" % name) + + +class NameMustBeStringError(Exception): + def __init__(self): + super(Exception, self).__init__("option names have to be strings") + + +class ValidationError(Exception): + def __init__(self, option): + super(Exception, self).__init__("invalid value for option '%s'" % option) + + +class UnsupportedTypeError(TypeError): + def __init__(self): + super(TypeError, self).__init__("only list, tuple, dict, bytes, " + "str, float, complex, int and bool are supported (same " + "thing applies to list, dict and tuple contents)") + + +class ExceptionInConfigError(Exception): + """ + Raised if an exception occurs while importing a config file. + + IN: error (hint: error that occured on import) + """ + def __init__(self, error): + self.error = error + super(Exception, self).__init__("error occured during config import (%s)" % + error.__class__.__name__) + +def validate_value(value): + type_value = type(value) + if type_value in (list, tuple): + for element in value: + if not validate_value(element): + return False + elif type_value is dict: + return validate_value(tuple(value.keys())) and validate_value(tuple(value.values())) + elif type_value in (bytes, str, float, complex, int, bool): + return True + else: + return False + return True + +def make_value(value): + if validate_value(value): + if type(value) is str: + value = '"%s"' % value + elif type(value) in (list, tuple, dict): + value = str(value) + return value + else: + raise UnsupportedTypeError() + + +class _EditableConfig: + """ + Automatically created proxy class. + + HINTS: + _values: All options with their respective values + _options: All Option instances that were originally added to + the Config instance + _file: Path to the config file + _validation_failed: Optional function that's called on a validation error + _debug: Debug mode on/off (obviously) + """ + def __init__(self, values, options, file, validation_failed=None, debug=False): + self._values = values + self._options = options + self._file = file + self._validation_failed = validation_failed + self._debug = debug + + + def __getattr__(self, name): + if name in self._values: + return self._values[name] + else: + raise AttributeError("config no attribute '%s'" % name) + + + def __setattr__(self, name, value): + if name == "_values" or name not in self._values: + self.__dict__[name] = value + else: + dump_required = False + for option in self._options: + if option.name == name: + if validate_value(value): + if option.validator != None: + if option.validator(value): + self._values[name] = value + dump_required = True + else: + if self._validation_failed != None: + self._validation_failed(option.name, value) + else: + raise ValidationError(option.name) + else: + self._values[name] = value + dump_required = True + else: + raise UnsupportedTypeError() + if dump_required: + if self._debug: + print("Rewriting config because the value of '%s' changed." % name) + open(self._file, "w").write(self._dumps()) + + + def _dumps(self): + out = "" + for option in self._options: + value = make_value(self._values[option.name]) + out += "%s = %s%s\n" % (option.name, value, + (" # %s" % option.comment) if option.comment else "") + return out.rstrip("\n") + + + def __repr__(self): + return self._dumps() + + +class Option: + + def __init__(self, name, default_value, validator=None, comment=""): + if not type(name) is str: + raise NameMustBeStringError() + if name.startswith("__"): + raise InvalidOptionName() + self._name = name + self.default_value = default_value + self.validator = validator + self.comment = comment + + + @property + def name(self): + return self._name + + @name.setter + def name(self, value): + if name.startswith("__"): + raise InvalidOptionName() + self._name = value + + + def __eq__(self, other): + if other.__class__ == Option: + return self.__dict__ == other.__dict__ + + + def __repr__(self): + return "%s = %s" % (self.name, str(self.default_value)) + + +class Config: + """ + The central element of Meh (TM). + IN: + options=[] (type: list, hint: a list of options) + validation_failed=None (type: function, hint: function accepting two + parameters that's called when a validation fails) + + Example usage: + from Meh import Config, Option + config = Config() + config.add(Option("number", 42, validator=lambda x: type(x) is int)) + + CONFIG_PATH = "awesome_config.cfg" + try: + config = config.load(CONFIG_PATH) + except IOError: + config.dump(CONFIG_PATH) + config = config.load(CONFIG_PATH) + + print(config.number) + """ + def __init__(self, options=[], validation_failed=None, debug=False): + if type(options) in (list, tuple): + for option in options: + if not option.__class__ == Option: + raise TypeError("all options must be of type Option") + else: + raise TypeError("options must be a list or tuple containing options of type Option") + self.options = options + self.validation_failed = validation_failed + self.debug = debug + self._iterator_index = 0 + + + def __iter__(self): + return self + + + def __next__(self): + if self._iterator_index < len(self.options): + self._iterator_index += 1 + return self.options[self._iterator_index - 1] + self._iterator_index = 0 + raise StopIteration + + + def load(self, file): + """ + Returns the actual read- and editable config + IN: file (type: str, hint: should be a valid path) + """ + if isfile(file): + try: + config = load_source("config", file) + except Exception as e: + raise ExceptionInConfigError(e) + option_missing = False + values = {} + for option in self.options: + # Make sure all options are avaliable (validators aren't run in this case + # because there are no values defined) + if option.name not in dir(config): + values[option.name] = option.default_value + option_missing = True + else: + # Retrieve the option value + value = getattr(config, option.name) + # Make sure validator passes + if option.validator != None: + # If validation doesn't pass + if not option.validator(value): + # Resort to default value + values[option.name] = option.default_value + if self.validation_failed != None: + self.validation_failed(option.name, value) + else: + raise ValidationError(option.name) + option_missing = True + # If validation passes + else: + values[option.name] = value + else: + values[option.name] = value + if option_missing: + self.dump(file) + return _EditableConfig(values, self.options, file, + validation_failed=self.validation_failed, debug=self.debug) + else: + error = "'%s' not found" % file + raise FileNotFoundError(error) if version.startswith("3") else IOError(error) + + + def __add__(self, other): + try: + self.add(other) + except TypeError: + return NotImplemented + return self + + + def __sub__(self, other): + try: + self.remove(other) + except TypeError: + return NotImplemented + return self + + + def add(self, option): + """ + Adds an option to a Config instance + IN: option (type: Option) + """ + if option.__class__ == Option: + for _option in self.options: + if option.name == _option.name: + raise OptionDuplicateError(_option.name) + self.options.append(option) + else: + raise TypeError("invalid type supplied") + + + def remove(self, option): + """ + Removes an option from a Config instance + IN: option (type: Option) + """ + if option.__class__ == Option: + if option in self.options: + del self.options[self.options.index(option)] + else: + raise OptionNotFoundError(option.name) + else: + raise TypeError("invalid type supplied") + + + def dump(self, file): + """ + Writes output of dumps() to the path provided + IN: file (type: str, hint: should be a valid path) + """ + open(file, "w").write(self.dumps()) + + + def dumps(self): + """ + Returns contents of config file as string + OUT: out (type: str, hint: config content) + """ + out = "" + for option in self.options: + value = make_value(option.default_value) + out += "%s = %s%s\n" % (option.name, value, + (" # %s" % option.comment) if option.comment else "") + return out.rstrip("\n") + + + def __repr__(self): + return self.dumps() \ No newline at end of file diff --git a/Shared/Utils.py b/Shared/Utils.py index bae4483..0972417 100644 --- a/Shared/Utils.py +++ b/Shared/Utils.py @@ -6,6 +6,8 @@ import struct import socket import fcntl import subprocess +import urllib + class HostnameNotChangedError(PermissionError): def __init__(self): @@ -13,11 +15,11 @@ class HostnameNotChangedError(PermissionError): class NotSupportedOnPlatform(OSError): def __init__(self): - super(OSError, self).__init__("feature not avaliable on OS") + super(OSError, self).__init__("feature not avaliable on OS") class PlaybackFailure(OSError): def __init__(self): - super(OSError, self).__init__("audio playback failed") + super(OSError, self).__init__("audio playback failed") def capture_trace(): @@ -38,7 +40,7 @@ def is_darwin(): def is_windows(): - return platform.uname().system == "Windows" + return platform.uname().system == "Windows" def set_hostname(hostname): @@ -82,6 +84,10 @@ def get_ip_address(ifname=None): return ip_address +def get_ip_from_url(url): + return urllib.parse.urlsplit(url).netloc.split(':')[0] + + def play_sound(path): if is_linux() or is_darwin(): try: @@ -89,4 +95,4 @@ def play_sound(path): except subprocess.CalledProcessError as e: raise PlaybackFailure() else: - raise NotSupportedOnPlatform() \ No newline at end of file + raise NotSupportedOnPlatform() diff --git a/Shared/bottle b/Shared/bottle new file mode 160000 index 0000000..5a6fc77 --- /dev/null +++ b/Shared/bottle @@ -0,0 +1 @@ +Subproject commit 5a6fc77c9c57501b08e5b4f9161ae07d277effa9 diff --git a/Shared/dashb0ard b/Shared/dashb0ard new file mode 160000 index 0000000..cca5173 --- /dev/null +++ b/Shared/dashb0ard @@ -0,0 +1 @@ +Subproject commit cca517355923990423a2c744bf2bc81080090ff3 diff --git a/Sublime/fl0w/CompileHighlight.sublime-syntax b/Sublime/fl0w/CompileHighlight.sublime-syntax deleted file mode 100644 index 3e997dc..0000000 --- a/Sublime/fl0w/CompileHighlight.sublime-syntax +++ /dev/null @@ -1,11 +0,0 @@ -%YAML 1.2 ---- -# http://www.sublimetext.com/docs/3/syntax.html -name: Compile Highlight -file_extensions: [] -hidden: true -scope: source.inspect -contexts: - main: - - match: \b(warning|error)\b - scope: keyword.control.c \ No newline at end of file diff --git a/Sublime/fl0w/Default (Linux).sublime-keymap b/Sublime/fl0w/Default (Linux).sublime-keymap deleted file mode 100644 index 00b52dc..0000000 --- a/Sublime/fl0w/Default (Linux).sublime-keymap +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "keys" : ["f8"], - "command" : "run" - }, - { - "keys" : ["f9"], - "command" : "stop" - }, - { - "keys" : ["f10"], - "command" : "sensor" - } -] \ No newline at end of file diff --git a/Sublime/fl0w/Default (OSX).sublime-keymap b/Sublime/fl0w/Default (OSX).sublime-keymap deleted file mode 100644 index 00b52dc..0000000 --- a/Sublime/fl0w/Default (OSX).sublime-keymap +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "keys" : ["f8"], - "command" : "run" - }, - { - "keys" : ["f9"], - "command" : "stop" - }, - { - "keys" : ["f10"], - "command" : "sensor" - } -] \ No newline at end of file diff --git a/Sublime/fl0w/Default (Windows).sublime-keymap b/Sublime/fl0w/Default (Windows).sublime-keymap deleted file mode 100644 index 00b52dc..0000000 --- a/Sublime/fl0w/Default (Windows).sublime-keymap +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "keys" : ["f8"], - "command" : "run" - }, - { - "keys" : ["f9"], - "command" : "stop" - }, - { - "keys" : ["f10"], - "command" : "sensor" - } -] \ No newline at end of file diff --git a/Sublime/fl0w/Shared b/Sublime/fl0w/Shared deleted file mode 120000 index 8c1ab6a..0000000 --- a/Sublime/fl0w/Shared +++ /dev/null @@ -1 +0,0 @@ -../../Shared \ No newline at end of file diff --git a/Sublime/fl0w/SublimeMenu.py b/Sublime/fl0w/SublimeMenu.py deleted file mode 100644 index 8ff2c59..0000000 --- a/Sublime/fl0w/SublimeMenu.py +++ /dev/null @@ -1,140 +0,0 @@ -FUNCTION = type(lambda: 1) - -class Input: - def __init__(self, caption, initial_text="", on_done=None, on_change=None, - on_cancel=None, kwargs={}): - self.caption = caption - self.initial_text = initial_text - self.on_done = on_done - self.on_change = on_change - self.on_cancel = on_cancel - self.kwargs = kwargs - - - def wrapped_on_done(self, input_): - if not self.on_done == None: - self.on_done(input_, **self.kwargs) - - - def wrapped_on_change(self, input_): - if not self.on_change == None: - self.on_change(input_, **self.kwargs) - - - def wrapped_on_cancel(self): - if not self.on_cancel == None: - self.on_cancel(**self.kwargs) - - - def invoke(self, window): - window.show_input_panel(self.caption, self.initial_text, - self.wrapped_on_done, self.wrapped_on_change, - self.wrapped_on_cancel) - - -class Entry: - def __init__(self, name, description="", action=None, kwargs={}, - sub_menu=None, input=None): - self.name = name - self.description = description - self.action = action - self.kwargs = kwargs - self.sub_menu = sub_menu - self.input = input - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - -class Menu: - def __init__(self, selected_index=-1, on_highlight=None, subtitles=True): - self.selected_index = selected_index - self.on_highlight = on_highlight - self.subtitles = subtitles - self.entries = {} - self.window = None - self.back = None - - def invoke(self, window, back=None): - self.window = window - self.back = back - entries = self.menu_entries - if back: - entries.insert(0, ["Back", - "Back to previous menu"] if self.subtitles else ["Back"]) - window.show_quick_panel(entries, self._action, - flags=0, selected_index=self.selected_index, - on_highlight=self.on_highlight) - - def _action(self, entry_id): - if entry_id != -1: - if self.back: - if entry_id != 0: - entry = self.entries[entry_id - 1] - else: - self.back.invoke(self.window, back=self.back.back) - return - else: - entry = self.entries[entry_id] - if entry.action != None: - entry.action(**entry.kwargs) - if entry.input != None: - entry.input.invoke(self.window) - if type(entry.sub_menu) is FUNCTION: - entry.sub_menu(entry).invoke(self.window, back=self) - elif entry.sub_menu != None: - entry.sub_menu.invoke(self.window, back=self) - - - @property - def menu_entries(self): - entries = [] - for entry_id in self.entries: - if self.subtitles: - entries.append([self.entries[entry_id].name, self.entries[entry_id].description]) - else: - entries.append([self.entries[entry_id].name]) - return entries - - - def __add__(self, other): - try: - self.add(other) - except TypeError: - return NotImplemented - return self - - - def __sub__(self, other): - try: - self.remove(other) - except TypeError: - return NotImplemented - return self - - - def add(self, entry): - if entry.__class__ == Entry: - if len(self.entries) > 0: - entry_id = tuple(self.entries.keys())[-1] + 1 - else: - entry_id = 0 - self.entries[entry_id] = entry - else: - raise TypeError("invalid type supplied") - - - def remove(self, entry): - if entry.__class__ == Entry: - if entry in self.entries.values(): - found_entry_id = None - for entry_id in self.entries: - if self.entries[entry_id] == entry: - found_entry_id = entry_id - if found_entry_id != None: - del self.entries[entry_id] - else: - raise TypeError("invalid type supplied") - - def clear(self): - self.entries = {} diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py deleted file mode 100644 index fa2ea18..0000000 --- a/Sublime/fl0w/fl0w.py +++ /dev/null @@ -1,749 +0,0 @@ -from sys import path -import os -from time import strftime -from functools import partial -import re - -fl0w_path = os.path.dirname(os.path.realpath(__file__)) -shared_path = os.path.dirname(os.path.realpath(__file__)) + "/Shared/" -if fl0w_path not in path: - path.append(fl0w_path) -if shared_path not in path: - path.append(shared_path) - - -import sublime -import sublime_plugin - -from Highway import Client, Route, Pipe, DummyPipe -from Utils import get_hostname - -from SublimeMenu import * -import Logging - -import webbrowser -import threading -from time import sleep -import os - -CHANNEL = 1 -FL0W_STATUS = "fl0w" - -def plugin_unloaded(): - for window in windows: - if hasattr(window, "fl0w") and window.fl0w.connected: - window.fl0w.invoke_disconnect() - for sensor_type in ("analog", "digital"): - window.active_view().erase_phantoms(sensor_type) - - -PARENTHESES_REGEX = re.compile("\((.*?)\)") -STYLE_OPEN = "" -STYLE_CLOSE = "" - -ERROR_OPEN = "" -ERROR_CLOSE = "" - -windows = [] -views = [] -sensor_phantoms = [] - -def set_status(status, window): - window.active_view().set_status(FL0W_STATUS, - "fl0w: %s" % status) - -class Target: - def __init__(self, id_, name): - self.id = id_ - self.name = name - - -class Fl0wClient(Client): - def setup(self, routes, fl0w, debug=False): - super().setup(routes, debug=debug) - self.fl0w = fl0w - - - def ready(self): - self.fl0w.connected = True - if self.fl0w.debug: - Logging.info("Connection ready!") - # Enlist on editor channel - self.send({"channel" : CHANNEL, "name" : get_hostname()}, "subscribe") - # Subscribe to controller channel - self.send({"subscribe" : [2]}, "peers") - - - def closed(self, code, reason): - self.fl0w.invoke_disconnect() - if self.fl0w.debug: - Logging.info("Connection closed: %s (%s)" % (reason, code)) - - - def peer_unavaliable(self, peer): - sublime.error_message("The specifed controller is not connected anymore.") - if self.fl0w.target.id == peer: - self.fl0w.target = None - - - class Info(Route): - def run(self, data, handler): - info = "" - for key in data: - info += "%s: %s\n" % (key.capitalize(), ", ".join(data[key])) - sublime.message_dialog(info) - handler.fl0w.meta.invoke(handler.fl0w.window, back=handler.fl0w.main_menu) - - - class Sensor(Pipe): - def run(self, data, peer, handler): - handler.fl0w.subscriptions_lock.acquire() - for sensor_phantom in handler.fl0w.subscriptions: - sensor_phantom.update_sensor_values(data) - handler.fl0w.subscriptions_lock.release() - - - class Peers(Route): - def start(self, handler): - self.selected_action_menu = None - - def run(self, data, handler): - handler.fl0w.controller_menu.clear() - if handler.fl0w.target != None: - if not handler.fl0w.target.id in data: - handler.fl0w.target = None - for id_ in data: - action_menu = Menu() - power_menu = Menu() - utilities_menu = Menu() - action_menu.id_ = id_ - action_menu.name = data[id_]["name"] - action_menu += Entry("Set Target", - "Set controller as target for program execution and sensor readouts", - action=partial(self.set_target, - handler, id_, data[id_]["name"])) - action_menu += Entry("Run program", - "Run a botball program on the controller", - action=partial(handler.pipe, None, "list_programs", id_)) - action_menu += Entry("Stop programs", - "Stop all currently running botball programs", - action=partial(handler.pipe, None, "stop_programs", id_)) - utilities_menu += Entry("Set Name", - "Sets the hostname of the selected controller", - action=partial(lambda handler, id_: Input("New Hostname:", - initial_text=data[id_]["name"], - on_done=lambda hostname: handler.pipe( - {"set" : hostname}, - "hostname", id_)).invoke(handler.fl0w.window), handler, id_)) - utilities_menu += Entry("Processes", - "Lists processes currently running on controller", - action=partial(handler.pipe, None, "processes", id_)) - utilities_menu += Entry("Identify", - "Plays an identification sound on the controller.", - action=partial(handler.pipe, None, "identify", id_)) - action_menu += Entry("Utilities", "Stuff you might need but probably won't", - sub_menu=utilities_menu) - power_menu += Entry("Shutdown", - "Shutdown the controller", - action=partial(handler.pipe, None, "shutdown", id_)) - power_menu += Entry("Reboot", - "Reboot the controller", - action=partial(handler.pipe, None, "reboot", id_)) - action_menu += Entry("Power", "Power related actions", sub_menu=power_menu) - action_menu.back = handler.fl0w.controller_menu - handler.fl0w.controller_menu += Entry(data[id_]["name"], id_, sub_menu=action_menu, - action=self.set_selected_action_menu, - kwargs={"selected_action_menu" : action_menu}) - - - def set_target(self, handler, peer, name): - handler.fl0w.target = Target(peer, name) - if handler.fl0w.debug: - set_status("Target: %s" % peer, handler.fl0w.window) - - - def set_selected_action_menu(self, selected_action_menu): - self.selected_action_menu = selected_action_menu - - - class Processes(Pipe): - def run(self, data, peer, handler): - view = handler.fl0w.window.new_file() - view.set_name("Processes") - view.settings().set("draw_indent_guides", False) - for line in data: - view.run_command("append", {"characters": line + "\n"}) - view.set_read_only(True) - - - class ListPrograms(Pipe): - def run(self, data, peer, handler): - program_menu = Menu(subtitles=False) - for program in data: - program_menu += Entry(program, - action=partial(self.run_program, - handler, handler.routes["peers"].selected_action_menu.id_, - program)) - program_menu.invoke(handler.fl0w.window, - back=handler.routes["peers"].selected_action_menu) - - def run_program(self, handler, id_, program): - handler.pipe(program, "run_program", id_) - - - class RunProgram(Pipe): - PROGRAM_NOT_FOUND = 1 - - def run(self, data, peer, handler): - if data == self.__class__.PROGRAM_NOT_FOUND: - sublime.error_message("Program not found.") - - - class StopPrograms(Pipe): - NO_PROGRAMS_RUNNING = 1 - - def run(self, data, peer, handler): - if data == self.__class__.NO_PROGRAMS_RUNNING: - sublime.error_message("No programs running.") - - - class StdStream(Pipe): - def start(self, handler): - self.output_panels = {} - - self.lock = threading.RLock() - self.buffer = {} - - self.handler = None - - self.fetcher = self.Fetcher(self.buffer, self.write_to_panel, - self.lock) - self.fetcher.start() - - - def create_output_panel(self, window, peer): - view = window.create_output_panel(peer) - view.settings().set("draw_white_space", False) - view.settings().set("draw_indent_guides", False) - view.settings().set("gutter", False) - view.settings().set("line_numbers", False) - view.set_read_only(True) - return view - - - def run(self, data, peer, handler): - self.handler = handler - if type(data) is str: - self.lock.acquire() - # try/except is faster than an explicit if as long as the - # condition is not met - try: - self.buffer[peer].append(data) - except KeyError: - self.buffer[peer] = [] - self.create_output_panel(handler.fl0w.window, peer) - self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer) - self.buffer[peer].append(data) - self.lock.release() - # Meta info comes in so infrequently that the conditional logic would - # slow down the regular output streaming - elif type(data) is dict: - meta_text = "" - if "exit_code" in data: - meta_text += "Program finished with exit code: %d\n" % data["exit_code"] - self.lock.acquire() - # try/except is faster than an explicit if as long as the - # condition is not met - # function call is also slower - try: - self.buffer[peer].append(meta_text) - except KeyError: - self.buffer[peer] = [] - self.create_output_panel(handler.fl0w.window, peer) - self.output_panels[peer] = self.create_output_panel(handler.fl0w.window, peer) - self.buffer[peer].append(meta_text) - self.lock.release() - - - - def write_to_panel(self, text, peer): - self.output_panels[peer].set_read_only(False) - self.output_panels[peer].run_command("append", {"characters": text, "scroll_to_end" : True}) - self.output_panels[peer].set_read_only(True) - self.handler.fl0w.window.run_command("show_panel", {"panel": "output.%s" % peer}) - - - # Sublime gets quite overwhelmed when faced with typical - # "while (1) { printf(...)}" output. - # That's why instead of directly writing to the view all received text - # is bundled together after a fixed period of time. - class Fetcher(threading.Thread): - def __init__(self, buffer, write_to_panel, lock, push_rate=0.2): - threading.Thread.__init__(self) - self.buffer = buffer - self.write_to_panel = write_to_panel - self.lock = lock - self.push_rate = push_rate - self.daemon = True - - def run(self): - while True: - self.lock.acquire() - for peer in self.buffer: - if len(self.buffer[peer]) > 0: - self.write_to_panel( - "".join(self.buffer[peer]), - peer) - self.buffer[peer] = [] - self.lock.release() - sleep(self.push_rate) - - -class Fl0w: - def __init__(self, window, debug=False): - self.settings = sublime.load_settings("fl0w.sublime-settings") - self.window = window - self.folder = window.folders()[0] - if self.folder != "/": - self.folder = self.folder + "/" - - self.connected = False - - self.subscriptions = {} - self.subscriptions_lock = threading.Lock() - self._combined_subscriptions = {"analog" : [], "digital" : []} - - self._target = None - self._debug = debug - - - self.start_menu = Menu() - self.start_menu += Entry("Connect", "Connect to a fl0w server", - action=partial(Input("Address:Port (auto-connect nyi)", - initial_text=self.settings.get("address", "127.0.0.1:3077"), - on_done=self.invoke_connect).invoke, self.window)) - self.start_menu += Entry("About", "Information about fl0w", - action=self.invoke_about) - - self.debug_menu = Menu(subtitles=False) - self.debug_menu += Entry("On", - action=lambda: self.set_debug(True)) - self.debug_menu += Entry("Off", - action=lambda: self.set_debug(False)) - - - self.settings = Menu() - self.settings += Entry("Debug", "Toggle debug mode", - sub_menu=self.debug_menu) - - - self.meta = Menu() - self.meta += Entry("Info", "Server info", - action=lambda: self.ws.send(None, "info")) - self.meta_entry = Entry("Meta", "Debug information about fl0w", - sub_menu=self.meta) - if self.debug: - self.main_menu += self.meta_entry - - - self.main_menu = Menu() - self.controller_menu = Menu() - self.main_menu += Entry("Controllers", "All connected controllers", - sub_menu=self.controller_menu) - self.main_menu += Entry("Settings", "General purpose settings", - sub_menu=self.settings) - self.main_menu += Entry("Disconnect", "Disconnect from server", - action=self.invoke_disconnect) - - # Patch all sensor phantom that have been created before a fl0w instance - # was attached to the window - for sensor_phantom in sensor_phantoms: - if sensor_phantom.window.id() == self.window.id(): - sensor_phantom.fl0w = self - if self.debug: - Logging.info("Patched sensor phantom '%s'" % str(sensor_phatom)) - - - @property - def target(self): - return self._target - - - @target.setter - def target(self, target): - if self.target != None: - self.ws.pipe("unsubscribe", "sensor", self.target.id) - self._target = target - if target != None: - set_status("Target: %s (%s)" % (target.name, target.id), self.window) - if self.combined_subscriptions != {"analog" : [], "digital" : []}: - self.ws.pipe({"subscribe" : self.combined_subscriptions}, "sensor", target.id) - else: - set_status("The target has become unavaliable.", self.window) - - - @property - def debug(self): - return self._debug - - - def set_debug(self, debug): - self.debug = debug - - - @debug.setter - def debug(self, debug): - if debug: - self._debug = True - if not self.meta_entry in self.main_menu.entries.values(): - self.main_menu += self.meta_entry - else: - self._debug = False - self.main_menu -= self.meta_entry - set_status("Debug set to %s" % self._debug, self.window) - - - # Could be simplified because only one view can be active at any time. - # This would definetly lead to some major performace improvements on - # view switching and less unnecessary unsubscribes. - - # On the other hand it might be a good idea to leave it in and provide - # an option to disable aggressive unsubscribes - @property - def combined_subscriptions(self): - return self._combined_subscriptions - - - @combined_subscriptions.setter - def combined_subscriptions(self, combined_subscriptions_): - if self.combined_subscriptions != combined_subscriptions_: - self._combined_subscriptions = combined_subscriptions_ - if self.connected and self.target != None: - self.ws.pipe("unsubscribe", "sensor", self.target.id) - if combined_subscriptions_ != {"analog" : [], "digital" : []}: - self.ws.pipe({"subscribe" : combined_subscriptions_}, "sensor", - self.target.id) - - - def subscribe(self, sensor_phatom, subscriptions): - self.subscriptions_lock.acquire() - self.subscriptions[sensor_phatom] = subscriptions - self.subscriptions_lock.release() - self.make_subscriptions() - - - - def unsubscribe(self, sensor_phantom): - if sensor_phantom in self.subscriptions: - """ - print("Lock will be aquired.") - self.subscriptions_lock.acquire() - print("Lock was aquired.") - del self.subscriptions[sensor_phantom] - print("Lock will be released.") - self.subscriptions_lock.release() - print("Lock was released.") - """ - # Temporary solution, locking caused sublime to freeze - # Could cause problems if lots (> 100) views are open. - self.subscriptions[sensor_phantom] = {"analog" : [], "digital" : []} - self.make_subscriptions() - - - def make_subscriptions(self): - combined_subscriptions = {"analog" : [], "digital" : []} - for sensor_phantom in self.subscriptions: - for sensor_type in ("analog", "digital"): - combined_subscriptions[sensor_type] = list( - set(combined_subscriptions[sensor_type]) | - set(self.subscriptions[sensor_phantom][sensor_type]) - ) - self.combined_subscriptions = combined_subscriptions - - - def run_program(self, path): - if self.connected and self.target != None: - relpath = os.path.relpath(path, self.folder) - if os.path.isfile(self.folder + relpath): - if self.debug: - Logging.info("Running program '%s'" % relpath) - self.ws.pipe(relpath.rstrip(".c"), "run_program", self.target.id) - - - def stop_programs(self): - if self.connected and self.target != None: - self.ws.pipe(None, "stop_programs", self.target.id) - - - def invoke_start_menu(self): - self.start_menu.invoke(self.window) - - - def invoke_main_menu(self): - self.main_menu.invoke(self.window) - - - def invoke_about(self): - if sublime.ok_cancel_dialog("fl0w by @robot0nfire", "robot0nfire.com"): - webbrowser.open("http://robot0nfire.com") - - - - def connect(self, address): - try: - self.ws = Fl0wClient(address) - self.ws.setup({"info" : Fl0wClient.Info(), "peers" : Fl0wClient.Peers(), - "processes" : Fl0wClient.Processes(), - "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor(), - "std_stream" : Fl0wClient.StdStream(), "run_program" : Fl0wClient.RunProgram(), - "stop_programs" : Fl0wClient.StopPrograms()}, - self, debug=True) - self.ws.connect() - sublime.set_timeout_async(self.ws.run_forever, 0) - set_status("Connection opened '%s'" % self.folder, self.window) - self.connected = True - self.settings.set("address", address) - except OSError as e: - sublime.error_message("Error during connection creation:\n %s" % str(e)) - - - - def invoke_connect(self, address): - # Will be removed once autoconnect works - self.connect("ws://%s" % address) - - - def invoke_disconnect(self): - if self.connected: - for sensor_phantom in sensor_phantoms: - if sensor_phantom.window.id() == self.window.id(): - sensor_phantom.enabled = False - self.target = None - self.ws.close() - set_status("Connection closed '%s'" % self.folder, self.window) - self.connected = False - - -class Fl0wCommand(sublime_plugin.WindowCommand): - def run(self): - valid_window_setup = True - folder_count = len(self.window.folders()) - if folder_count > 1: - sublime.error_message("Only one open folder per window is allowed.") - valid_window_setup = False - elif folder_count == 0: - sublime.error_message("No folder open in window.") - valid_window_setup = False - if valid_window_setup: - if not hasattr(self.window, "fl0w"): - folder = self.window.folders()[0] - files = os.listdir(folder) - if not ".no-fl0w" in files: - if not ".fl0w" in files: - open(folder + "/.fl0w", 'a').close() - self.window.fl0w = Fl0w(self.window) - windows.append(self.window) - self.window.fl0w.start_menu.invoke(self.window) - else: - self.window.fl0w = Fl0w(self.window) - windows.append(self.window) - self.window.fl0w.start_menu.invoke(self.window) - else: - sublime.error_message("fl0w can't be opened in your current directory (.no-fl0w file exists)") - else: - if not self.window.fl0w.connected: - self.window.fl0w.invoke_start_menu() - else: - self.window.fl0w.invoke_main_menu() - else: - if hasattr(self.window, "fl0w"): - sublime.error_message("Window setup was invalidated (Don't close or open any additional folders in a fl0w window)") - self.window.fl0w.invoke_disconnect() - - -class RunCommand(sublime_plugin.WindowCommand): - def run(self): - if hasattr(self.window, "fl0w"): - if self.window.fl0w.connected: - if self.window.fl0w.target == None: - sublime.error_message("A target controller has to be set to " - "run programs.") - else: - file_name = self.window.active_view().file_name() - if file_name != None and file_name.endswith(".c"): - self.window.fl0w.run_program(file_name) - else: - sublime.error_message("fl0w is not connected.") - else: - sublime.error_message("fl0w is not running in your current window.") - -class StopCommand(sublime_plugin.WindowCommand): - def run(self): - if hasattr(self.window, "fl0w"): - if self.window.fl0w.connected: - if self.window.fl0w.target == None: - sublime.error_message("A target controller has to be set to " - "stop programs.") - else: - self.window.fl0w.stop_programs() - else: - sublime.error_message("fl0w is not connected.") - else: - sublime.error_message("fl0w is not running in your current window.") - - -class SensorCommand(sublime_plugin.WindowCommand): - def run(self): - if hasattr(self.window, "fl0w"): - if self.window.fl0w.connected: - if self.window.fl0w.target == None: - sublime.error_message("A target controller has to be set to " - "enable inline sensor readouts.") - else: - view_id = self.window.active_view().id() - for view in views: - if view.id() == view_id: - view.sensor_phantom.enabled = not view.sensor_phantom.enabled - view_file_name = view.file_name() - if view_file_name == None: - view_file_name = "untitled" - set_status("%s sensor phantoms for '%s'." % ( - "Enabled" if view.sensor_phantom.enabled else "Disabled", - view_file_name), - self.window) - else: - sublime.error_message("fl0w is not connected.") - else: - sublime.error_message("fl0w is not running in your current window.") - - - -class SensorPhantom(sublime_plugin.ViewEventListener): - def __init__(self, view): - self.view = view - self.view.sensor_phantom = self - if not view in views: - views.append(view) - self.window = view.window() - - # Is patched by the fl0w instance that is in control of the same window - self.fl0w = None - self._enabled = False - - self.previously_enabled = False - - self._matches = {"analog" : [], "digital" : []} - - self.timeout_scheduled = False - self.needs_update = False - - for window in windows: - if hasattr(window, "fl0w"): - self.fl0w = window.fl0w - if not self in sensor_phantoms: - sensor_phantoms.append(self) - - - @property - def enabled(self): - return self._enabled - - - @enabled.setter - def enabled(self, enabled_): - if enabled_: - if self.fl0w != None: - self.find_matches() - self.fl0w.subscribe(self, self.subscriptions) - self._enabled = True - else: - if self.fl0w != None: - self.fl0w.unsubscribe(self) - for sensor_type in ("analog", "digital"): - self.view.erase_phantoms(sensor_type) - self._enabled = False - - - @property - def matches(self): - return self._matches - - @matches.setter - def matches(self, matches_): - if not matches_ == self.matches: - self._matches = matches_ - self.fl0w.subscribe(self, self.subscriptions) - - - @property - def subscriptions(self): - subscriptions_ = {"analog" : [], "digital" : []} - for sensor_type in ("analog", "digital"): - subscriptions_[sensor_type] = [sensor[0] for sensor in self.matches[sensor_type]] - return subscriptions_ - - - def find_matches(self): - matches = {"analog" : [], "digital" : []} - # Don't do any calculations on 1MB or larger files - if self.view.size() < 2**20: - for method_name in ("analog", "digital"): - candidates = self.view.find_all("%s\(\d*\)" % method_name) - for candidate in candidates: - line = self.view.substr(candidate) - port_candidates = re.findall(PARENTHESES_REGEX, line) - if len(port_candidates) == 1: - if port_candidates[0].isnumeric(): - matches[method_name].append( - ( - int(port_candidates[0]), - sublime.Region(self.view.line(candidate.a).b) - )) - self.matches = matches - - # Called by fl0w instance - def update_sensor_values(self, readouts): - for sensor_type in ("analog", "digital"): - self.view.erase_phantoms(sensor_type) - for match in self.matches[sensor_type]: - try: - self.view.add_phantom(sensor_type, match[1], - STYLE_OPEN + str(readouts[sensor_type][str(match[0])]) + STYLE_CLOSE, - sublime.LAYOUT_INLINE) - except KeyError: - self.view.add_phantom(sensor_type, match[1], - ERROR_OPEN + "!" + ERROR_CLOSE, - sublime.LAYOUT_INLINE) - - - def handle_timeout(self): - self.timeout_scheduled = False - if self.needs_update: - self.needs_update = False - self.find_matches() - - - def on_modified(self): - if self.enabled: - if self.timeout_scheduled: - self.needs_update = True - else: - sublime.set_timeout(lambda: self.handle_timeout(), 500) - self.find_matches() - - - - def on_deactivated(self): - self.previously_enabled = self.enabled - if self.enabled: - self.enabled = False - - def on_activated(self): - if not self.enabled and self.previously_enabled: - self.enabled = True - - - def __del__(self): - self.enabled = False - if self in sensor_phantoms: - del sensor_phantoms[sensor_phantoms.index(self)] diff --git a/Sublime/fl0w/fl0w.sublime-commands b/Sublime/fl0w/fl0w.sublime-commands deleted file mode 100644 index a2b89be..0000000 --- a/Sublime/fl0w/fl0w.sublime-commands +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "caption": "fl0w: Menu", - "command": "fl0w" - } -] \ No newline at end of file diff --git a/Sublime/fl0w/fl0w.sublime-settings b/Sublime/fl0w/fl0w.sublime-settings deleted file mode 100644 index 51a7f3b..0000000 --- a/Sublime/fl0w/fl0w.sublime-settings +++ /dev/null @@ -1,4 +0,0 @@ -{ - "server_address": "", // Last server address - "compression_level": 2 -} diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 24b5e99..7c85393 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -1,6 +1,6 @@ from Highway import Route, Pipe, Client +from Meh import Config, Option, ExceptionInConfigError import Logging -import Config import Utils import socket @@ -10,27 +10,24 @@ import sys import subprocess from random import randint from _thread import start_new_thread +from ctypes import cdll import threading +import json +import re + CHANNEL = 2 IS_WALLABY = Utils.is_wallaby() -PATH = "/home/root/Documents/KISS/bin/" if IS_WALLABY else (sys.argv[1] if len(sys.argv) > 1 else None) - -PATH = os.path.abspath(PATH) - -if PATH[-1] != "/": - PATH = PATH + "/" - LIB_WALLABY = "/usr/lib/libwallaby.so" -WALLABY_PROGRAMS = "/root/Documents/KISS/bin/" -if not PATH: - Logging.error("No path specified. (Necessary on simulated Wallaby controllers.)") - exit(1) +CONFIG_PATH = "wallaby.cfg" -if not IS_WALLABY: - Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.") +config = Config() +config.add(Option("server_address", "ws://127.0.0.1:3077")) +config.add(Option("output_unbuffer", "stdbuf")) +config.add(Option("identify_sound", "Wallaby/identify.wav", + validator=lambda sound: os.path.isfile(sound))) class SensorReadout: @@ -40,7 +37,7 @@ class SensorReadout: MODES = tuple(NAMED_MODES.keys()) - def __init__(self, handler, poll_rate=0.5): + def __init__(self, handler, poll_rate=0.2): self.poll_rate = poll_rate self.handler = handler self.peer_lock = threading.Lock() @@ -154,22 +151,11 @@ class SensorReadout: class Identify(Pipe): def run(self, data, peer, handler): - Utils.play_sound(config.identify_sound) - if handler.debug: - Logging.success("I was identified!") - - -class ListPrograms(Pipe): - def run(self, data, peer, handler): - programs = [] - if os.path.isdir(PATH): - for program in os.listdir(PATH): - if "botball_user_program" in os.listdir(PATH + program): - programs.append(program) - else: - Logging.error("Harrogate folder structure does not exist. " - "You broke something, mate.") - handler.pipe(programs, handler.reverse_routes[self], peer) + try: + Utils.play_sound(config.identify_sound) + except Utils.PlaybackFailure: + Logging.error("Could not play identification sound") + Logging.success("I was identified!") class StopPrograms(Pipe): @@ -178,7 +164,7 @@ class StopPrograms(Pipe): def run(self, data, peer, handler): if handler.debug: Logging.info("Stopping all botball programs.") - if subprocess.call(["killall", "botball_user_program"], + if subprocess.call(["killall", "botball_user_program"], stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT): handler.pipe(self.__class__.NO_PROGRAMS_RUNNING, "stop_programs", peer) @@ -196,7 +182,7 @@ class RunProgram(Pipe): data = data + "/" path = "%s%s/botball_user_program" % (PATH, data) if os.path.isfile(path): - program = subprocess.Popen(self.command + [path], + program = subprocess.Popen(self.command + [path], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) start_new_thread(self.stream_stdout, (program, peer, handler)) else: @@ -252,6 +238,8 @@ class Sensor(Pipe): self.sensor_readout = SensorReadout(handler) + + class Shutdown(Pipe): def run(self, data, peer, handler): try: @@ -281,21 +269,6 @@ class WhoAmI(Route): handler.send(None, "whoami") -class Hostname(Pipe): - def run(self, data, peer, handler): - if type(data) is dict: - if "set" in data: - try: - Utils.set_hostname(str(data["set"])) - except Utils.HostnameNotChangedError: - if IS_WALLABY: - Logging.error("Hostname change unsuccessful. " - "Something is wrong with your Wallaby.") - else: - Logging.warning("Hostname change unsuccessful. " - "This seems to be a dev-system, so don't worry too " - "much about it.") - class Processes(Pipe): def run(self, data, peer, handler): @@ -304,43 +277,90 @@ class Processes(Pipe): "processes", peer) +class Output(Pipe): + def __init__(self): + self.content = [] + self.handler = None + self.subscribers = [] + + self.escape_regex = re.compile(r"(\[\d+m?)") + + self.stdout = Output.Std(Logging.stdout, self.push) + self.stderr = Output.Std(Logging.stderr, self.push) + + Logging.stdout = self.stdout + Logging.stderr = self.stderr + + def run(self, data, peer, handler): + if data == "subscribe": + self.subscribers.append(peer) + elif data == "unsubscribe": + self.unsubscribe(peer) + + + def start(self, handler): + self.handler = handler + + + def push(self, s): + s = self.escape_regex.sub("", s) + for peer in self.subscribers: + self.handler.pipe(s, "output", peer) + + + def unsubscribe(self, peer): + if peer in self.subscribers: + del self.subscribers[self.subscribers.index(peer)] + + class Std: + def __init__(self, old_std, write_callback): + self.old_std = old_std + self.write_callback = write_callback + + + def write(self, s): + self.old_std.write(s) + self.write_callback(s) + + + def flush(self): + self.old_std.flush() + + class Handler(Client): def setup(self, routes, debug=False): super().setup(routes, debug=debug) + def peer_unavaliable(self, peer): if self.debug: Logging.info("Unsubscribing '%s' from all sensor updates." % peer) self.routes["sensor"].sensor_readout.unsubscribe_all(peer) - - -CONFIG_PATH = "wallaby.cfg" - -config = Config.Config() -config.add(Config.Option("server_address", "ws://127.0.0.1:3077")) -config.add(Config.Option("debug", False, validator=lambda x: True if True or False else False)) -config.add(Config.Option("output_unbuffer", "stdbuf")) -config.add(Config.Option("identify_sound", "Wallaby/identify.wav", - validator=lambda x: os.path.isfile(x))) - -try: - config = config.read_from_file(CONFIG_PATH) -except FileNotFoundError: - config.write_to_file(CONFIG_PATH) - config = config.read_from_file(CONFIG_PATH) + self.routes["output"].unsubscribe(peer) try: - ws = Handler(config.server_address) - # setup has to be called before the connection is established - ws.setup({"subscribe" : Subscribe(), "hostname" : Hostname(), - "processes" : Processes(), "sensor" : Sensor(), - "identify" : Identify(), "list_programs" : ListPrograms(), - "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer), + config = config.load(CONFIG_PATH) +except (IOError, ExceptionInConfigError): + config.dump(CONFIG_PATH) + config = config.load(CONFIG_PATH) + + +try: + while 1: + ws = Handler(config.server_address) + # setup has to be called before the connection is established + ws.setup({"subscribe" : Subscribe(), "sensor" : Sensor(), + "identify" : Identify(), "whoami" : WhoAmI(), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), - "reboot" : Reboot()}, - debug=config.debug) - ws.connect() + "reboot" : Reboot(), "output" : Output()}, + debug=False) + try: + ws.connect() + break + except ConnectionRefusedError: + Logging.warning("Server not running... Retrying in 5s") + time.sleep(5) ws.run_forever() except KeyboardInterrupt: - ws.close() \ No newline at end of file + ws.close() diff --git a/Wallaby/identify.wav b/Wallaby/identify.wav index 4ab0ae3..8a444b1 100644 --- a/Wallaby/identify.wav +++ b/Wallaby/identify.wav @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c66eb1c4a17efbeaf27e2dae37d547886b447d72a75168fe0d439d699f25fcb -size 75918 +oid sha256:81832bc5b34359bc87173dc234cbb30ea23aa541814809ad16eb1ba9bd7335af +size 22858 diff --git a/fl0w.py b/fl0w.py index a446800..bac79b4 100644 --- a/fl0w.py +++ b/fl0w.py @@ -1,3 +1,4 @@ from sys import path -from os.path import abspath -path.append(abspath("Shared")) \ No newline at end of file +from os.path import abspath, dirname, realpath, join + +path.append(join(abspath(dirname(realpath(__file__))), 'Shared'))