Преглед на файлове

支持设置title,修复yield tornado future bug,添加聊天室demo

wangweimin преди 5 години
родител
ревизия
5241399a43
променени са 9 файла, в които са добавени 115 реда и са изтрити 42 реда
  1. 51 0
      chat_room.py
  2. 2 0
      doc/log.md
  3. 4 0
      doc/spec.md
  4. 5 0
      test.py
  5. 26 9
      wsrepl/framework.py
  6. 1 1
      wsrepl/html/index.html
  7. 6 3
      wsrepl/html/js/form.js
  8. 4 5
      wsrepl/interact.py
  9. 16 24
      wsrepl/ioloop.py

+ 51 - 0
chat_room.py

@@ -0,0 +1,51 @@
+from tornado import gen
+from tornado.ioloop import IOLoop
+from tornado import websocket
+import json
+
+from wsrepl.ioloop import start_ioloop
+from wsrepl.interact import *
+from tornado.gen import sleep
+
+chat_msgs = []  # 聊天记录 (name, msg)
+
+
+def refresh_msg(my_name):
+    last_idx = len(chat_msgs)
+    while True:
+        yield sleep(0.5)
+        for m in chat_msgs[last_idx:]:
+            if m[0] != my_name:  # 仅刷新其他人的新信息
+                text_print('%s:%s' % m)
+        last_idx = len(chat_msgs)
+
+
+# 业务逻辑 协程
+def main():
+    """
+    有返回值的交互函数需要yield from
+    :return:
+    """
+    set_title("Chat Room")
+    text_print("欢迎来到聊天室,你可以和当前所有在线的人聊天")
+    nickname = yield from input("请输入你的昵称", required=True)
+
+    chat_msgs.append(('*系统*', '%s加入房间' % nickname))
+    text_print("*系统*: %s加入房间" % nickname)
+    run_async(refresh_msg(nickname))
+
+    while True:
+        data = yield from input_group('输入消息', [
+            input('', name='msg'),
+            actions('', name='cmd', buttons=['发送', '退出'])
+        ])
+        if data['cmd'] == '退出':
+            break
+
+        text_print('%s:%s' % (nickname, data['msg']))
+        chat_msgs.append((nickname, data['msg']))
+
+    text_print("你已经退出聊天室")
+
+
+start_ioloop(main)

+ 2 - 0
doc/log.md

@@ -37,3 +37,5 @@ coro.send 内部可能还会存在 激活协程的调用,要禁止嵌套创建
 
 
 
 
 
 
+2/12 
+发现tornado对于一个ws连接,若on——message不结束,无法进行下一个

+ 4 - 0
doc/spec.md

@@ -82,6 +82,10 @@ output:
     type: text
     type: text
     content: {}
     content: {}
 
 
+output_ctl:
+    title
+    
+
 
 
 客户端->服务器
 客户端->服务器
 {
 {

+ 5 - 0
test.py

@@ -10,6 +10,11 @@ from tornado.gen import sleep
 
 
 # 业务逻辑 协程
 # 业务逻辑 协程
 def say_hello():
 def say_hello():
+    """
+    有返回值的交互函数需要yield from
+    :return:
+    """
+    set_title("This is title")
     # 向用户输出文字
     # 向用户输出文字
     text_print("Welcome!!!")
     text_print("Welcome!!!")
     res = yield from actions('Action button', [
     res = yield from actions('Action button', [

+ 26 - 9
wsrepl/framework.py

@@ -5,8 +5,11 @@ from tornado.gen import coroutine, sleep
 import random, string
 import random, string
 from contextlib import contextmanager
 from contextlib import contextmanager
 from tornado.log import gen_log
 from tornado.log import gen_log
+from tornado import ioloop
+from tornado.concurrent import Future
 
 
-class Future:
+
+class Future_:
     def __iter__(self):
     def __iter__(self):
         result = yield
         result = yield
         return result
         return result
@@ -39,7 +42,6 @@ class Task:
         return '%s-%s' % (name, random_str)
         return '%s-%s' % (name, random_str)
 
 
     def __init__(self, coro, ws):
     def __init__(self, coro, ws):
-        print('into Task __init__ `', coro, ws)
         self.ws = ws
         self.ws = ws
         self.coro = coro
         self.coro = coro
         self.coro_id = None
         self.coro_id = None
@@ -48,16 +50,18 @@ class Task:
 
 
         self.coro_id = self.gen_coro_id(self.coro)
         self.coro_id = self.gen_coro_id(self.coro)
 
 
+        self.pending_futures = {}  # id(future) -> future
+
+        gen_log.debug('Task[%s] __init__ ', self.coro_id)
 
 
-    @coroutine
     def step(self, result=None):
     def step(self, result=None):
         try:
         try:
             with self.ws_context():
             with self.ws_context():
-                res = self.coro.send(result)
-            while res is not None:
-                r = yield res
-                with self.ws_context():
-                    res = self.coro.send(r)
+                future_or_none = self.coro.send(result)
+            if future_or_none is not None:
+                if not self.ws.closed():
+                    future_or_none.add_done_callback(self._tornado_future_callback)
+                    self.pending_futures[id(future_or_none)] = future_or_none
         except StopIteration as e:
         except StopIteration as e:
             if len(e.args) == 1:
             if len(e.args) == 1:
                 self.result = e.args[0]
                 self.result = e.args[0]
@@ -66,7 +70,20 @@ class Task:
 
 
             gen_log.debug('Task[%s] finished, self.coros:%s', self.coro_id, self.ws.coros)
             gen_log.debug('Task[%s] finished, self.coros:%s', self.coro_id, self.ws.coros)
 
 
-            # raise
+    def _tornado_future_callback(self, future: Future):
+        del self.pending_futures[id(future)]
+        self.step(future.result())
+
+    def cancel(self):
+        gen_log.debug('Task[%s] canceled', self.coro_id)
+        self.coro.close()
+        while self.pending_futures:
+            _, f = self.pending_futures.popitem()
+            f.cancel()
+
+    def __del__(self):
+        if not self.task_finished:
+            gen_log.warning('Task[%s] not finished when destroy', self.coro_id)
 
 
 
 
 class Msg:
 class Msg:

+ 1 - 1
wsrepl/html/index.html

@@ -91,7 +91,7 @@
         viewer.scrollTop(md_body[0].scrollHeight);  // 自动滚动
         viewer.scrollTop(md_body[0].scrollHeight);  // 自动滚动
 
 
         var msg = JSON.parse(evt.data);
         var msg = JSON.parse(evt.data);
-        // console.log('>>>', msg);
+        console.log('>>>', msg);
         ctrl.handle_message(msg);
         ctrl.handle_message(msg);
     };
     };
     ws.onclose = function () {
     ws.onclose = function () {

+ 6 - 3
wsrepl/html/js/form.js

@@ -95,11 +95,14 @@
         this.md_parser = new Mditor.Parser();
         this.md_parser = new Mditor.Parser();
 
 
         this.handle_message = function (msg) {
         this.handle_message = function (msg) {
-            this.container_elem[0].innerHTML += this.md_parser.parse(msg.spec.content);
+            if (msg.command === 'output')
+                this.container_elem[0].innerHTML += this.md_parser.parse(msg.spec.content);
+            else if (msg.command === 'output_ctl')
+                $('#title').text(msg.spec.title);  // todo 不规范
         }
         }
     }
     }
 
 
-    OutputController.prototype.accept_command = ['output'];
+    OutputController.prototype.accept_command = ['output', 'output_ctl'];
 
 
 
 
     FormsController.prototype.accept_command = ['input', 'input_group', 'update_input', 'destroy_form'];
     FormsController.prototype.accept_command = ['input', 'input_group', 'update_input', 'destroy_form'];
@@ -246,7 +249,7 @@
         this.element = $(html);
         this.element = $(html);
 
 
         // 如果表单最后一个输入元素为actions组件,则隐藏默认的"提交"/"重置"按钮
         // 如果表单最后一个输入元素为actions组件,则隐藏默认的"提交"/"重置"按钮
-        if(this.spec.inputs.length && this.spec.inputs[this.spec.inputs.length-1].type==='actions')
+        if (this.spec.inputs.length && this.spec.inputs[this.spec.inputs.length - 1].type === 'actions')
             this.element.find('.ws-form-submit-btns').hide();
             this.element.find('.ws-form-submit-btns').hide();
 
 
         // 输入控件创建
         // 输入控件创建

+ 4 - 5
wsrepl/interact.py

@@ -210,14 +210,14 @@ def _make_actions_input_spec(label, buttons, name):
     for act in buttons:
     for act in buttons:
         if isinstance(act, Mapping):
         if isinstance(act, Mapping):
             assert 'value' in act and 'label' in act, 'actions item must have value and label key'
             assert 'value' in act and 'label' in act, 'actions item must have value and label key'
-        elif isinstance(act, Sequence):
+        elif isinstance(act, list):
             assert len(act) in (2, 3), 'actions item format error'
             assert len(act) in (2, 3), 'actions item format error'
             act = dict(zip(('value', 'label', 'disabled'), act))
             act = dict(zip(('value', 'label', 'disabled'), act))
         else:
         else:
             act = dict(value=act, label=act)
             act = dict(value=act, label=act)
         act_res.append(act)
         act_res.append(act)
 
 
-    input_item = dict(type='actions', label=label, name=name, buttons=buttons)
+    input_item = dict(type='actions', label=label, name=name, buttons=act_res)
     return input_item
     return input_item
 
 
 
 
@@ -285,9 +285,8 @@ def input_group(label, inputs, valid_func=None):
     return data
     return data
 
 
 
 
-def ctrl_coro(ctrl_info):
-    msg = dict(command="ctrl", spec=ctrl_info)
-    Global.active_ws.write_message(json.dumps(msg))
+def set_title(title):
+    send_msg('output_ctl', dict(title=title))
 
 
 
 
 def text_print(text, *, ws=None):
 def text_print(text, *, ws=None):

+ 16 - 24
wsrepl/ioloop.py

@@ -30,37 +30,34 @@ def start_ioloop(coro_func, port=8080):
             self.callbacks = OrderedDict()  # UI元素时的回调, key -> callback, mark_id
             self.callbacks = OrderedDict()  # UI元素时的回调, key -> callback, mark_id
             self.mark2id = {}  # mark_name -> mark_id
             self.mark2id = {}  # mark_name -> mark_id
 
 
+            self._closed = False
             self.inactive_coro_instances = []  # 待激活的协程实例列表
             self.inactive_coro_instances = []  # 待激活的协程实例列表
-            # self.tornado_coro_instances = []  # 待执行的tornado coro列表
 
 
-            task = Task(coro_func(), ws=self)
-            self.coros[task.coro_id] = task
+            self.main_task = Task(coro_func(), ws=self)
+            self.coros[self.main_task.coro_id] = self.main_task
 
 
-            yield task.step()
+            self.step_task(self.main_task)
+
+        def step_task(self, task, result=None):
+            task.step(result)
             if task.task_finished:
             if task.task_finished:
                 gen_log.debug('del self.coros[%s]', task.coro_id)
                 gen_log.debug('del self.coros[%s]', task.coro_id)
                 del self.coros[task.coro_id]
                 del self.coros[task.coro_id]
 
 
-            yield self.after_step()
-
-        @coroutine
-        def after_step(self):
             while self.inactive_coro_instances:
             while self.inactive_coro_instances:
                 coro = self.inactive_coro_instances.pop()
                 coro = self.inactive_coro_instances.pop()
                 task = Task(coro, ws=self)
                 task = Task(coro, ws=self)
                 self.coros[task.coro_id] = task
                 self.coros[task.coro_id] = task
-                yield task.step()
+                task.step()
                 if self.coros[task.coro_id].task_finished:
                 if self.coros[task.coro_id].task_finished:
                     gen_log.debug('del self.coros[%s]', task.coro_id)
                     gen_log.debug('del self.coros[%s]', task.coro_id)
                     del self.coros[task.coro_id]
                     del self.coros[task.coro_id]
-                # yield self.after_step()
 
 
-            # while self.tornado_coro_instances:
-            #     yield self.tornado_coro_instances.pop()
+            if self.main_task.task_finished:
+                self.close()
 
 
-        @coroutine
         def on_message(self, message):
         def on_message(self, message):
-            # print('on_message', message)
+            print('on_message', message)
             # { event:, coro_id:, data: }
             # { event:, coro_id:, data: }
             data = json.loads(message)
             data = json.loads(message)
             coro_id = data['coro_id']
             coro_id = data['coro_id']
@@ -69,20 +66,15 @@ def start_ioloop(coro_func, port=8080):
                 gen_log.error('coro not found, coro_id:%s', coro_id)
                 gen_log.error('coro not found, coro_id:%s', coro_id)
                 return
                 return
 
 
-            yield coro.step(data)
-
-            if coro.task_finished:
-                gen_log.debug('del self.coros[%s]', coro_id)
-                del self.coros[coro_id]
-
-            yield self.after_step()
-
-            if not self.coros:
-                self.close()
+            self.step_task(coro, data)
 
 
         def on_close(self):
         def on_close(self):
+            self._closed = True
             print("WebSocket closed")
             print("WebSocket closed")
 
 
+        def closed(self):
+            return self._closed
+
     handlers = [(r"/test", EchoWebSocket),
     handlers = [(r"/test", EchoWebSocket),
                 (r"/(.*)", StaticFileHandler,
                 (r"/(.*)", StaticFileHandler,
                  {"path": '%s/html/' % project_dir,
                  {"path": '%s/html/' % project_dir,