def _check_long_timers(): now_secs = window.Date().getTime() / 1000 for name, ob in _long_timers.items(): if ob.next_time < now_secs: try: ob.func() except Exception as err: console.warn(err) ob.next_time = now_secs + ob.interval
async def _pull(self, authtoken): # Fetch and wait for response url = tools.build_api_url("updates?since=" + self._server_time) init = dict(method="GET", headers={"authtoken": authtoken}) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") self._pull_statuses.append(res.status) self._pull_statuses = self._pull_statuses[-5:] # Process response if res.status != 200: text = await res.text() console.warn(res.status + " (" + res.statusText + ") " + text) self._set_state("error") # E.g. Wifi or server down, or 500 if res.status == 401: # Our token is probably expired. There may be local # changes that have not yet been pushed, which would # be lost if we logout. On the other hand, this may be # a lost/stolen device and the user revoked the token. # We can distinguish between these cases by determining # that the 401 is due to a token seed mismatch (revoked). self._auth_cantuse = text if "revoked" in text: window.location.href = "../logout" else: ob = JSON.parse(await res.text()) if ob.server_time: self._log_load("server", ob) # Reset? if ob.reset: await self._clear_cache() self.reset() self._server_time = ob.server_time # The odds of something going wrong here are tiny ... # but if they happen, we're out of sync with the server :( try: self.settings._put_received(*ob.settings) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (settings), see dev console for details.") try: self.records._put_received(*ob.records) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (records), see dev console for details.") # Set state to ok if we got new items, and if there were no errors if ob.settings or ob.records: if self.state != "warning": self._set_state("ok")
async def _pull(self, authtoken): # Build url url = location.protocol + "//" + location.hostname + ":" + location.port url = url.rstrip(":") + "/api/v1/updates?since=" + self._server_time # Fetch and wait for response init = dict(method="GET", headers={"authtoken": authtoken}) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") self._pull_statuses.append(res.status) self._pull_statuses = self._pull_statuses[-5:] # Process response if res.status != 200: console.warn(res.status + " (" + res.statusText + ") " + await res.text()) self._set_state("error") # E.g. Wifi or server down, or 500 if res.status == 403: if self._pull_statuses[-2] != 403 and self._pull_statuses[ -3] != 403: # We do have internet, but token is invalid: renew and sync sooner await window.auth.renew_maybe() self.sync_soon(4) else: ob = JSON.parse(await res.text()) if ob.server_time: self._log_load("server", ob) # Reset? if ob.reset: await self._clear_cache() self.reset() self._server_time = ob.server_time # The odds of something going wrong here are tiny ... # but if they happen, we're out of sync with the server :( try: self.settings._put_received(*ob.settings) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (settings), see dev console for details.") try: self.records._put_received(*ob.records) except Exception as err: self._set_state("warning") console.error(err) window.alert( "Sync error (records), see dev console for details.") # Set state to ok if we got new items, and if there were no errors if ob.settings or ob.records: if self.state != "warning": self._set_state("ok")
def get_auth_info(): """Get the authentication info or None.""" x = localStorage.getItem("timetagger_auth_info") if x: try: return JSON.parse(x) except Exception as err: console.warn("Cannot parse JSON auth info: " + str(err)) return None else: return None
async def _save_to_cache(self): if self._auth and self._auth.username: try: dump = { "key": self._auth.username, "server_time": self._server_time, "settings": self.settings.get_dump(), "records": self.records.get_dump(), } storage = window.tools.AsyncStorage() await storage.setItem(dump) except Exception as err: console.warn(err)
async def _push(self, kind, authtoken): # Build url url = location.protocol + "//" + location.hostname + ":" + location.port url = url.rstrip(":") + "/api/v1/" + kind # Take items, only proceed if nonempty items = self._to_push[kind] if len(items.keys()) == 0: return self._to_push[kind] = {} # Fetch and wait for response init = dict( method="PUT", body=JSON.stringify(items.values()), headers={"authtoken": authtoken}, ) try: res = await window.fetch(url, init) except Exception as err: res = dict(status=0, statusText=str(err), text=lambda: "") # Process response if res.status != 200: # The server was not able to process the request, maybe the # wifi is down or the server is restarting. # Put items back, but don't overwrite if the item was updated again. for key, item in items.items(): self._to_push[kind].setdefault(key, item) self._set_state( "error") # is usually less bad than the fail's below text = await res.text() console.warn(res.status + " (" + res.statusText + ") " + text) # Also notify the user for 402 errors if res.status == 402 and window.canvas: window.canvas.notify_once(text) else: # Success, but it can still mean that some records failed. In this # case these records are likely corrupt, so we delete them, and # will get the server's version back when we pull. d = JSON.parse(await res.text()) # d.accepted -> list of ok keys for key in d.fail: self[kind]._drop(key) for err in d.errors: self._set_state("warning") console.warn(f"Server dropped a {kind}: {err}")
async def _load_from_cache(self): if self._auth and self._auth.username: try: storage = window.tools.AsyncStorage() ob = await storage.getItem(self._auth.username) if ob and ob.server_time: self._log_load("cache", ob) self._server_time = ob.server_time self.settings._put_received(*ob.settings) self.records._put_received(*ob.records) for item in ob.settings: if item.st == 0: self._to_push["settings"][item.key] = item for item in ob.records: if item.st == 0: self._to_push["records"][item.key] = item except Exception as err: console.warn(err)
async def renew_webtoken(verbose=True, reset=False): """Renew the webtoken. Each webtoken expires after 14 days. But while valid, it can be exhcanged for a new one. By doing this while the app is active, users won't be logged out unless this device does not use the app for 14 days. If reset is True, the token seed is reset, causing all issued web tokens to become invalid. In other words: all sessions on other devices will be logged out. """ # Get current auth info auth = get_auth_info() if not auth: if verbose: console.warn("Could not renew token - not logged in") return # Make request and wait for response url = build_api_url("webtoken") if reset: url += "?reset=1" init = dict(method="GET", headers={"authtoken": auth.token}) res = await fetch(url, init) # Handle if res.status != 200: text = await res.text() console.warn("Could not renew token: " + text) if res.status == 401 and "revoked" in text: # When revoked, we logout to drop local changes. # See notes in stores.py where we do the same. if "/app/" in location.pathname: location.href = "../logout" else: location.href = "./logout" return # Are we still logged in. User may have logged out in the mean time. auth = get_auth_info() if not auth: return # Apply d = JSON.parse(await res.text()) set_auth_info_from_token(d.token) if verbose: console.warn("webtoken renewed")
def _receive_command(self, command): """ Process a command send from the server. """ cmd = command[0] if cmd == 'PING': # Used for roundtrip stuff, do at least one iter loop here ... window.setTimeout(self.send_command, 10, 'PONG', command[1]) elif cmd == 'INIT_DONE': window.flexx.spin(None) while len(self._pending_commands): self._receive_raw_command(self._pending_commands.pop(0)) self._pending_commands = None # print('init took', time() - self._init_time) elif cmd == 'PRINT': (window.console.ori_log or window.console.log)(command[1]) elif cmd == 'EXEC': eval(command[1]) elif cmd == 'EVAL': x = None if len(command) == 2: x = eval(command[1]) elif len(command) == 3: x = eval('this.instances.' + command[1] + '.' + command[2]) console.log(str(x)) # print (and thus also sends back result) elif cmd == 'EVALANDRETURN': try: x = eval(command[1]) except Exception as err: x = str(err) eval_id = command[2] # to identify the result in Python self.send_command("EVALRESULT", x, eval_id) elif cmd == 'INVOKE': id, name, args = command[1:] ob = self.instances.get(id, None) if ob is None: console.warn('Cannot invoke %s.%s; ' 'session does not know it (anymore).' % (id, name)) elif ob._disposed is True: pass # deleted, but other end might not be aware when command was send else: ob[name](*args) elif cmd == 'INSTANTIATE': self.instantiate_component( *command[1:]) # module, cname, id, args, kwargs elif cmd == 'DISPOSE': id = command[1] c = self.instances.get(id, None) if c is not None and c._disposed is False: # else: no need to warn c._dispose() self.send_command('DISPOSE_ACK', command[1]) self.instances.pop(id, None) # Drop local reference now elif cmd == 'DISPOSE_ACK': self.instances.pop(command[1], None) # Drop reference elif cmd == 'DEFINE': #and command[1] == 'JS' or command[1] == 'DEFINE-JS-EVAL '): kind, name, code = command[1:] window.flexx.spin() address = window.location.protocol + '//' + self.ws_url.split( '/')[2] code += '\n//# sourceURL=%s/flexx/assets/shared/%s\n' % (address, name) if kind == 'JS-EVAL': eval(code) elif kind == 'JS': # With this method, sourceURL does not work on Firefox, # but eval might not work for assets that don't "use strict" # (e.g. Bokeh). Note, btw, that creating links to assets does # not work because these won't be loaded on time. el = window.document.createElement("script") el.id = name el.innerHTML = code window.flexx.asset_node.appendChild(el) elif kind == 'CSS': el = window.document.createElement("style") el.type = "text/css" el.id = name el.innerHTML = code window.flexx.asset_node.appendChild(el) else: window.console.error('Dont know how to DEFINE ' + name + ' with "' + kind + '".') elif cmd == 'OPEN': window.win1 = window.open(command[1], 'new', 'chrome') else: window.console.error('Invalid command: "' + cmd + '"') return command
def _receive_command(self, command): """ Process a command send from the server. """ cmd = command[0] if cmd == 'PING': # Used for roundtrip stuff, do at least one iter loop here ... window.setTimeout(self.send_command, 10, 'PONG', command[1]) elif cmd == 'INIT_DONE': window.flexx.spin(None) while len(self._pending_commands): self._receive_raw_command(self._pending_commands.pop(0)) self._pending_commands = None # print('init took', time() - self._init_time) elif cmd == 'PRINT': (window.console.ori_log or window.console.log)(command[1]) elif cmd == 'EXEC': eval(command[1]) elif cmd == 'EVAL': x = None if len(command) == 2: x = eval(command[1]) elif len(command) == 3: x = eval('this.instances.' + command[1] + '.' + command[2]) console.log(str(x)) # print (and thus also sends back result) elif cmd == 'EVALANDRETURN': try: x = eval(command[1]) except Exception as err: x = str(err) eval_id = command[2] # to identify the result in Python self.send_command("EVALRESULT", x, eval_id) elif cmd == 'INVOKE': id, name, args = command[1:] ob = self.instances.get(id, None) if ob is None: console.warn('Cannot invoke %s.%s; ' 'session does not know it (anymore).' % (id, name)) elif ob._disposed is True: pass # deleted, but other end might not be aware when command was send else: ob[name](*args) elif cmd == 'INSTANTIATE': self.instantiate_component(*command[1:]) # module, cname, id, args, kwargs elif cmd == 'DISPOSE': id = command[1] c = self.instances.get(id, None) if c is not None and c._disposed is False: # else: no need to warn c._dispose() self.send_command('DISPOSE_ACK', command[1]) self.instances.pop(id, None) # Drop local reference now elif cmd == 'DISPOSE_ACK': self.instances.pop(command[1], None) # Drop reference elif cmd == 'DEFINE': #and command[1] == 'JS' or command[1] == 'DEFINE-JS-EVAL '): kind, name, code = command[1:] window.flexx.spin() address = window.location.protocol + '//' + self.ws_url.split('/')[2] code += '\n//# sourceURL=%s/flexx/assets/shared/%s\n' % (address, name) if kind == 'JS-EVAL': eval(code) elif kind == 'JS': # With this method, sourceURL does not work on Firefox, # but eval might not work for assets that don't "use strict" # (e.g. Bokeh). Note, btw, that creating links to assets does # not work because these won't be loaded on time. el = window.document.createElement("script") el.id = name el.innerHTML = code window.flexx.asset_node.appendChild(el) elif kind == 'CSS': el = window.document.createElement("style") el.type = "text/css" el.id = name el.innerHTML = code window.flexx.asset_node.appendChild(el) else: window.console.error('Dont know how to DEFINE ' + name + ' with "' + kind + '".') elif cmd == 'OPEN': window.win1 = window.open(command[1], 'new', 'chrome') else: window.console.error('Invalid command: "' + cmd + '"') return command