ソースを参照

支持设置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
     content: {}
 
+output_ctl:
+    title
+    
+
 
 客户端->服务器
 {

+ 5 - 0
test.py

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

+ 26 - 9
wsrepl/framework.py

@@ -5,8 +5,11 @@ from tornado.gen import coroutine, sleep
 import random, string
 from contextlib import contextmanager
 from tornado.log import gen_log
+from tornado import ioloop
+from tornado.concurrent import Future
 
-class Future:
+
+class Future_:
     def __iter__(self):
         result = yield
         return result
@@ -39,7 +42,6 @@ class Task:
         return '%s-%s' % (name, random_str)
 
     def __init__(self, coro, ws):
-        print('into Task __init__ `', coro, ws)
         self.ws = ws
         self.coro = coro
         self.coro_id = None
@@ -48,16 +50,18 @@ class Task:
 
         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):
         try:
             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:
             if len(e.args) == 1:
                 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)
 
-            # 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:

+ 1 - 1
wsrepl/html/index.html

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

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

@@ -95,11 +95,14 @@
         this.md_parser = new Mditor.Parser();
 
         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'];
@@ -246,7 +249,7 @@
         this.element = $(html);
 
         // 如果表单最后一个输入元素为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();
 
         // 输入控件创建

+ 4 - 5
wsrepl/interact.py

@@ -210,14 +210,14 @@ def _make_actions_input_spec(label, buttons, name):
     for act in buttons:
         if isinstance(act, Mapping):
             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'
             act = dict(zip(('value', 'label', 'disabled'), act))
         else:
             act = dict(value=act, label=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
 
 
@@ -285,9 +285,8 @@ def input_group(label, inputs, valid_func=None):
     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):

+ 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.mark2id = {}  # mark_name -> mark_id
 
+            self._closed = False
             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:
                 gen_log.debug('del self.coros[%s]', task.coro_id)
                 del self.coros[task.coro_id]
 
-            yield self.after_step()
-
-        @coroutine
-        def after_step(self):
             while self.inactive_coro_instances:
                 coro = self.inactive_coro_instances.pop()
                 task = Task(coro, ws=self)
                 self.coros[task.coro_id] = task
-                yield task.step()
+                task.step()
                 if self.coros[task.coro_id].task_finished:
                     gen_log.debug('del self.coros[%s]', 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):
-            # print('on_message', message)
+            print('on_message', message)
             # { event:, coro_id:, data: }
             data = json.loads(message)
             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)
                 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):
+            self._closed = True
             print("WebSocket closed")
 
+        def closed(self):
+            return self._closed
+
     handlers = [(r"/test", EchoWebSocket),
                 (r"/(.*)", StaticFileHandler,
                  {"path": '%s/html/' % project_dir,