diff --git a/APSyncFramework/APSync.py b/APSyncFramework/APSync.py index 5aca64a..2253b79 100644 --- a/APSyncFramework/APSync.py +++ b/APSyncFramework/APSync.py @@ -1,9 +1,12 @@ import time, select, sys, signal, os, shlex import multiprocessing, threading, setproctitle -import traceback +import traceback, logging +from logging.handlers import RotatingFileHandler + from APSyncFramework.modules.lib import APSync_module from APSyncFramework.utils.common_utils import PeriodicEvent, pid_exists, wait_pid from APSyncFramework.utils.json_utils import json_unwrap_with_target +from APSyncFramework.utils.file_utils import mkdir_p # global for signals only. apsync_state = [] @@ -12,7 +15,24 @@ class APSync(object): def __init__(self): self.modules = [] self.should_exit = False - + self.begin_logging() + + def begin_logging(self): + logFormatter = logging.Formatter("%(asctime)s [%(levelname)-5.5s] %(message)s") + rootLogger = logging.getLogger() + rootLogger.setLevel(1) + log_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'logs') + mkdir_p(log_dir) + fileHandler = RotatingFileHandler("{0}/{1}.log".format(log_dir, 'APSync'), maxBytes = 100000, backupCount = 5) + fileHandler.setFormatter(logFormatter) + fileHandler.setLevel(1) # used to control what is printed to console + rootLogger.addHandler(fileHandler) + + consoleHandler = logging.StreamHandler() + consoleHandler.setFormatter(logFormatter) + consoleHandler.setLevel(1) # used to control what is printed to console + rootLogger.addHandler(consoleHandler) + @property def loaded_modules(self): return [module_instance.name for (module_instance,module) in self.modules] @@ -150,17 +170,19 @@ def main_loop(self): pid = os.getpid() signal.signal(signal.SIGINT, self.signal_handler) signal.signal(signal.SIGTERM, self.signal_handler) - self.load_module('mavlink', start_now=True) +# self.load_module('mavlink', start_now=True) self.load_module('webserver', start_now=True) + self.load_module('dfsync', start_now=True) # TODO: make the input thread optional (this can be replaced by the web UI) self.input_loop_queue = multiprocessing.Queue() + self.inject_data_queue = multiprocessing.Queue() input_loop_thread = threading.Thread(target=self.input_loop, args = (lock, apsync_state, self.input_loop_queue)) input_loop_thread.daemon = True input_loop_thread.start() - queue_handling_thread = threading.Thread(target=self.queue_handling, args = (lock, self.event, apsync_state,)) + queue_handling_thread = threading.Thread(target=self.queue_handling, args = (lock, self.event, apsync_state, self.inject_data_queue)) queue_handling_thread.daemon = True queue_handling_thread.start() @@ -174,6 +196,11 @@ def main_loop(self): cmd = args[0] if cmd == 'module': self.cmd_module(args[1:]) + if (cmd == 'help') or (cmd == '?'): + print "try one of these:\n\nmodule reload webserver\nmodule reload mavlink\nmodule list\nor paste some json if you are game\n\n" + if (line[0] == '{' and line[-1] == '}'): + # assume the line is json + self.inject_json(line) for event in periodic_events: event.trigger() @@ -194,7 +221,7 @@ def input_loop(self, lock, apsync_state, out_queue): line = line.strip() # remove leading and trailing whitespace out_queue.put_nowait(line) - def queue_handling(self, lock, event, apsync_state): + def queue_handling(self, lock, event, apsync_state, inject_data_queue): setproctitle.setproctitle("APSync") while not self.should_exit: @@ -202,6 +229,7 @@ def queue_handling(self, lock, event, apsync_state): modules = self.modules module_names = self.loaded_modules out_queues = [i.out_queue for (i,m) in modules] + out_queues.append(inject_data_queue) # used to pass targeted data from the cmd line to modules in_queues = [i.in_queue for (i,m) in modules] queue_file_discriptors = [q._reader for q in out_queues] event.clear() @@ -222,6 +250,19 @@ def queue_handling(self, lock, event, apsync_state): where = (idx for idx,(i,m) in enumerate(self.modules) if i.name==data['name']).next() (i,m) = self.modules[where] i.last_ping = data + elif target == 'logging': + if data['level'].upper() == 'CRITICAL': + logging.critical(data['msg']) + elif data['level'].upper() == 'ERROR': + logging.error(data['msg']) + elif data['level'].upper() == 'WARNING': + logging.warning(data['msg']) + elif data['level'].upper() == 'INFO': + logging.info(data['msg']) + elif data['level'].upper() == 'DEBUG': + logging.debug(data['msg']) + else: + pass else: idx = module_names.index(target) in_queues[idx].put(data) @@ -245,7 +286,18 @@ def shlex_quotes(self, value): lex.whitespace_split = True lex.commenters = '' return list(lex) + + def inject_json(self, json_data): + '''pass json data to the queue handler thread + e.g.: { "_target":"webserver", "data":{"json_data":{"command":"sendIdentityRequest","replyto":"getIdentityResponse"}} }''' + try: + (target,data,priority) = json_unwrap_with_target(json_data) + print('\nTARGET:\t\t{0}\nPRIORITY:\t{1}\nDATA:\n{2}'.format(target,priority,data)) + self.inject_data_queue.put_nowait(json_data) + except Exception as e: + print('Something went wrong while trying to unwrap your json:\n{0}'.format(json_data)) + def cmd_module(self, args): '''module commands''' usage = "usage: module " diff --git a/APSyncFramework/conf/.gitignore b/APSyncFramework/conf/.gitignore new file mode 100644 index 0000000..f14b1da --- /dev/null +++ b/APSyncFramework/conf/.gitignore @@ -0,0 +1 @@ +/WebConfigServer.json diff --git a/APSyncFramework/conf/WebConfigServer.json b/APSyncFramework/conf/WebConfigServer.json index 4689521..d6bd1c9 100644 --- a/APSyncFramework/conf/WebConfigServer.json +++ b/APSyncFramework/conf/WebConfigServer.json @@ -14,5 +14,9 @@ "connection": "tcp:127.0.0.1:5763", "dialect": "ardupilotmega", - "webserver_port": "4443" + "webserver_port": "4443", + + "cloudsync_user": "apsync", + "cloudsync_port": "2221", + "cloudsync_address": "www.mavcesium.io" } diff --git a/APSyncFramework/logs/.gitignore b/APSyncFramework/logs/.gitignore new file mode 100644 index 0000000..fe0a8fc --- /dev/null +++ b/APSyncFramework/logs/.gitignore @@ -0,0 +1 @@ +/APSync.log diff --git a/APSyncFramework/modules/APSync_dfsync/__init__.py b/APSyncFramework/modules/APSync_dfsync/__init__.py new file mode 100644 index 0000000..305153f --- /dev/null +++ b/APSyncFramework/modules/APSync_dfsync/__init__.py @@ -0,0 +1,290 @@ +from APSyncFramework.modules.lib import APSync_module +from APSyncFramework.utils.common_utils import pid_exists, wait_pid +from APSyncFramework.utils.json_utils import json_wrap_with_target +from APSyncFramework.utils.file_utils import mkdir_p, write_config, read_config, file_get_contents +from APSyncFramework.utils.requests_utils import create_session, register, upload_request, verify +from APSyncFramework.utils.network_utils import generate_key_fingerprint + +import os, time, subprocess, uuid, shutil, signal, re, base64 +from datetime import datetime +import requests + +class DFSyncModule(APSync_module.APModule): + def __init__(self, in_queue, out_queue): + super(DFSyncModule, self).__init__(in_queue, out_queue, "dfsync") + self.load_config() + self.get_ssh_creds() # need ssh keys in place + self.have_path_to_cloud = False # assume no internet facing network on module load + self.is_not_armed = None # arm state is unknown on module load + self.syncing_enabled = True + self.cloudsync_session = False + self.last_verify_message = 0 + self.verify_message_interval = 120 + + + self.cloudsync_remote_dir = '~' + self.datalog_dir = os.path.join(os.path.expanduser('~'), 'dflogger') + self.datalog_archive_dir = os.path.join(os.path.expanduser('~'),'dflogger', 'dataflash-archive') + # create us a ~/dflogger/ folder and ~/dflogger/dataflash-archive/ if it's not already there. + mkdir_p(self.datalog_dir) + mkdir_p(self.datalog_archive_dir) + self.old_time = 3 # seconds a file must remain unchanged before being considered okay to sync + + self.datalogs = {} + self.rsync_pid = None + self.rsync_time = re.compile(r'[0-9]:([0-5][0-9]):([0-5][0-9])') + + ### TODO update these values from other modules via process_in_queue_data() + self.have_path_to_cloud = True + self.is_not_armed = True + self.syncing_enabled = True + ### + + self.client = requests.Session() + + ### + self.cloudsync_url_base = "https://"+self.cloudsync_address+"/" + self.cloudsync_url_register = self.cloudsync_url_base+'register' + self.cloudsync_url_verify = self.cloudsync_url_base+'verify' + self.cloudsync_url_upload = self.cloudsync_url_base+'upload' + + def load_config(self): + # TODO: move some of this into the module lib... + self.cloudsync_port = self.set_config('cloudsync_port', 22) + self.cloudsync_user = self.set_config('cloudsync_user', 'apsync') + self.cloudsync_address = self.set_config('cloudsync_address', 'apsync.cloud') + self.cloudsync_account_registered = self.set_config('cloudsync_account_registered', False) + self.cloudsync_account_verified = self.set_config('cloudsync_account_verified', False) + self.cloudsync_ssh_identity_file = self.set_config('cloudsync_ssh_identity_file', os.path.expanduser('~/.ssh/id_apsync')) + self.cloudsync_vehicle_id = self.set_config('cloudsync_vehicle_id', '') + self.cloudsync_user_id = self.set_config('cloudsync_user_id', '') + self.cloudsync_email = self.set_config('cloudsync_email', '') + if self.config_changed: + # TODO: send a msg to the webserver to update / reload the current page + self.log("At least one of your cloudsync settings was missing or has been updated, please reload the webpage if open.", 'INFO') + self.config_changed = False + write_config(self.config) + + def main(self): + if self.have_path_to_cloud: + self.cloudsync_session = create_session(self.cloudsync_url_base, self.client) + + if (self.cloudsync_session and self.cloudsync_account_registered and not self.cloudsync_account_verified): + payload = {'public_key_fingerprint': base64.b64encode(self.ssh_cred_fingerprint), '_xsrf':self.client.cookies['_xsrf'] } + verify_response = verify(self.cloudsync_url_verify, self.client, payload) + if verify_response: + if verify_response['verify']: + self.config['cloudsync_vehicle_id'] = verify_response['vehicle_id'] + self.config['cloudsync_user_id'] = verify_response['user_id'] + self.config['cloudsync_account_verified'] = True + self.load_config() + + j = {'message':verify_response['msg'], 'current_time':time.time(), 'replyto':'dfsyncSyncRegister'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : j}, target = 'webserver')) + self.log('Cloudsync account verified', 'INFO') + else: + self.config['cloudsync_account_verified'] = False + self.load_config() + if time.time() >= (self.last_verify_message + self.verify_message_interval): + j = {'message':verify_response['msg'], 'current_time':time.time(), 'replyto':'dfsyncSyncRegister'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : j}, target = 'webserver')) + self.log('Cloudsync credentials need to be verified! Please verify them by clicking on the link sent to your email address', 'INFO') + self.last_verify_message = time.time() + self.verify_message_interval + + + stat_file_info = self.stat_files_in_dir(self.datalog_dir) + for key in stat_file_info.keys(): + if key in self.datalogs: + if (stat_file_info[key]['size'] == self.datalogs[key]['size'] and stat_file_info[key]['modify'] == self.datalogs[key]['modify']): + stat_file_info[key]['age'] = time.time()-self.datalogs[key]['time'] + stat_file_info[key]['time'] = self.datalogs[key]['time'] + self.datalogs[key] = stat_file_info[key] + else: + stat_file_info[key]['age'] = time.time()-stat_file_info[key]['time'] + self.datalogs[key] = stat_file_info[key] + + self.files_to_sync = {} + for key in self.datalogs.keys(): +# print self.datalogs + if self.datalogs[key]['age'] > self.old_time: + self.files_to_sync[key] = self.datalogs[key]['modify'] + # we have a dict of file names and last modified times + self.files_to_sync = sorted(self.files_to_sync.items(), key = lambda x:x[1]) + + if (len(self.files_to_sync) == 0 or not self.okay_to_sync()): + time.sleep(2) + return + + payload = {'public_key_fingerprint': base64.b64encode(self.ssh_cred_fingerprint), '_xsrf':self.client.cookies['_xsrf'] } + upload_response = upload_request(self.cloudsync_url_upload, self.client, payload) + self.log(upload_response, 'DEBUG') + + if not upload_response: + time.sleep(3) + return + + # sync the oldest file first + file_to_send = self.files_to_sync[-1][0] + send_path = os.path.join(self.datalog_dir,file_to_send) + + + # upload_response + archive_folder = upload_response['archive_folder'] + rsynccmd = '''rsync -ahHzv --progress -e "ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -F /dev/null -i {0} -p {1}" "{2}" {3}@{4}:{5}'''.format(self.cloudsync_ssh_identity_file, + self.cloudsync_port, + send_path, + self.cloudsync_user, + self.cloudsync_address, + self.cloudsync_remote_dir) + + + self.datalogs.pop(file_to_send) + status_update = {'percent_sent':'0', 'current_time':time.time(), 'file':file_to_send, 'status':'starting', 'replyto':'dfsyncSyncUpdate'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : status_update}, target = 'webserver')) + + rsyncproc = subprocess.Popen(rsynccmd, + shell=True, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + self.rsync_pid = rsyncproc.pid + while self.okay_to_sync(): + next_line = rsyncproc.stdout.readline().decode("utf-8") + # TODO: log all of stdout to disk + if self.rsync_time.search(next_line): + # we found a line containing a status update + current_status = next_line.strip().split() + current_status = current_status[:4] + current_status[1] = current_status[1].strip('%') + current_status.append(str(time.time())) + current_status.append(file_to_send) + current_status.append('progress') + current_status.append('dfsyncSyncUpdate') + # send this to the webserver... + status_update = dict(zip(['data_sent', 'percent_sent', 'sending_rate', 'time_remaining', 'current_time', 'file', 'status', 'replyto'], current_status)) + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : status_update}, target = 'webserver')) + self.log({'dfsyncSyncUpdate': status_update}, 'DEBUG') + if not next_line: + break + + if self.okay_to_sync(): + # wait until process is really terminated + exitcode = rsyncproc.wait() + # check exit code + if exitcode == 0: + # archive the log on the CC + target_path = os.path.join(self.datalog_archive_dir, archive_folder) + mkdir_p(target_path) + archive_file_path = os.path.join(target_path, file_to_send) + shutil.move(send_path, archive_file_path) + msg = '{0} - Datalog rsync complete. Original datalog archived at {1}\n'.format(file_to_send, archive_file_path) + status_update = {'percent_sent':'100', 'current_time':time.time(), 'file':file_to_send, 'message':msg, 'status':'complete', 'replyto':'dfsyncSyncUpdate'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : status_update}, target = 'webserver')) + self.log(msg, 'INFO') + else: + error_lines = rsyncproc.stderr.readlines() + err_trace = '' + for line in error_lines: + err_trace += line.decode("utf-8") + msg = '{0} - An error during datalog rsync. Exit code: {1}. Error trace: \n{2}\n'.format(file_to_send, exitcode, err_trace) + status_update = {'error':err_trace, 'current_time':time.time(), 'file':file_to_send, 'status':'error', 'message':msg, 'replyto':'dfsyncSyncUpdate'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : status_update}, target = 'webserver')) + self.log(msg,'WARNING') + else: + self.request_rsync_exit() + + def request_rsync_exit(self): + if not self.rsync_pid: + return + + if pid_exists(self.rsync_pid): + # the rsync process is required to exit + print('INFO: attempting to stop rsync process') + + os.kill(self.rsync_pid, signal.SIGTERM) + try: + wait_pid(self.rsync_pid, timeout=0.1) + timeout = False + except: + timeout = True + + if timeout and pid_exists(self.rsync_pid): + os.kill(self.rsync_pid, signal.SIGKILL) + try: + wait_pid(self.rsync_pid, timeout=0.1) + timeout = False + except: + timeout = True + + if timeout and pid_exists(self.rsync_pid): + print("ERROR: failed to terminate and kill rsync process with pid: {0}".format(self.rsync_pid)) + + else: + print('INFO: rsync process stopped successfully') + + def stat_files_in_dir(self, datalog_dir): + ret = {} + datalogs = [f for f in os.listdir(datalog_dir) if os.path.isfile(os.path.join(datalog_dir, f))] + for datalog in datalogs: + datalog_path = os.path.join(datalog_dir, datalog) + datalog_stat = os.stat(datalog_path) + ret[datalog] = {'size':datalog_stat.st_size, 'modify':datalog_stat.st_mtime, 'time':time.time()} + return ret + + def okay_to_sync(self): + if (self.is_not_armed and self.have_path_to_cloud and self.syncing_enabled and not self.needs_unloading.is_set() and self.cloudsync_session and self.cloudsync_account_registered and self.cloudsync_account_verified): + return True + else: + return False + + def get_ssh_creds(self): + ssh_cred_path = os.path.expanduser(self.cloudsync_ssh_identity_file+'.pub') # use the public key + self.ssh_cred = file_get_contents(ssh_cred_path).strip() # need the '.strip()'! + self.ssh_cred_fingerprint = generate_key_fingerprint(ssh_cred_path) + + def process_in_queue_data(self, data): + print("{0} module got the following data: {1}".format(self.name, data)) + if 'dfsync_register' in data.keys(): + for key in data['dfsync_register'].keys(): + self.config[key] = data['dfsync_register'][key] + self.load_config() + self.get_ssh_creds() + # attempt registration with server + if self.have_path_to_cloud: + self.cloudsync_session = create_session(self.cloudsync_url_base, self.client) + + if self.cloudsync_session: + + payload = {'email': self.cloudsync_email, 'public_key': base64.b64encode(self.ssh_cred), '_xsrf':self.client.cookies['_xsrf'] } + ret = register(self.cloudsync_url_register, self.client, payload) + if ret: + # registration was OK + self.config['cloudsync_account_registered'] = True + self.load_config() + # TODO: report registration attempt success + j = {'message': ret['msg'], 'current_time':time.time(), 'replyto':'dfsyncSyncRegister'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : j}, target = 'webserver')) + self.log('cloudsync registration attempt successful', 'INFO') + return + + self.config['cloudsync_account_registered'] = False + self.load_config() + # TODO: report registration attempt fail and some useful details on how to fix it... + j = {'message':'Registration with cloudsync server failed', 'current_time':time.time(), 'replyto':'dfsyncSyncRegister'} + self.out_queue.put_nowait(json_wrap_with_target({"json_data" : j}, target = 'webserver')) + self.log('Cloudsync registration attempt failed', 'INFO') + # look at mavlink and set self.is_not_armed + # look at network and set have_path_to_cloud + # look at webserver and set syncing_enabled + pass + + + def unload_callback(self): + self.request_rsync_exit() + +def init(in_queue, out_queue): + '''initialise module''' + return DFSyncModule(in_queue, out_queue) + \ No newline at end of file diff --git a/APSyncFramework/modules/APSync_webserver/__init__.py b/APSyncFramework/modules/APSync_webserver/__init__.py index 06b42a7..166df23 100644 --- a/APSyncFramework/modules/APSync_webserver/__init__.py +++ b/APSyncFramework/modules/APSync_webserver/__init__.py @@ -2,11 +2,14 @@ import tornado.web import tornado.websocket import tornado.httpserver -import time, os, json +import time, os, json, logging +import base64 from APSyncFramework.modules.lib import APSync_module from APSyncFramework.utils.json_utils import json_wrap_with_target -from APSyncFramework.utils.file_utils import read_config, write_config +from APSyncFramework.utils.file_utils import read_config, write_config,file_get_contents +from APSyncFramework.utils.network_utils import make_ssh_key +from APSyncFramework.utils.common_utils import MatchDict from pymavlink import mavutil @@ -19,10 +22,17 @@ def __init__(self, in_queue, out_queue): self.mavlink = mavutil.mavlink.MAVLink('') def process_in_queue_data(self, data): - websocket_send_message(data) - + websocket_send_message(data) + def send_out_queue_data(self, data): - # work out what the data is + print "callback routed to send_out_queue_data for queue-up:"+str(data) + # work out what the data is and either pass it to a specific module for mandling, or handle it here immediately. + # we assume everything coming back from the websocket is a dict. If the data does not take this form then bail out + if not type(data) is dict: + print("websocket data is not of type dict: {0}".format(data)) + return + + # this is passing the data off to the "mavlink" module to handle this, as we don't know how to do that. if "mavlink_data" in data.keys(): if "mavpackettype" in data["mavlink_data"].keys(): msg_type = data["mavlink_data"]["mavpackettype"] @@ -39,13 +49,32 @@ def send_out_queue_data(self, data): base_mode = mavutil.mavlink.MAV_MODE_FLAG_TEST_ENABLED, custom_mode = 0, system_status = 4) - self.out_queue.put_nowait(json_wrap_with_target(msg, target = 'mavlink')) - + + # if its a block of config-file type data, we'll just write it to disk now. elif "config" in data.keys(): config = data["config"] write_config(config) + + # if it's something else calling itself json_data, then we will handle it here and pretend it came from somwhere else + elif "json_data" in data.keys(): # + folder = os.path.join(os.path.expanduser('~'), '.ssh') + # make it if we don't have it. + cred_name = 'id_apsync' # load this from config? + cred_path = os.path.join(folder, cred_name+'.pub') # only expose the public key? + if not os.path.isfile(cred_path): + make_ssh_key(folder, cred_name) + cred = file_get_contents(cred_path) + j = '{"json_data":{"result":"'+base64.b64encode(cred)+'","replyto":"getIdentityResponse"}}'; + print j + msg = json.loads(j) + # send it back out the websocket immediately, no need to wrap it, as it's not being routed beyond tornado and browser. + websocket_send_message(msg) + # its dfsync related, forward it to the dfsync module for processing + elif "dfsync_register" in data.keys(): + self.out_queue.put_nowait(json_wrap_with_target(data, target = 'dfsync')) + else: pass @@ -63,9 +92,8 @@ def unload(self): class MainHandler(tornado.web.RequestHandler): def get(self): - configs = read_config() # we read the .json config file on every non-websocket http request - for config_option in configs: - print "config_option: %s" %str(config_option) + configs = read_config() # we read the .json config file on every non-websocket http request + configs = dict((k, v) for k, v in configs.iteritems() if (isinstance(v, basestring))) self.render("index.html", configs=configs) class DefaultWebSocket(tornado.websocket.WebSocketHandler): @@ -81,16 +109,24 @@ def open(self): def on_message(self, message): print("received websocket message: {0}".format(message)) message = json.loads(message) - self.callback(message) + self.callback(message) # this sends it to the module.send_out_queue_data for further processing. def on_close(self): print("websocket closed") - + +class DFSyncHandler(tornado.web.RequestHandler): + def get(self): + configs = read_config() # we read the .json config file on every non-websocket http request + dfsync_configs = dict((k, v) for k, v in configs.iteritems() if (k.split('_')[0] == 'cloudsync' and isinstance(v, basestring))) + print dfsync_configs + self.render("dfsync.html", configs=dfsync_configs) + class Application(tornado.web.Application): def __init__(self, module): handlers = [ (r"/", MainHandler), (r"/websocket/", DefaultWebSocket, dict(callback=module.send_out_queue_data)), + (r"/dfsync", DFSyncHandler), ] settings = dict( cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__", @@ -101,10 +137,11 @@ def __init__(self, module): super(Application, self).__init__(handlers, **settings) def start_app(module): + logging.getLogger("tornado").setLevel(logging.WARNING) application = Application(module) # find config files, relative to where this .py file is kept: confdir = os.path.dirname(os.path.realpath(__file__)) - print confdir + module.log(confdir, 'DEBUG') server = tornado.httpserver.HTTPServer(application, ssl_options = { "certfile": os.path.join(confdir,"certs","certificate.pem"), "keyfile": os.path.join(confdir,"certs","privatekey.pem") diff --git a/APSyncFramework/modules/APSync_webserver/certs/authorized_keys b/APSyncFramework/modules/APSync_webserver/certs/authorized_keys deleted file mode 100644 index 3eec12a..0000000 --- a/APSyncFramework/modules/APSync_webserver/certs/authorized_keys +++ /dev/null @@ -1,2 +0,0 @@ -ssh-dss AAAAB3NzaC1kc3MAAACBAORh/JBxKdeRHf158AkUQWh3JG4Zt6VNcAkn0ooOJGu5fMfvaBIfSAyoQritjXvPUd3iFberi3WtFBHHpk0mah8unO+sh905929qAVCcIqZG5YfbLnXgVJdZCb4L1nYrDAYDbZuU/GsC9+mg35LLbiby//11WRH3AetWLkuZ/+ohAAAAFQCIJ24FgJkEPAjUpFcHkqPm2mjhpwAAAIAE2WzacRoj+u8rqdd3Talf90oPM8gYYgZF5XhCqoCS/JoRTKq5W18Sb4CP4xrnISD5imN7fcV5fHue7vCAxEHmOuPBA4z9DSruPL+VGMUJ/QNeLbNLjmkZdW8Ct75wK1mujn8xuFU/M8E3YE4kVWLxTMY6Ep2JlIu9rODtp7lgbAAAAIEAnGzjZWiYolhdAl2UOQwxFBp/8G9WMM5MGmNsFyY2utuJvKR1iPuxGW/c7KZHJeOJPX/6OAZIsHgaNSab1FugJ2nTYu5NKGPgYYdO8LobwcPI97hQU+HefMPHPpeH6xS85tyOUxy3OtZkFTKMgfX7ytW6EYBSkKksFI7PTH4psMA= buzz@buzzs-macbook-pro.local -ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFeRulhm757d0kHXmYIVzRczkJTQQSj4W+w+bRz0DlnMgKtHyNQSeF97b6QO3gspgAvl+rg8RfQxeptZG4hJ/3P366PkanMQWAh05kFLLru6sJMqMIC5Fl8S2Ei8HJoABb/GmaHbCeQslCKBT5Q5mphTL+AlC85JMUms+tfRl4QwpDgsnVXOrDNyWcGphDp7opzR4abZYmS8/fgr8qr6ZoiiYsZgCu9xDYdhc49tUHWIVLPurbXeBn+BHrZPWaUpZ+hvLLFNWQigx3YbVdTrsnp6srZHkk8+r0oPjeqo+P/0Os+kH2wLuoMJ/uALYOd+BBZce9WzyiGJokvqJhOQEl buzz@buzzbook.local diff --git a/APSyncFramework/modules/APSync_webserver/static/favicon.ico b/APSyncFramework/modules/APSync_webserver/static/favicon.ico new file mode 100644 index 0000000..c852a83 Binary files /dev/null and b/APSyncFramework/modules/APSync_webserver/static/favicon.ico differ diff --git a/APSyncFramework/modules/APSync_webserver/static/js/dfsync.js b/APSyncFramework/modules/APSync_webserver/static/js/dfsync.js new file mode 100644 index 0000000..387c402 --- /dev/null +++ b/APSyncFramework/modules/APSync_webserver/static/js/dfsync.js @@ -0,0 +1,15 @@ +var config = {}; + +document.getElementById("register_btn").onclick = submitConfig; + +function submitConfig(){ + config = {}; + $("form#dfsync_register_form :input").each(function(){ + console.log(this.type, this.id, this.value ); + config[this.id] = this.value + }); + console.log(JSON.stringify(config)) + send(JSON.stringify({"dfsync_register" : config})) +}; + + diff --git a/APSyncFramework/modules/APSync_webserver/static/js/websocket.js b/APSyncFramework/modules/APSync_webserver/static/js/websocket.js index dbb030f..afd26cb 100644 --- a/APSyncFramework/modules/APSync_webserver/static/js/websocket.js +++ b/APSyncFramework/modules/APSync_webserver/static/js/websocket.js @@ -38,11 +38,15 @@ function open_websocket() { console.log(event.data) return false; } - console.log(response); if (response.mavlink_data) { update_data_stream(response.mavlink_data); } + if (response.json_data) { + func = response.json_data.replyto + args = response.json_data + window[func](args); // execute the name of the function that came from the json as 'replyto', pass it the json-as-javascript. + } } } socket.onclose = function(e) { diff --git a/APSyncFramework/modules/APSync_webserver/templates/dfsync.html b/APSyncFramework/modules/APSync_webserver/templates/dfsync.html new file mode 100644 index 0000000..9df10f5 --- /dev/null +++ b/APSyncFramework/modules/APSync_webserver/templates/dfsync.html @@ -0,0 +1,108 @@ + + + + + APSync + + + + + + + + +
+ +
+
+ + + + + +
+ +
+ +
+
+ {% for config in configs %} +
+ +
+ +
+
+ {% end %} +
+ +
+ +
+ +
File:
+
+
+
+
+
+
+ +
+
+ +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/APSyncFramework/modules/APSync_webserver/templates/index.html b/APSyncFramework/modules/APSync_webserver/templates/index.html index 10951f4..f8d6ce9 100644 --- a/APSyncFramework/modules/APSync_webserver/templates/index.html +++ b/APSyncFramework/modules/APSync_webserver/templates/index.html @@ -5,6 +5,7 @@ APSync + @@ -43,12 +44,21 @@ - +

Awaiting heartbeat...

+
+ + + + +
  +
Enter text here...
+
+ @@ -71,4 +81,23 @@ send(JSON.stringify({"mavlink_data" : msg})) }; + + \ No newline at end of file diff --git a/APSyncFramework/modules/lib/APSync_module.py b/APSyncFramework/modules/lib/APSync_module.py index 5cd96d0..3f195f2 100644 --- a/APSyncFramework/modules/lib/APSync_module.py +++ b/APSyncFramework/modules/lib/APSync_module.py @@ -6,7 +6,7 @@ import traceback import setproctitle from APSyncFramework.utils.common_utils import PeriodicEvent -from APSyncFramework.utils.json_utils import ping +from APSyncFramework.utils.json_utils import ping, json_wrap_with_target from APSyncFramework.utils.file_utils import read_config class APModule(Process): @@ -16,6 +16,7 @@ def __init__(self, in_queue, out_queue, name, description = None): signal.signal(signal.SIGINT, self.exit_gracefully) signal.signal(signal.SIGTERM, self.exit_gracefully) self.daemon = True + self.config_changed = False self.config = read_config() self.start_time = time.time() self.last_ping = None @@ -43,8 +44,13 @@ def exit_gracefully(self, signum, frame): def unload(self): print self.name, 'called unload' + self.unload_callback() self.needs_unloading.set() + def unload_callback(self): + ''' overload to perform any module specific cleanup''' + pass + def run(self): if self.in_queue_thread is not None: self.in_queue_thread.start() @@ -79,6 +85,29 @@ def in_queue_handling(self, lock=None): def process_in_queue_data(self, data): pass + def log(self, message, level = 'INFO'): + +# CRITICAL +# ERROR +# WARNING +# INFO +# DEBUG +# NOTSET + self.out_queue.put_nowait(json_wrap_with_target({'msg':message, 'level':level}, target = 'logging')) + + def set_config(self, var_name, var_default): + new_val = self.config.get(var_name, var_default) + try: + cur_val = self.config[var_name] + if new_val != cur_val: + self.config_changed = True + except: + self.config_changed = True + + finally: + self.config[var_name] = new_val + return new_val + class Unload(): def __init__(self, name): self.ack = False diff --git a/APSyncFramework/utils/common_utils.py b/APSyncFramework/utils/common_utils.py index 27ce9ef..059a976 100644 --- a/APSyncFramework/utils/common_utils.py +++ b/APSyncFramework/utils/common_utils.py @@ -1,6 +1,7 @@ -import sys, time, os, errno +import sys, time, os, errno, re from pymavlink import mavutil + class Connection(object): def __init__(self, connection): @@ -137,3 +138,9 @@ def check_timeout(delay): else: # should never happen raise RuntimeError("unknown process exit status") + + + +class MatchDict(dict): + def get_matching(self, event): + return dict((k, v) for k, v in self.iteritems() if k.split('_')[0] == event) \ No newline at end of file diff --git a/APSyncFramework/utils/file_utils.py b/APSyncFramework/utils/file_utils.py index ffd325f..1432df1 100644 --- a/APSyncFramework/utils/file_utils.py +++ b/APSyncFramework/utils/file_utils.py @@ -5,6 +5,7 @@ from os.path import isfile, join, getmtime, dirname, realpath import re import time +import errno # determine current directory, as it's the "root" of the Web if sys.platform == 'win32': @@ -60,7 +61,7 @@ def write_config(json_data): dirname = WinAppRoot+'\\conf\\' filename = dirname+'WebConfigServer.windows.json' else: - filename = os.path.join(AppRoot,'conf','WebConfigServer_1.json') + filename = os.path.join(AppRoot,'conf','WebConfigServer.json') old = read_config() # for something to compage against before we change it. @@ -93,6 +94,7 @@ def file_put_contents(filename,data): f.close() + def file_get_contents(filename): maxlen = 10000 fp = open(filename,'rb') @@ -266,3 +268,13 @@ def change_leds(r=None,g=None,b=None): if (r != R ) or ( g != G ) or ( b != B ) : file_put_contents(folder+thefile,json.dumps(leds)) + +# https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python +def mkdir_p(path): + try: + os.makedirs(path) + except OSError as exc: # Python >2.5 + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise diff --git a/APSyncFramework/utils/network_utils.py b/APSyncFramework/utils/network_utils.py new file mode 100644 index 0000000..9220196 --- /dev/null +++ b/APSyncFramework/utils/network_utils.py @@ -0,0 +1,414 @@ +# python calls to linux shell commands +# unsure if network manager will be used long term... + +import subprocess, time +import os + +def run(args, shell = False): + try: + p = subprocess.check_output(args, stderr=subprocess.STDOUT, shell=shell).decode("utf-8") +# print p # prints the output if any for a zero return + return (0, p) + except OSError as e: # bad command +# print e + # TODO: log this error + return + except subprocess.CalledProcessError as e: # non zero return +# print e.returncode # the non zero return code +# print e.cmd # the cmd that caused it +# print e.output # the error output (if any) + return (e.returncode, e.output) + +def make_ssh_key(key_path = os.path.join(os.path.expanduser('~'), '.ssh'), key_name = 'id_apsync'): + + args = ["ssh-keygen", "-t", "rsa", "-f", os.path.join(key_path, key_name), "-N", ""] + print str(args) + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + print str(output) + for line in output.split('\n'): + if 'transmitted' in line.strip(): + print line + # we could do something with this output if desired... + + if returncode == 0: + return + else: + return False + +def generate_key_fingerprint(key_path): + args = ['ssh-keygen', '-lf', key_path] + ret = run(args) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + return output.strip().split(' ')[1].strip() + else: + return False + +def ping(ip, interface = "wlan0"): + print("Pinging {0} on {1}".format(ip, interface)) + args = ["ping", "-c", "2", "-I", str(interface), str(ip)] + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + for line in output.split('\n'): + if 'transmitted' in line.strip(): + print line + # we could do something with this output if desired... + + if returncode == 0: + return True + else: + return False + +def get_internet_status(interface = "wlan0"): + ping('google.com', interface = interface) + +def get_wifi_aps(password, interface): + args = "echo '{0}' | sudo -S wpa_cli -i {1} scan".format(password, interface) + attempt_count = 0 + result = None + while attempt_count < 10: + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + if returncode == 0: + output = output.strip().split(' ')[-1] + result = output.strip() + if 'OK' in result: + result = True + break + result = False + + if returncode == 255: + # not a wifi interface? + print 'ERROR: {0}'.format(output).strip() + return False + + time.sleep(1) + attempt_count += 1 + + if not result: + print 'ERROR: {0}'.format(output).strip() + return + + args = "echo '{0}' | sudo -S wpa_cli scan_results | grep PSK".format(password) + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + wifi = {} + output = output.split('\n') + for ent in output: + ent = ent.split(' ')[-1] + ent = ent.split('\t') + if len(ent) == 5: + wifi[ent[4]] = {'mac':ent[0], 'freq':ent[1], 'signal':ent[2], 'enc':ent[3]} + return wifi + + else: + return False + +def get_wifi_status(password, interface): + args = "echo '{0}' | sudo -S wpa_cli -i {1} status".format(password, interface) + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + wifi = {} + output = output.rstrip().split('\n') + for ent in output: + ent = ent.split(' ')[-1].strip("'") + ent = ent.split('=') + if len(ent) == 2: + wifi[ent[0]] = ent[1] + else: + wifi['interface'] = ent[0] + return wifi + + if returncode == 255: + # not a wifi interface? + print 'ERROR: {0}'.format(output).strip() + return False + +def restart_interface(password, interface = 'wlan0'): + args = "echo '{0}' | sudo -S ifdown {1}".format(password, interface) + ret = run(args, shell = True) + try: + (returncode, output) = ret + print ret + except ValueError: + # bad command + return + + args = "echo '{0}' | sudo -S ifup {1}".format(password, interface) + ret = run(args, shell = True) + try: + (returncode, output) = ret + print ret + except ValueError: + # bad command + return + +def search_string(line, _string): + _sub_string = _string.split(' ')[-1].strip() + try: + idx = line.index(_string) + sub_line = line[idx+len(_string):] + return sub_line.split(' ')[0].strip() + except ValueError: + return + +def get_network_interfaces(): + args = ["ifconfig"] + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + interfaces = {} + details = {} + new_interface = True + for line in output.split('\n')[:-1]: # drop the last line + if new_interface: + interface = line.split(' ')[0].strip() + new_interface = False + + elif line == '': + new_interface = True + # save the details from the last interface + if interface: + interfaces[interface] = details + # reset the current interface detials + interface = None + details = {} + + else: + for val in ['RX packets:', 'TX packets:', 'HWaddr ', 'inet addr:', 'Mask:', 'RX bytes:', 'TX bytes:', 'Bcast:' ]: + ret = search_string(line, val) + if ret: + details[val.strip().strip(':')] = ret + + if returncode == 0: + return interfaces + else: + return False + +def get_serial_ids(): + args = ['ls', '/dev/serial/by-id/*'] + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + +def shutdown(password): + args = "echo '{0}' | sudo -S shutdown -h now".format(password) + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + +def reboot(password): + args = "echo '{0}' | sudo -S reboot now".format(password) + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + +def nmcli_add_wifi_conn_client(ssid, wifi_key, interface = 'wlan0', conn_name="WiFiClient"): + connections = nmcli_c() + if not (connections or isinstance(connections, dict)): + # nmcli not supported? + return + + arg_list = [] + + connection = connections.get(conn_name, None) + if connection: + print("INFO: a connection named '{0}' already exists, skipping creation".format(conn_name)) + else: + args = "nmcli connection add con-name {0} type wifi ifname {1} ssid {2}".format(conn_name, interface, ssid) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} connection.autoconnect no".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} 802-11-wireless.mode infrastructure".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} wifi-sec.key-mgmt wpa-psk".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} 802-11-wireless-security.auth-alg open".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} wifi-sec.psk {1}".format(conn_name, wifi_key) + arg_list.append(args.split(' ')) + args = "nmcli connection up {0}".format(conn_name) + arg_list.append(args.split(' ')) + + for args in arg_list: + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode != 0: + # TODO: log this + print ret, arg_list + return False + + if output != '': + print output.strip() + + return True + +def nmcli_add_wifi_conn_ap(ssid='ardupilot', wifi_key='enRouteArduPilot', interface = 'wlan0', conn_name='WiFiAP', band = 'bg'): + supported_bands = ['bg', 'a'] # TODO: obtain these values for the interface + # see https://unix.stackexchange.com/questions/184175/how-to-set-up-wifi-hotspot-with-802-11n-mode for 'n' mode support + if band not in supported_bands: + print("WARNING: band '{0}' is not supported, valid options are 'bg' or 'a'. Defaulting to 'bg'".format(band)) + band = 'bg' + connections = nmcli_c() + if not (connections or isinstance(connections, dict)): + # nmcli not supported? + return + + arg_list = [] + + connection = connections.get(conn_name, None) + if connection: + print("INFO: a connection named '{0}' already exists, skipping creation".format(conn_name)) + else: + args = "nmcli connection add con-name {0} type wifi ifname {1} ssid {2}".format(conn_name, interface, ssid) + arg_list.append(args.split(' ')) + + args = "nmcli connection modify {0} connection.autoconnect no".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} 802-11-wireless.mode ap 802-11-wireless.band {1} ipv4.method shared".format(conn_name, band) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} wifi-sec.key-mgmt wpa-psk".format(conn_name) + arg_list.append(args.split(' ')) + args = "nmcli connection modify {0} wifi-sec.psk {1}".format(conn_name, wifi_key) + arg_list.append(args.split(' ')) + args = "nmcli connection up {0}".format(conn_name) + arg_list.append(args.split(' ')) + + for args in arg_list: + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode != 0: + # TODO: log this + print ret, arg_list + return False + + if output != '': + print output.strip() + + return True + +def nmcli_restart(password): + args = "echo '{0}' | sudo -S service network-manager restart".format(password) + ret = run(args, shell = True) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + return True + else: + return False + + +def nmcli_c(): + args = ['nmcli', 'c'] + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + output = output.strip().split('\n') + connections = {} + for ent in output: + if 'NAME' in ent: + col_names = ent.strip().lower().split() + else: + ent = ent.strip().split() + connections[ent[0]] = dict(zip(col_names[1:], ent[1:])) + + return connections + else: + return False + +def nmcli_d(): + args = ['nmcli', 'd'] + ret = run(args, shell = False) + try: + (returncode, output) = ret + except ValueError: + # bad command + return + + if returncode == 0: + output = output.strip().split('\n') + interfaces = {} + for ent in output: + if 'DEVICE' in ent: + col_names = ent.strip().lower().split() + else: + ent = ent.strip().split() + interfaces[ent[0]] = dict(zip(col_names[1:], ent[1:])) + + return interfaces + + else: + return False + +# need to check if the wifi device supports AP mode iw list (look for AP) +# then use iw dev to match the phy# to a device + +# can't seem to go from AP mode back to client mode without requiring a restart of network manager +# need to do the following: +# nmcli_add_wifi_conn_ap() +# nmcli_restart() +# nmcli_add_wifi_conn_client() \ No newline at end of file diff --git a/APSyncFramework/utils/requests_utils.py b/APSyncFramework/utils/requests_utils.py new file mode 100644 index 0000000..1194e3b --- /dev/null +++ b/APSyncFramework/utils/requests_utils.py @@ -0,0 +1,109 @@ +from APSyncFramework.utils.network_utils import make_ssh_key, generate_key_fingerprint +from APSyncFramework.utils.file_utils import mkdir_p, file_get_contents +import requests, sys, base64 + +debug = True # TODO:replace with logging + +def create_session(URL, client): + # Retrieve the CSRF token first + if not client.cookies.get('_xsrf', False): + # we dont have a xsrf cookie yet... + r = client.get(URL, verify=True) # sets cookie + if check_response(r): + if client.cookies.get('_xsrf', False): + # we now have a xsrf cookie + return True + else: + # the response from the server was OK, + # however we failed to get a xsrf cookie from the server + return False + else: + # the response from the server was bad + return False + else: + # we have an existing xsrf cookie for this session + return True + +def check_response(r): + try: + r.raise_for_status() + except Exception as e: + print('An error occured when handling your request: {0} - {1}\n{2}'.format(r.status_code, r.url, e)) + return False + if debug: + print('{0} - {1}'.format(r.status_code, r.url)) + return True + +def register(URL, client, payload): + r = client.post(URL, data=payload, verify=True) + if check_response(r): + r_dict = r.json() + return r_dict + return False + +def verify(URL, client, payload): + r = client.post(URL, data=payload, verify=True) + if check_response(r): + r_dict = r.json() + return r_dict + return False + +def upload_request(URL, client, payload): + r = client.post(URL, data=payload, verify=True) + if check_response(r): + r_dict = r.json() + return r_dict + return False + +if __name__ == '__main__': + import subprocess, os + + verified_with_server = False # set to True once you have registed with your public key and email + user_email_address = 'example@gmail.com' # verification email will be sent here + ssh_cred_name = 'id_apsync' # will be made if it does not exist + file_to_upload = '~/dflogger/APSync.log'# ~/example.txt + + file_to_upload = os.path.expanduser(file_to_upload) + ssh_cred_folder = os.path.join(os.path.expanduser('~'), '.ssh') + mkdir_p(ssh_cred_folder) # make the dir if it does not exist + ssh_cred_path = os.path.join(ssh_cred_folder, ssh_cred_name+'.pub') # only expose the public key? + if not os.path.isfile(ssh_cred_path): + make_ssh_key(ssh_cred_folder, ssh_cred_name) + ssh_cred = file_get_contents(ssh_cred_path).strip() # need the '.strip()'! + + client = requests.Session() + + URL0 = "https://apsync.cloud/" + URL1 = "https://apsync.cloud/register" + URL2 = "https://apsync.cloud/verify?hash=" + URL3 = "https://apsync.cloud/upload" + + if not verified_with_server: + # register + if create_session(URL0, client): + payload = {'email': user_email_address, 'public_key': base64.b64encode(ssh_cred), '_xsrf':client.cookies['_xsrf'] } + if register(URL1, client, payload): + print('verify your details via your email before re-running with "verified_with_server = True"') + else: + # check to see if the upload file exists + if not os.path.isfile(file_to_upload): + print('Could not find file: {0}'.format(file_to_upload)) + sys.exit(1) + + if create_session(URL0, client): + payload = {'public_key_fingerprint': base64.b64encode(generate_key_fingerprint(ssh_cred_path)), '_xsrf':client.cookies['_xsrf'] } + upload_request(URL3, client, payload) + + rsynccmd = '''rsync -ahHzv --progress -e "ssh -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -F /dev/null -i {0} -p 22" "{1}" apsync@apsync.cloud:~'''.format(os.path.join(ssh_cred_folder, ssh_cred_name), file_to_upload) + rsyncproc = subprocess.Popen( + rsynccmd, + shell=True, + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + ) + for line in rsyncproc.stdout.readlines(): + print(line) + exitcode = rsyncproc.wait() + print('rsync exit code: {0}'.format(exitcode)) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 1a39ec5..a9eba62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ setproctitle -protobuf tornado +future \ No newline at end of file diff --git a/run.py b/run.py index e3a451f..b0052a9 100755 --- a/run.py +++ b/run.py @@ -2,13 +2,7 @@ # when run from this entry point, you don't need to explicity set your PYTHONPATH to have the APSyncFramework/ folder in it. from APSyncFramework import APSync -from APSyncFramework.modules.lib import APSync_module -from APSyncFramework.utils.common_utils import Connection -from APSyncFramework.utils.json_utils import json_wrap_with_target - -apsync_state = None if __name__ == '__main__': apsync_state = APSync.APSync() - apsync_state.main_loop() - + apsync_state.main_loop() \ No newline at end of file