From bf3f32c0d3f04db30435c5391332fbd66f6a13f4 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 23 Feb 2017 12:01:04 +0100 Subject: [PATCH 01/36] Deprecated, was not used anywhere --- Server/Broadcast.py | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 Server/Broadcast.py 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 From 5dfeee59777e92ed0a13f4917f6f8c44210ed64a Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 23 Feb 2017 12:01:43 +0100 Subject: [PATCH 02/36] Replaced Config with newer version --- Shared/Config.py | 118 ----------------- Shared/Meh.py | 328 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 118 deletions(-) delete mode 100644 Shared/Config.py create mode 100644 Shared/Meh.py 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/Meh.py b/Shared/Meh.py new file mode 100644 index 0000000..e704158 --- /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 From 2d58f095bdc2f5e33ba17424e2befd94d5913a47 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 23 Feb 2017 12:04:05 +0100 Subject: [PATCH 03/36] Create fl0w user by default --- Wallaby/Wallaby.py | 69 +++++++++++++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 24b5e99..0b5e85a 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,29 +10,28 @@ import sys import subprocess from random import randint from _thread import start_new_thread +from ctypes import cdll import threading +import json 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 + "/" +USERS_LOCATION = "/home/root/Documents/KISS/users.json" 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) if not IS_WALLABY: Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.") +def get_users(): + if IS_WALLABY: + return list(json.loads(open(USERS_LOCATION, "r").read()).keys()) + return ["Default User"] + + class SensorReadout: ANALOG = 1 DIGITAL = 2 @@ -40,7 +39,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() @@ -159,6 +158,7 @@ class Identify(Pipe): Logging.success("I was identified!") + class ListPrograms(Pipe): def run(self, data, peer, handler): programs = [] @@ -178,7 +178,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 +196,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: @@ -313,21 +313,40 @@ class Handler(Client): Logging.info("Unsubscribing '%s' from all sensor updates." % peer) self.routes["sensor"].sensor_readout.unsubscribe_all(peer) +if IS_WALLABY: + if not "fl0w" in get_users(): + json.loads(open(USERS_LOCATION, "r").read())["fl0w"] = {"mode" : "Advanced"} + try: + os.mkdir(FL0W_USER_PATH) + except FileExistsError: + pass + PATH = FL0W_USER_PATH + "bin/" +else: + if len(sys.argv) == 2: + if os.path.exists(sys.argv[1]): + PATH = os.path.abspath(sys.argv[1]) + else: + Logging.error("Location has to be provided in dev-env.") + exit(1) + +if PATH[-1] != "/": + PATH = PATH + "/" 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))) +config = Config() +config.add(Option("server_address", "ws://127.0.0.1:3077")) +config.add(Option("debug", True, validator=lambda x: True if True or False else False)) +config.add(Option("output_unbuffer", "stdbuf")) +config.add(Option("identify_sound", "Wallaby/identify.wav", + validator=lambda sound: os.path.isfile(sound))) + 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) try: @@ -335,7 +354,7 @@ try: # 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(), + "identify" : Identify(), "list_programs" : ListPrograms(), "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), "reboot" : Reboot()}, From 048042d1b15350cf0139dc59eae9103b4d1f5e35 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 1 Mar 2017 21:48:13 +0100 Subject: [PATCH 04/36] Fix appending of the 'Shared'-folder to the python search path. It assumed that 'Shared' was in the current working directory, which may not be always true. --- fl0w.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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')) From 5188457be5d7a69db15a6d3fc055881d0a990d66 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 1 Mar 2017 21:53:34 +0100 Subject: [PATCH 05/36] Add behem0th as submodule --- .gitmodules | 3 +++ Shared/behem0th | 1 + 2 files changed, 4 insertions(+) create mode 160000 Shared/behem0th diff --git a/.gitmodules b/.gitmodules index 7f3769d..cb0c267 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "Shared/ws4py"] path = Shared/ws4py url = https://github.com/robot0nfire/ws4py.git +[submodule "Shared/behem0th"] + path = Shared/behem0th + url = https://github.com/robot0nfire/behem0th.git diff --git a/Shared/behem0th b/Shared/behem0th new file mode 160000 index 0000000..df83861 --- /dev/null +++ b/Shared/behem0th @@ -0,0 +1 @@ +Subproject commit df8386161138c74be9f2b1a5dc959dec9ea2a86c From 7f067f40e9702ea5e1733b01aefef4d4efe9f850 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 1 Mar 2017 21:56:29 +0100 Subject: [PATCH 06/36] Initial integration of behem0th. --- Server/Server.py | 7 +++++++ Shared/Utils.py | 6 ++++++ Sublime/fl0w/fl0w.py | 12 ++++++++++-- Wallaby/Wallaby.py | 9 ++++++++- 4 files changed, 31 insertions(+), 3 deletions(-) diff --git a/Server/Server.py b/Server/Server.py index e058d29..6356f98 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -18,6 +18,8 @@ from ws4py.server.wsgiutils import WebSocketWSGIApplication from Highway import Server, Route, DummyPipe +import behem0th + class Info(Route): def run(self, data, handler): @@ -245,10 +247,15 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "reboot" : DummyPipe()}})) +sync_client = behem0th.Client(verbose_log=True) + + try: Logging.header("Server loop starting.") + sync_client.listen() server.serve_forever() except KeyboardInterrupt: Logging.header("Gracefully shutting down server.") server.server_close() + sync_client.close() Logging.success("Server shutdown successful.") diff --git a/Shared/Utils.py b/Shared/Utils.py index bae4483..e8b5c05 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): @@ -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: diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index fa2ea18..30c8c30 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -16,7 +16,9 @@ import sublime import sublime_plugin from Highway import Client, Route, Pipe, DummyPipe -from Utils import get_hostname +from Utils import get_hostname, get_ip_from_url + +import behem0th from SublimeMenu import * import Logging @@ -355,6 +357,8 @@ class Fl0w: self.main_menu += Entry("Disconnect", "Disconnect from server", action=self.invoke_disconnect) + self.sync_client = behem0th.Client(path=self.folder, verbose_log=True) + # Patch all sensor phantom that have been created before a fl0w instance # was attached to the window for sensor_phantom in sensor_phantoms: @@ -503,7 +507,9 @@ class Fl0w: set_status("Connection opened '%s'" % self.folder, self.window) self.connected = True self.settings.set("address", address) - except OSError as e: + + self.sync_client.connect(get_ip_from_url(address)) + except (OSError, ConnectionRefusedError) as e: sublime.error_message("Error during connection creation:\n %s" % str(e)) @@ -523,6 +529,8 @@ class Fl0w: set_status("Connection closed '%s'" % self.folder, self.window) self.connected = False + self.sync_client.close() + class Fl0wCommand(sublime_plugin.WindowCommand): def run(self): diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 24b5e99..7c62a36 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -3,6 +3,8 @@ import Logging import Config import Utils +import behem0th + import socket import time import os @@ -331,6 +333,8 @@ except FileNotFoundError: try: + sync_client = behem0th.Client(path=self.folder, verbose_log=True) + ws = Handler(config.server_address) # setup has to be called before the connection is established ws.setup({"subscribe" : Subscribe(), "hostname" : Hostname(), @@ -341,6 +345,9 @@ try: "reboot" : Reboot()}, debug=config.debug) ws.connect() + sync_client.connect(Utils.get_ip_from_url(config.server_address)) + ws.run_forever() except KeyboardInterrupt: - ws.close() \ No newline at end of file + ws.close() + sync_client.close() From 25f900e33ca6f0661ef62e38fe58d55cdcceeebc Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Thu, 2 Mar 2017 10:01:41 +0100 Subject: [PATCH 07/36] Sync correct path on Wallabys --- Wallaby/Wallaby.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 7c62a36..eb36ed0 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -333,7 +333,7 @@ except FileNotFoundError: try: - sync_client = behem0th.Client(path=self.folder, verbose_log=True) + sync_client = behem0th.Client(path=PATH, verbose_log=True) ws = Handler(config.server_address) # setup has to be called before the connection is established From 5467112a865bccd773189cadd163fec95c416d60 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Thu, 2 Mar 2017 17:20:39 +0100 Subject: [PATCH 08/36] Update behem0th --- Shared/behem0th | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/behem0th b/Shared/behem0th index df83861..bc02ded 160000 --- a/Shared/behem0th +++ b/Shared/behem0th @@ -1 +1 @@ -Subproject commit df8386161138c74be9f2b1a5dc959dec9ea2a86c +Subproject commit bc02ded60f1cda8ed8373763e78ffd50c4e4ed71 From 2b97f0313ffb89d68ae8b00cd01cb5b157ad2065 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 20:11:25 +0100 Subject: [PATCH 09/36] Added bottle.py to serve dashb0ard --- .gitmodules | 3 +++ Shared/bottle | 1 + 2 files changed, 4 insertions(+) create mode 160000 Shared/bottle diff --git a/.gitmodules b/.gitmodules index cb0c267..599ef48 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "Shared/behem0th"] path = Shared/behem0th url = https://github.com/robot0nfire/behem0th.git +[submodule "Shared/bottle"] + path = Shared/bottle + url = https://github.com/bottlepy/bottle.git 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 From 7f74a6b56fc6c260587026addd74c88b82617fb2 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 20:12:05 +0100 Subject: [PATCH 10/36] Replaced Config with Meh (better in every regard, basically) Meh is a good config "engine". Config isn't. --- Shared/Config.py | 118 ----------------- Shared/Meh.py | 328 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+), 118 deletions(-) delete mode 100644 Shared/Config.py create mode 100644 Shared/Meh.py 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/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 From f8112743b7788b6deb0f8a4ba4f95f1c5dec0367 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 20:12:56 +0100 Subject: [PATCH 11/36] Removed Broadcast because it wasn't used anywhere Piping got rid of broadcasts. That's good. --- Server/Broadcast.py | 47 --------------------------------------------- 1 file changed, 47 deletions(-) delete mode 100644 Server/Broadcast.py 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 From dfdbf0634d58a2ebaa2ed114f6e7c1da28c0945d Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 20:13:48 +0100 Subject: [PATCH 12/36] Removed unused imports, moved to Meh, removed Broadcast, ... Fixed behem0th path, disabled compile for now --- Server/Server.py | 46 ++++++++++++++++------------------------------ 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/Server/Server.py b/Server/Server.py index 6356f98..c523bda 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -1,23 +1,18 @@ +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 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 - import behem0th @@ -78,13 +73,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) @@ -171,9 +165,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" @@ -184,8 +177,6 @@ class Handler(Server): 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) @@ -202,25 +193,20 @@ 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("server_address", ("127.0.0.1", 3077))) +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], @@ -229,7 +215,7 @@ server = make_server(config.server_address[0], config.server_address[1], 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(), @@ -247,7 +233,7 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "reboot" : DummyPipe()}})) -sync_client = behem0th.Client(verbose_log=True) +sync_client = behem0th.Client(path=config.path, verbose_log=config.debug) try: From 7c7412f88cf5e6c9bf687a7ced73a6896199aa69 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 20:15:04 +0100 Subject: [PATCH 13/36] Fixed sublime settings --- Sublime/fl0w/fl0w.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index 30c8c30..f071d62 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -334,8 +334,8 @@ class Fl0w: action=lambda: self.set_debug(False)) - self.settings = Menu() - self.settings += Entry("Debug", "Toggle debug mode", + self.settings_menu = Menu() + self.settings_menu += Entry("Debug", "Toggle debug mode", sub_menu=self.debug_menu) @@ -353,7 +353,7 @@ class Fl0w: 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) + sub_menu=self.settings_menu) self.main_menu += Entry("Disconnect", "Disconnect from server", action=self.invoke_disconnect) @@ -507,8 +507,8 @@ class Fl0w: set_status("Connection opened '%s'" % self.folder, self.window) self.connected = True self.settings.set("address", address) - self.sync_client.connect(get_ip_from_url(address)) + except (OSError, ConnectionRefusedError) as e: sublime.error_message("Error during connection creation:\n %s" % str(e)) From 65d2715ea588091cfb8544bbb6db24eb80314a1a Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 22:23:53 +0100 Subject: [PATCH 14/36] Temporarily added dashb1ard as a submodule --- .gitmodules | 3 +++ Shared/dashb1ard | 1 + 2 files changed, 4 insertions(+) create mode 160000 Shared/dashb1ard diff --git a/.gitmodules b/.gitmodules index 599ef48..d5320c0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "Shared/bottle"] path = Shared/bottle url = https://github.com/bottlepy/bottle.git +[submodule "Shared/dashb1ard"] + path = Shared/dashb1ard + url = https://github.com/robot0nfire/dashb1ard.git diff --git a/Shared/dashb1ard b/Shared/dashb1ard new file mode 160000 index 0000000..90326b9 --- /dev/null +++ b/Shared/dashb1ard @@ -0,0 +1 @@ +Subproject commit 90326b9bd0466aac8540d6da2925d247338bdf73 From d147a81499cba9163eb7ef47c597f035c19e4895 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 22:24:39 +0100 Subject: [PATCH 15/36] Serve dashb0ard and static content on startup --- Server/Server.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/Server/Server.py b/Server/Server.py index c523bda..34944e0 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -8,14 +8,24 @@ import re import pwd import platform 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 import behem0th +from bottle.bottle import route, run, static_file +@route("/") +def index(): + return static_file("index.html", root="Shared/dashb1ard") + +@route("/static/") +def static(filepath): + return static_file(filepath, root="Shared/dashb1ard/static") + class Info(Route): def run(self, data, handler): handler.send({"routes" : list(handler.routes.keys())}, "info") @@ -195,7 +205,9 @@ CONFIG_PATH = "server.cfg" config = Config() -config.add(Option("server_address", ("127.0.0.1", 3077))) +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)) @@ -209,7 +221,7 @@ except (IOError, ExceptionInConfigError): #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() @@ -239,6 +251,12 @@ sync_client = behem0th.Client(path=config.path, verbose_log=config.debug) try: Logging.header("Server loop starting.") sync_client.listen() + 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.") From 868505126ec42ccd795ebda13c977292824faeaa Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 2 Mar 2017 22:34:22 +0100 Subject: [PATCH 16/36] Updated fl0w image url --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From a8ebee04c173bc676a989433f81822b02da557a5 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Fri, 3 Mar 2017 20:25:29 +0100 Subject: [PATCH 17/36] Move to Meh, create Harrogate user on first start, initial binary notifications --- Wallaby/Wallaby.py | 104 ++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 34 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index eb36ed0..1f64a6f 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -1,10 +1,8 @@ from Highway import Route, Pipe, Client +from Meh import Config, Option, ExceptionInConfigError import Logging -import Config import Utils -import behem0th - import socket import time import os @@ -12,29 +10,30 @@ import sys import subprocess from random import randint from _thread import start_new_thread +from ctypes import cdll import threading +import json + +from behem0th import Client as SyncClient +from behem0th import EventHandler 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 + "/" - +USERS_LOCATION = "/home/root/Documents/KISS/users.json" 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) if not IS_WALLABY: Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.") +def get_users(): + if IS_WALLABY: + return list(json.loads(open(USERS_LOCATION, "r").read()).keys()) + return ["Default User"] + + class SensorReadout: ANALOG = 1 DIGITAL = 2 @@ -42,7 +41,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() @@ -161,6 +160,7 @@ class Identify(Pipe): Logging.success("I was identified!") + class ListPrograms(Pipe): def run(self, data, peer, handler): programs = [] @@ -180,7 +180,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) @@ -198,7 +198,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: @@ -254,6 +254,20 @@ class Sensor(Pipe): self.sensor_readout = SensorReadout(handler) +class BinaryReceived(Pipe, EventHandler): + def on_modified(self, event): + print(event) + + + def on_created(self, event): + print(event) + + + def start(self, handler): + self.handler = handler + + + class Shutdown(Pipe): def run(self, data, peer, handler): try: @@ -310,44 +324,66 @@ 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) +if IS_WALLABY: + if not "fl0w" in get_users(): + json.loads(open(USERS_LOCATION, "r").read())["fl0w"] = {"mode" : "Advanced"} + try: + os.mkdir(FL0W_USER_PATH) + except FileExistsError: + pass + PATH = FL0W_USER_PATH + "bin/" +else: + if len(sys.argv) == 2: + if os.path.exists(sys.argv[1]): + PATH = os.path.abspath(sys.argv[1]) + else: + Logging.error("Location has to be provided in dev-env.") + exit(1) + +if PATH[-1] != "/": + PATH = PATH + "/" + + + 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) +config = Config() +config.add(Option("server_address", "ws://127.0.0.1:3077")) +config.add(Option("debug", True, validator=lambda x: True if True or False else False)) +config.add(Option("output_unbuffer", "stdbuf")) +config.add(Option("identify_sound", "Wallaby/identify.wav", + validator=lambda sound: os.path.isfile(sound))) try: - sync_client = behem0th.Client(path=PATH, verbose_log=True) + config = config.load(CONFIG_PATH) +except (IOError, ExceptionInConfigError): + config.dump(CONFIG_PATH) + config = config.load(CONFIG_PATH) + +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(), + "identify" : Identify(), "list_programs" : ListPrograms(), "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), - "reboot" : Reboot()}, + "reboot" : Reboot(), "binary_received" : BinaryReceived()}, debug=config.debug) ws.connect() + sync_client = SyncClient(path=PATH, verbose_log=config.debug, + event_handler=ws.routes["binary_received"]) sync_client.connect(Utils.get_ip_from_url(config.server_address)) - ws.run_forever() except KeyboardInterrupt: ws.close() - sync_client.close() + #sync_client.close() \ No newline at end of file From ae81d646f62ed0431c33e9ce9ff1edfca649ea6e Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Wed, 15 Mar 2017 20:49:11 +0100 Subject: [PATCH 18/36] Added output dummy pipe --- Server/Server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Server/Server.py b/Server/Server.py index 34944e0..d0766d6 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -242,7 +242,8 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "std_stream" : DummyPipe(), "stop_programs" : DummyPipe(), "shutdown" : DummyPipe(), - "reboot" : DummyPipe()}})) + "reboot" : DummyPipe(), + "output" : DummyPipe()}})) sync_client = behem0th.Client(path=config.path, verbose_log=config.debug) From 3061d5569be5977aceb6fb0ca883e4847a93ed0f Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Wed, 15 Mar 2017 20:49:59 +0100 Subject: [PATCH 19/36] Removed unnecessary import --- Shared/Logging.py | 1 - 1 file changed, 1 deletion(-) 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 From 1a918b5843b49d2acacfda87ad46ceecf4f1ef8d Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Wed, 15 Mar 2017 20:50:16 +0100 Subject: [PATCH 20/36] Added selective debug suppression "pipe" can get quite noisy (main reason was recursion because of output) --- Shared/Highway.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Shared/Highway.py b/Shared/Highway.py index ffc8233..c64b25f 100644 --- a/Shared/Highway.py +++ b/Shared/Highway.py @@ -285,7 +285,7 @@ def prepare_data(data): class Shared: - def setup(self, routes, debug=False): + def setup(self, routes, debug_suppress_routes=None, debug=False): self.routes = routes self.routes[META_ROUTE] = Meta() self.routes = create_routes(self.routes) @@ -297,6 +297,10 @@ class Shared: # would fail during route lookup). self.peer_exchange_routes = {META_ROUTE_INDEX : META_ROUTE} self.peer_reverse_exchange_routes = reverse_dict(self.peer_exchange_routes) + if debug_suppress_routes != None: + self.debug_suppress_routes = debug_suppress_routes + else: + self.debug_suppress_routes = [] self.debug = debug @@ -326,14 +330,14 @@ class Shared: m_route, self.address, self.port)) return data = convert_data(message[METADATA_LENGTH:], data_type) - if self.debug: + if self.debug and route not in self.debug_suppress_routes: 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: @@ -357,7 +361,7 @@ class Shared: except KeyError: Logging.error("'%s' is not a valid peer route." % route) else: - if self.debug: + if self.debug and route not in self.debug_suppress_routes: data_repr = str(data).replace("\n", " ") if len(data_repr) > 80: data_repr = data_repr[:80] + "..." @@ -368,9 +372,9 @@ class Shared: class Server(WebSocket, Shared): - def setup(self, routes, websockets, debug=False): + def setup(self, routes, websockets, debug_suppress_routes=None, debug=False): routes[PIPE_ROUTE] = ServerPipe() - super().setup(routes, debug=debug) + super().setup(routes, debug_suppress_routes=debug_suppress_routes, debug=debug) self.websockets = websockets self._last_websockets = self.websockets.copy() self._peers = {} @@ -399,9 +403,9 @@ class Server(WebSocket, Shared): class Client(WebSocketClient, Shared): - def setup(self, routes, debug=False): + def setup(self, routes, debug_suppress_routes=None, debug=False): routes[PIPE_ROUTE] = DummyPipe() - super().setup(routes, debug=debug) + super().setup(routes, debug_suppress_routes=debug_suppress_routes, debug=debug) self.override_methods() From 526695e209b018b13c72b054e1572520f6b5cebe Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Wed, 15 Mar 2017 20:52:07 +0100 Subject: [PATCH 21/36] Added output streaming stdout as well as stderr can now be subscribed to. ANSI control char filtering is still wip --- Wallaby/Wallaby.py | 60 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 1f64a6f..4961bc2 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -13,6 +13,7 @@ from _thread import start_new_thread from ctypes import cdll import threading import json +import re from behem0th import Client as SyncClient from behem0th import EventHandler @@ -320,15 +321,66 @@ 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) + super().setup(routes, debug_suppress_routes=["pipe"], 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) + self.routes["output"].unsubscribe(peer) if IS_WALLABY: @@ -343,6 +395,9 @@ else: if len(sys.argv) == 2: if os.path.exists(sys.argv[1]): PATH = os.path.abspath(sys.argv[1]) + else: + Logging.error("Location does not exist.") + exit(1) else: Logging.error("Location has to be provided in dev-env.") exit(1) @@ -377,7 +432,8 @@ try: "identify" : Identify(), "list_programs" : ListPrograms(), "whoami" : WhoAmI(), "run_program" : RunProgram(config.output_unbuffer), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), - "reboot" : Reboot(), "binary_received" : BinaryReceived()}, + "reboot" : Reboot(), "binary_received" : BinaryReceived(), + "output" : Output()}, debug=config.debug) ws.connect() sync_client = SyncClient(path=PATH, verbose_log=config.debug, From e3f6d56d02e66a82acdfe147abd0c96a67e0a971 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 22 Mar 2017 19:38:49 +0100 Subject: [PATCH 22/36] Remove superfluous whitespaces at the end of lines --- Server/Server.py | 8 +-- Shared/Utils.py | 8 +-- Sublime/fl0w/fl0w.py | 137 +++++++++++++++++++++---------------------- 3 files changed, 76 insertions(+), 77 deletions(-) diff --git a/Server/Server.py b/Server/Server.py index d0766d6..1c97565 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -96,8 +96,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]) @@ -125,7 +125,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): @@ -184,7 +184,7 @@ 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.debug: diff --git a/Shared/Utils.py b/Shared/Utils.py index e8b5c05..0972417 100644 --- a/Shared/Utils.py +++ b/Shared/Utils.py @@ -15,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(): @@ -40,7 +40,7 @@ def is_darwin(): def is_windows(): - return platform.uname().system == "Windows" + return platform.uname().system == "Windows" def set_hostname(hostname): @@ -95,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/Sublime/fl0w/fl0w.py b/Sublime/fl0w/fl0w.py index f071d62..1250eef 100644 --- a/Sublime/fl0w/fl0w.py +++ b/Sublime/fl0w/fl0w.py @@ -51,7 +51,7 @@ views = [] sensor_phantoms = [] def set_status(status, window): - window.active_view().set_status(FL0W_STATUS, + window.active_view().set_status(FL0W_STATUS, "fl0w: %s" % status) class Target: @@ -120,38 +120,38 @@ class Fl0wClient(Client): 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_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", + 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"], + action=partial(lambda handler, id_: Input("New Hostname:", + initial_text=data[id_]["name"], on_done=lambda hostname: handler.pipe( - {"set" : hostname}, + {"set" : hostname}, "hostname", id_)).invoke(handler.fl0w.window), handler, id_)) - utilities_menu += Entry("Processes", - "Lists processes currently running on controller", + 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.", + 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", + action_menu += Entry("Utilities", "Stuff you might need but probably won't", sub_menu=utilities_menu) - power_menu += Entry("Shutdown", - "Shutdown the controller", + power_menu += Entry("Shutdown", + "Shutdown the controller", action=partial(handler.pipe, None, "shutdown", id_)) - power_menu += Entry("Reboot", - "Reboot the controller", + 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 += 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, @@ -182,11 +182,11 @@ class Fl0wClient(Client): def run(self, data, peer, handler): program_menu = Menu(subtitles=False) for program in data: - program_menu += Entry(program, + program_menu += Entry(program, action=partial(self.run_program, handler, handler.routes["peers"].selected_action_menu.id_, program)) - program_menu.invoke(handler.fl0w.window, + program_menu.invoke(handler.fl0w.window, back=handler.routes["peers"].selected_action_menu) def run_program(self, handler, id_, program): @@ -206,7 +206,7 @@ class Fl0wClient(Client): def run(self, data, peer, handler): if data == self.__class__.NO_PROGRAMS_RUNNING: - sublime.error_message("No programs running.") + sublime.error_message("No programs running.") class StdStream(Pipe): @@ -218,7 +218,7 @@ class Fl0wClient(Client): self.handler = None - self.fetcher = self.Fetcher(self.buffer, self.write_to_panel, + self.fetcher = self.Fetcher(self.buffer, self.write_to_panel, self.lock) self.fetcher.start() @@ -237,7 +237,7 @@ class Fl0wClient(Client): self.handler = handler if type(data) is str: self.lock.acquire() - # try/except is faster than an explicit if as long as the + # try/except is faster than an explicit if as long as the # condition is not met try: self.buffer[peer].append(data) @@ -252,9 +252,9 @@ class Fl0wClient(Client): elif type(data) is dict: meta_text = "" if "exit_code" in data: - meta_text += "Program finished with exit code: %d\n" % data["exit_code"] + 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 + # try/except is faster than an explicit if as long as the # condition is not met # function call is also slower try: @@ -264,7 +264,7 @@ class Fl0wClient(Client): 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() + self.lock.release() @@ -275,8 +275,8 @@ class Fl0wClient(Client): self.handler.fl0w.window.run_command("show_panel", {"panel": "output.%s" % peer}) - # Sublime gets quite overwhelmed when faced with typical - # "while (1) { printf(...)}" output. + # 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): @@ -294,7 +294,7 @@ class Fl0wClient(Client): for peer in self.buffer: if len(self.buffer[peer]) > 0: self.write_to_panel( - "".join(self.buffer[peer]), + "".join(self.buffer[peer]), peer) self.buffer[peer] = [] self.lock.release() @@ -310,51 +310,51 @@ class Fl0w: 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"), + 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", + self.start_menu += Entry("About", "Information about fl0w", action=self.invoke_about) self.debug_menu = Menu(subtitles=False) - self.debug_menu += Entry("On", + self.debug_menu += Entry("On", action=lambda: self.set_debug(True)) - self.debug_menu += Entry("Off", + self.debug_menu += Entry("Off", action=lambda: self.set_debug(False)) self.settings_menu = Menu() - self.settings_menu += Entry("Debug", "Toggle debug mode", + self.settings_menu += Entry("Debug", "Toggle debug mode", sub_menu=self.debug_menu) self.meta = Menu() - self.meta += Entry("Info", "Server info", + self.meta += Entry("Info", "Server info", action=lambda: self.ws.send(None, "info")) - self.meta_entry = Entry("Meta", "Debug information about fl0w", + self.meta_entry = Entry("Meta", "Debug information about fl0w", sub_menu=self.meta) - if self.debug: + 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", + self.main_menu += Entry("Controllers", "All connected controllers", sub_menu=self.controller_menu) - self.main_menu += Entry("Settings", "General purpose settings", + self.main_menu += Entry("Settings", "General purpose settings", sub_menu=self.settings_menu) - self.main_menu += Entry("Disconnect", "Disconnect from server", + self.main_menu += Entry("Disconnect", "Disconnect from server", action=self.invoke_disconnect) self.sync_client = behem0th.Client(path=self.folder, verbose_log=True) @@ -410,7 +410,7 @@ class Fl0w: # 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 @@ -442,7 +442,7 @@ class Fl0w: """ print("Lock will be aquired.") self.subscriptions_lock.acquire() - print("Lock was aquired.") + print("Lock was aquired.") del self.subscriptions[sensor_phantom] print("Lock will be released.") self.subscriptions_lock.release() @@ -459,7 +459,7 @@ class Fl0w: for sensor_phantom in self.subscriptions: for sensor_type in ("analog", "digital"): combined_subscriptions[sensor_type] = list( - set(combined_subscriptions[sensor_type]) | + set(combined_subscriptions[sensor_type]) | set(self.subscriptions[sensor_phantom][sensor_type]) ) self.combined_subscriptions = combined_subscriptions @@ -492,15 +492,14 @@ class Fl0w: 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(), + "processes" : Fl0wClient.Processes(), "list_programs" : Fl0wClient.ListPrograms(), "sensor" : Fl0wClient.Sensor(), "std_stream" : Fl0wClient.StdStream(), "run_program" : Fl0wClient.RunProgram(), - "stop_programs" : Fl0wClient.StopPrograms()}, + "stop_programs" : Fl0wClient.StopPrograms()}, self, debug=True) self.ws.connect() sublime.set_timeout_async(self.ws.run_forever, 0) @@ -614,10 +613,10 @@ class SensorCommand(sublime_plugin.WindowCommand): view.sensor_phantom.enabled = not view.sensor_phantom.enabled view_file_name = view.file_name() if view_file_name == None: - view_file_name = "untitled" + view_file_name = "untitled" set_status("%s sensor phantoms for '%s'." % ( - "Enabled" if view.sensor_phantom.enabled else "Disabled", - view_file_name), + "Enabled" if view.sensor_phantom.enabled else "Disabled", + view_file_name), self.window) else: sublime.error_message("fl0w is not connected.") @@ -633,7 +632,7 @@ class SensorPhantom(sublime_plugin.ViewEventListener): 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 @@ -656,7 +655,7 @@ class SensorPhantom(sublime_plugin.ViewEventListener): def enabled(self): return self._enabled - + @enabled.setter def enabled(self, enabled_): if enabled_: @@ -704,7 +703,7 @@ class SensorPhantom(sublime_plugin.ViewEventListener): if port_candidates[0].isnumeric(): matches[method_name].append( ( - int(port_candidates[0]), + int(port_candidates[0]), sublime.Region(self.view.line(candidate.a).b) )) self.matches = matches @@ -715,12 +714,12 @@ class SensorPhantom(sublime_plugin.ViewEventListener): 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, + 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, + self.view.add_phantom(sensor_type, match[1], + ERROR_OPEN + "!" + ERROR_CLOSE, sublime.LAYOUT_INLINE) @@ -751,7 +750,7 @@ class SensorPhantom(sublime_plugin.ViewEventListener): self.enabled = True - def __del__(self): + def __del__(self): self.enabled = False if self in sensor_phantoms: del sensor_phantoms[sensor_phantoms.index(self)] From 4e611c33e925361df8034ed87155bfdd6a283746 Mon Sep 17 00:00:00 2001 From: Christoph Heiss Date: Wed, 22 Mar 2017 19:39:14 +0100 Subject: [PATCH 23/36] Update behem0th --- Shared/behem0th | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/behem0th b/Shared/behem0th index bc02ded..3931f2a 160000 --- a/Shared/behem0th +++ b/Shared/behem0th @@ -1 +1 @@ -Subproject commit bc02ded60f1cda8ed8373763e78ffd50c4e4ed71 +Subproject commit 3931f2a9a2f00b95d82ccb3c5e7c13b3fbb5f4d7 From 9340ae44cce1c78e85c94d6171736c53673ea893 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 11:04:23 +0200 Subject: [PATCH 24/36] Removed temporary dashb0ard version --- .gitmodules | 3 --- Shared/dashb1ard | 1 - 2 files changed, 4 deletions(-) delete mode 160000 Shared/dashb1ard diff --git a/.gitmodules b/.gitmodules index d5320c0..599ef48 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,6 +7,3 @@ [submodule "Shared/bottle"] path = Shared/bottle url = https://github.com/bottlepy/bottle.git -[submodule "Shared/dashb1ard"] - path = Shared/dashb1ard - url = https://github.com/robot0nfire/dashb1ard.git diff --git a/Shared/dashb1ard b/Shared/dashb1ard deleted file mode 160000 index 90326b9..0000000 --- a/Shared/dashb1ard +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 90326b9bd0466aac8540d6da2925d247338bdf73 From 75b3a24aeca3d5797e248d0a5dcf9d6cde85e557 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 11:05:09 +0200 Subject: [PATCH 25/36] Removed Sublime Text plugin --- Sublime/fl0w/CompileHighlight.sublime-syntax | 11 - Sublime/fl0w/Default (Linux).sublime-keymap | 14 - Sublime/fl0w/Default (OSX).sublime-keymap | 14 - Sublime/fl0w/Default (Windows).sublime-keymap | 14 - Sublime/fl0w/Shared | 1 - Sublime/fl0w/SublimeMenu.py | 140 ---- Sublime/fl0w/fl0w.py | 756 ------------------ Sublime/fl0w/fl0w.sublime-commands | 6 - Sublime/fl0w/fl0w.sublime-settings | 4 - 9 files changed, 960 deletions(-) delete mode 100644 Sublime/fl0w/CompileHighlight.sublime-syntax delete mode 100644 Sublime/fl0w/Default (Linux).sublime-keymap delete mode 100644 Sublime/fl0w/Default (OSX).sublime-keymap delete mode 100644 Sublime/fl0w/Default (Windows).sublime-keymap delete mode 120000 Sublime/fl0w/Shared delete mode 100644 Sublime/fl0w/SublimeMenu.py delete mode 100644 Sublime/fl0w/fl0w.py delete mode 100644 Sublime/fl0w/fl0w.sublime-commands delete mode 100644 Sublime/fl0w/fl0w.sublime-settings 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 1250eef..0000000 --- a/Sublime/fl0w/fl0w.py +++ /dev/null @@ -1,756 +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, get_ip_from_url - -import behem0th - -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 = Menu() - self.settings_menu += 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_menu) - self.main_menu += Entry("Disconnect", "Disconnect from server", - action=self.invoke_disconnect) - - self.sync_client = behem0th.Client(path=self.folder, verbose_log=True) - - # 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) - self.sync_client.connect(get_ip_from_url(address)) - - except (OSError, ConnectionRefusedError) 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 - - self.sync_client.close() - - -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 -} From 3468ea2cca825917a0e779785d3dd266b2c66c57 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 11:09:25 +0200 Subject: [PATCH 26/36] Removed behem0th --- .gitmodules | 3 --- Shared/behem0th | 1 - 2 files changed, 4 deletions(-) delete mode 160000 Shared/behem0th diff --git a/.gitmodules b/.gitmodules index 599ef48..450ad62 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,9 +1,6 @@ [submodule "Shared/ws4py"] path = Shared/ws4py url = https://github.com/robot0nfire/ws4py.git -[submodule "Shared/behem0th"] - path = Shared/behem0th - url = https://github.com/robot0nfire/behem0th.git [submodule "Shared/bottle"] path = Shared/bottle url = https://github.com/bottlepy/bottle.git diff --git a/Shared/behem0th b/Shared/behem0th deleted file mode 160000 index 3931f2a..0000000 --- a/Shared/behem0th +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3931f2a9a2f00b95d82ccb3c5e7c13b3fbb5f4d7 From fc8e117a93c6c1c44da36958e6a6c9db9bcfaf5c Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 11:09:51 +0200 Subject: [PATCH 27/36] Removed fl0w ignore file for Sublime Text --- .no-fl0w | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .no-fl0w diff --git a/.no-fl0w b/.no-fl0w deleted file mode 100644 index e69de29..0000000 From 1d0253cb81fdc361622a78dedc2a9a63095d57c3 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 16:49:51 +0200 Subject: [PATCH 28/36] Removed Disc0very (obsolete) --- Shared/Disc0very.py | 97 --------------------------------------------- 1 file changed, 97 deletions(-) delete mode 100644 Shared/Disc0very.py 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 From 1c872e1c6fa5861912735f3db5a79d45c9e61988 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 16:50:34 +0200 Subject: [PATCH 29/36] Replaced identification sound --- Wallaby/identify.wav | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 7e299779c1777d9fc80da30b45f611917f3639b7 Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 17:22:28 +0200 Subject: [PATCH 30/36] Reintroduced dashb0ard --- .gitmodules | 3 +++ Shared/dashb0ard | 1 + 2 files changed, 4 insertions(+) create mode 160000 Shared/dashb0ard diff --git a/.gitmodules b/.gitmodules index 450ad62..8739090 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [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/Shared/dashb0ard b/Shared/dashb0ard new file mode 160000 index 0000000..c99bd9d --- /dev/null +++ b/Shared/dashb0ard @@ -0,0 +1 @@ +Subproject commit c99bd9d574cece380c7de5bb10a776db762e8b78 From d5af3524bcd21f250b6a55e16ff53cbfa151c38c Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 17:22:48 +0200 Subject: [PATCH 31/36] Upped dashb0ard version --- Shared/dashb0ard | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/dashb0ard b/Shared/dashb0ard index c99bd9d..cca5173 160000 --- a/Shared/dashb0ard +++ b/Shared/dashb0ard @@ -1 +1 @@ -Subproject commit c99bd9d574cece380c7de5bb10a776db762e8b78 +Subproject commit cca517355923990423a2c744bf2bc81080090ff3 From 7fc4a0697b3f2fb00420adb612fdde915ce709ce Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 17:25:02 +0200 Subject: [PATCH 32/36] Removed selective debug suppression --- Shared/Highway.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/Shared/Highway.py b/Shared/Highway.py index c64b25f..90c679f 100644 --- a/Shared/Highway.py +++ b/Shared/Highway.py @@ -285,7 +285,7 @@ def prepare_data(data): class Shared: - def setup(self, routes, debug_suppress_routes=None, debug=False): + def setup(self, routes, debug=False): self.routes = routes self.routes[META_ROUTE] = Meta() self.routes = create_routes(self.routes) @@ -297,10 +297,6 @@ class Shared: # would fail during route lookup). self.peer_exchange_routes = {META_ROUTE_INDEX : META_ROUTE} self.peer_reverse_exchange_routes = reverse_dict(self.peer_exchange_routes) - if debug_suppress_routes != None: - self.debug_suppress_routes = debug_suppress_routes - else: - self.debug_suppress_routes = [] self.debug = debug @@ -330,7 +326,7 @@ class Shared: m_route, self.address, self.port)) return data = convert_data(message[METADATA_LENGTH:], data_type) - if self.debug and route not in self.debug_suppress_routes: + if self.debug: data_repr = str(data).replace("\n", " ") if len(data_repr) > 80: data_repr = data_repr[:80] + "..." @@ -361,7 +357,7 @@ class Shared: except KeyError: Logging.error("'%s' is not a valid peer route." % route) else: - if self.debug and route not in self.debug_suppress_routes: + if self.debug: data_repr = str(data).replace("\n", " ") if len(data_repr) > 80: data_repr = data_repr[:80] + "..." @@ -372,9 +368,9 @@ class Shared: class Server(WebSocket, Shared): - def setup(self, routes, websockets, debug_suppress_routes=None, debug=False): + def setup(self, routes, websockets, debug=False): routes[PIPE_ROUTE] = ServerPipe() - super().setup(routes, debug_suppress_routes=debug_suppress_routes, debug=debug) + super().setup(routes, debug=debug) self.websockets = websockets self._last_websockets = self.websockets.copy() self._peers = {} @@ -403,9 +399,9 @@ class Server(WebSocket, Shared): class Client(WebSocketClient, Shared): - def setup(self, routes, debug_suppress_routes=None, debug=False): + def setup(self, routes, debug=False): routes[PIPE_ROUTE] = DummyPipe() - super().setup(routes, debug_suppress_routes=debug_suppress_routes, debug=debug) + super().setup(routes, debug=debug) self.override_methods() From 90e15a8c141c0da087184033098dc21cd0e2b22d Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 17:26:05 +0200 Subject: [PATCH 33/36] Removed fl0w-proper bootstrapping code, unneeded routes --- Wallaby/Wallaby.py | 116 ++++++--------------------------------------- 1 file changed, 14 insertions(+), 102 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 4961bc2..50404bd 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -15,24 +15,19 @@ import threading import json import re -from behem0th import Client as SyncClient -from behem0th import EventHandler CHANNEL = 2 IS_WALLABY = Utils.is_wallaby() -USERS_LOCATION = "/home/root/Documents/KISS/users.json" LIB_WALLABY = "/usr/lib/libwallaby.so" +CONFIG_PATH = "wallaby.cfg" -if not IS_WALLABY: - Logging.warning("Binaries that were created for Wallaby Controllers will not run on a simulated Wallaby.") - - -def get_users(): - if IS_WALLABY: - return list(json.loads(open(USERS_LOCATION, "r").read()).keys()) - return ["Default User"] +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: @@ -157,22 +152,7 @@ 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) + Logging.success("I was identified!") class StopPrograms(Pipe): @@ -255,18 +235,6 @@ class Sensor(Pipe): self.sensor_readout = SensorReadout(handler) -class BinaryReceived(Pipe, EventHandler): - def on_modified(self, event): - print(event) - - - def on_created(self, event): - print(event) - - - def start(self, handler): - self.handler = handler - class Shutdown(Pipe): @@ -298,21 +266,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): @@ -354,7 +307,7 @@ class Output(Pipe): def unsubscribe(self, peer): if peer in self.subscribers: - del self.subscribers[self.subscribers.index(peer)] + del self.subscribers[self.subscribers.index(peer)] class Std: def __init__(self, old_std, write_callback): @@ -373,7 +326,7 @@ class Output(Pipe): class Handler(Client): def setup(self, routes, debug=False): - super().setup(routes, debug_suppress_routes=["pipe"], debug=debug) + super().setup(routes, debug=debug) def peer_unavaliable(self, peer): @@ -383,40 +336,6 @@ class Handler(Client): self.routes["output"].unsubscribe(peer) -if IS_WALLABY: - if not "fl0w" in get_users(): - json.loads(open(USERS_LOCATION, "r").read())["fl0w"] = {"mode" : "Advanced"} - try: - os.mkdir(FL0W_USER_PATH) - except FileExistsError: - pass - PATH = FL0W_USER_PATH + "bin/" -else: - if len(sys.argv) == 2: - if os.path.exists(sys.argv[1]): - PATH = os.path.abspath(sys.argv[1]) - else: - Logging.error("Location does not exist.") - exit(1) - else: - Logging.error("Location has to be provided in dev-env.") - exit(1) - -if PATH[-1] != "/": - PATH = PATH + "/" - - - -CONFIG_PATH = "wallaby.cfg" - -config = Config() -config.add(Option("server_address", "ws://127.0.0.1:3077")) -config.add(Option("debug", True, validator=lambda x: True if True or False else False)) -config.add(Option("output_unbuffer", "stdbuf")) -config.add(Option("identify_sound", "Wallaby/identify.wav", - validator=lambda sound: os.path.isfile(sound))) - - try: config = config.load(CONFIG_PATH) except (IOError, ExceptionInConfigError): @@ -427,19 +346,12 @@ except (IOError, ExceptionInConfigError): 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), + ws.setup({"subscribe" : Subscribe(), "sensor" : Sensor(), + "identify" : Identify(), "whoami" : WhoAmI(), "stop_programs" : StopPrograms(), "shutdown" : Shutdown(), - "reboot" : Reboot(), "binary_received" : BinaryReceived(), - "output" : Output()}, - debug=config.debug) + "reboot" : Reboot(), "output" : Output()}, + debug=False) ws.connect() - sync_client = SyncClient(path=PATH, verbose_log=config.debug, - event_handler=ws.routes["binary_received"]) - sync_client.connect(Utils.get_ip_from_url(config.server_address)) ws.run_forever() except KeyboardInterrupt: - ws.close() - #sync_client.close() \ No newline at end of file + ws.close() \ No newline at end of file From ae93513fb88fa134576cee5c0c36e3de781a7cee Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Thu, 20 Apr 2017 17:27:20 +0200 Subject: [PATCH 34/36] Removed Compile route and behem0th, updated dashb0ard path --- Server/Server.py | 58 +++++------------------------------------------- 1 file changed, 5 insertions(+), 53 deletions(-) diff --git a/Server/Server.py b/Server/Server.py index 1c97565..5e36eb4 100644 --- a/Server/Server.py +++ b/Server/Server.py @@ -14,63 +14,22 @@ from wsgiref.simple_server import make_server from ws4py.server.wsgirefserver import WSGIServer, WebSocketWSGIRequestHandler from ws4py.server.wsgiutils import WebSocketWSGIApplication -import behem0th from bottle.bottle import route, run, static_file @route("/") def index(): - return static_file("index.html", root="Shared/dashb1ard") + return static_file("index.html", root="Shared/dashb0ard") @route("/static/") def static(filepath): - return static_file(filepath, root="Shared/dashb1ard/static") + 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 @@ -237,8 +196,6 @@ 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(), @@ -246,21 +203,16 @@ server.set_app(WebSocketWSGIApplication(handler_cls=Handler, "output" : DummyPipe()}})) -sync_client = behem0th.Client(path=config.path, verbose_log=config.debug) - - try: Logging.header("Server loop starting.") - sync_client.listen() - start_new_thread(run, (), {"host" : config.dashb0ard_address[0], + 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], + 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], + 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.") server.server_close() - sync_client.close() Logging.success("Server shutdown successful.") From fc95b26c45924cf9e55af2739dbc90d5b90925ce Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Sat, 22 Apr 2017 19:56:04 +0200 Subject: [PATCH 35/36] Handle playback failure --- Wallaby/Wallaby.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index 50404bd..ca28e9c 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -151,7 +151,10 @@ class SensorReadout: class Identify(Pipe): def run(self, data, peer, handler): - Utils.play_sound(config.identify_sound) + try: + Utils.play_sound(config.identify_sound) + except Utils.PlaybackFailure: + Logging.error("Could not play identification sound") Logging.success("I was identified!") @@ -354,4 +357,4 @@ try: ws.connect() ws.run_forever() except KeyboardInterrupt: - ws.close() \ No newline at end of file + ws.close() From 11a4c760d76521f3ab1c4df3a730d82e4ea976ff Mon Sep 17 00:00:00 2001 From: Philip Trauner Date: Mon, 24 Apr 2017 14:25:17 +0300 Subject: [PATCH 36/36] Wait for server to become avaliable --- Wallaby/Wallaby.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/Wallaby/Wallaby.py b/Wallaby/Wallaby.py index ca28e9c..7c85393 100644 --- a/Wallaby/Wallaby.py +++ b/Wallaby/Wallaby.py @@ -347,14 +347,20 @@ except (IOError, ExceptionInConfigError): try: - ws = Handler(config.server_address) - # setup has to be called before the connection is established - ws.setup({"subscribe" : Subscribe(), "sensor" : Sensor(), + 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(), "output" : Output()}, debug=False) - ws.connect() + try: + ws.connect() + break + except ConnectionRefusedError: + Logging.warning("Server not running... Retrying in 5s") + time.sleep(5) ws.run_forever() except KeyboardInterrupt: ws.close()