Pārlūkot izejas kodu

初步可用 2/11

wangweimin 5 gadi atpakaļ
vecāks
revīzija
0b04baef93
8 mainītis faili ar 402 papildinājumiem un 139 dzēšanām
  1. 18 1
      doc/log.md
  2. 12 8
      doc/spec.md
  3. 11 78
      test.py
  4. 10 11
      wsrepl/framework.py
  5. 3 2
      wsrepl/html/index.html
  6. 86 14
      wsrepl/html/js/form.js
  7. 236 15
      wsrepl/interact.py
  8. 26 10
      wsrepl/ioloop.py

+ 18 - 1
doc/log.md

@@ -19,4 +19,21 @@
 NOTE: 
 含有yield的函数一定是生成器,不管会不会执行到 (比如在分支里)
 
-coro.send 内部可能还会存在 激活协程的调用,要禁止或者将Global改成栈式存储
+coro.send 内部可能还会存在 激活协程的调用,要禁止嵌套创建协程Task或者将Global改成栈式存储
+
+
+2/10
+当前问题:
+    对于tornado coro的支持不是很友好:连续 yield tornado coro时,无法在yield间隙调度到其他coro执行 [todo]
+    使用tornado Future的callback应该可以解决
+    
+对于yield input()和 yield input_group([input(), input()])语法的实现:
+    input()返回一个msg对象,task接收到后,处理 发送
+    比上述更好地实现 [ok]
+    
+    
+2/11
+用户输入函数中,对结果无影响的非法参数可以以warnning而不是异常的方式提示用户 [ok]
+
+
+

+ 12 - 8
doc/spec.md

@@ -35,25 +35,27 @@
     required:
     value:
     placeholder: placeholder 属性是提示用户内容的输入格式。某些情况下 placeholder 属性对用户不可见, 所以当没有它时也需要保证form能被理解。
-    ^on_focus
-    ^on_blur
     ^inline  // type==checkbox,radio
     ^options // type==checkbox,radio , 字典列表 {*value:, *label:, checked,disabled }
 
 
 
-<button>
-ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button
-
-<select>
+type=<select>
 ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/select
 
-<textarea>
+type=<textarea>
 ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/textarea
 
+<button>
+ref https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button
+
+type=buttons
+    label
+    name
+    actions 字典列表 {*value:, *label:, disabled}
 
 input_group:
-    label:
+    label: # todo change to label
     inputs: [ <input>, ] // 若只有一个input 则可以忽略其label属性
 
 
@@ -93,6 +95,8 @@ input_event
     name:
     value:
 
+checkbox_radio 不产生blur事件
+
 from_submit:
     
 

+ 11 - 78
test.py

@@ -8,91 +8,24 @@ from wsrepl.interact import *
 from tornado.gen import sleep
 
 
-def other():
-    text_print("Welcome from other!!!")
-
-
-    event = yield from get_response("input_group", spec={
-        "label": 'other',
-        "inputs": [{
-            'name': 'other',
-            'type': 'text',
-            'autofocus': True,
-            'label': 'other',
-            'help_text': 'other help_text',
-        }]
-    })
-    idx = 0
-    while 1:
-        yield sleep(0.5)
-        text_print(str(idx))
-
-
 # 业务逻辑 协程
 def say_hello():
-    # yield sleep(0.5)
-
     # 向用户输出文字
     text_print("Welcome!!!")
-    # run_async(other())
-    # name = yield from text_input_coro('input your name')
-
-    event = yield from get_response("input_group", spec={
-        "label": 'another',
-        "inputs": [{
-            'name': 'name',
-            'type': 'text',
-            'autofocus': True,
-            'label': 'another text',
-            'help_text': 'another text help_text',
-        }]
-    })
-
-    event = yield from get_response("input_group", spec={
-        "label": 'label',
-        "inputs": [{
-            'name': 'name',
-            'type': 'text',
-            'autofocus': True,
-            'label': 'text',
-            'help_text': 'text help_text',
-        },
-            {
-                'name': 'checkbox',
-                'type': 'checkbox',
-                'inline': True,
-                'label': '性别',
-                'help_text': 'help_text',
-                'options': [
-                    {'value': 'man', 'label': '男', 'checked': True},
-                    {'value': 'woman', 'label': '女', 'checked': False}
-                ]
-            }
-        ]
-    })
-    json_print(event)
-
-    while event['event'] != 'from_submit':
-        json_print(event)
-        if event['event'] == 'input_event':
-            send_msg("update_input", spec={
-                'target_name': event['data']['name'],
-                'attributes': {
-                    'valid_status': True,
-                    'valid_feedback': 'ok'
-                }
-            })
-        event = yield
-
-    yield sleep(0.5)
-
-    text_print("收到")
+    res = yield from input('This is single input')
+    text_print('Your input:%s' % res)
 
-    yield from get_response("destroy_form", spec={})
+    res = yield from input('This is another single input')
+    text_print('Your input:%s' % res)
 
-    text_print("Bye ")
+    res = yield from input_group('Group input', [
+        input('Input 1', name='one'),
+        input('Input 2', name='two'),
+        select('Input 2', options=['A', 'B', 'C'], type=CHECKBOX, name='three')
+    ])
 
-    yield sleep(1)
+    text_print('Your input:')
+    json_print(res)
 
 
 start_ioloop(say_hello)

+ 10 - 11
wsrepl/framework.py

@@ -4,10 +4,9 @@ from collections import defaultdict
 from tornado.gen import coroutine, sleep
 import random, string
 from contextlib import contextmanager
-
+from tornado.log import gen_log
 
 class Future:
-
     def __iter__(self):
         result = yield
         return result
@@ -49,23 +48,23 @@ class Task:
 
         self.coro_id = self.gen_coro_id(self.coro)
 
-        # todo issue: 激活协程后,写成的返回值可能是tornado coro,需要执行
-        with self.ws_context():
-            res = self.coro.send(None)  # 激活协程
 
-        if res is not None:  # todo 执行完,还需要获取结果,coro.send(res)
-            self.ws.tornado_coro_instances.append(res)
-
-    def step(self, result):
+    @coroutine
+    def step(self, result=None):
         try:
             with self.ws_context():
                 res = self.coro.send(result)
-            return res
+            while res is not None:
+                r = yield res
+                with self.ws_context():
+                    res = self.coro.send(r)
         except StopIteration as e:
             if len(e.args) == 1:
                 self.result = e.args[0]
 
-            self.task_finished = Task
+            self.task_finished = True
+
+            gen_log.debug('Task[%s] finished, self.coros:%s', self.coro_id, self.ws.coros)
 
             # raise
 

+ 3 - 2
wsrepl/html/index.html

@@ -61,6 +61,7 @@
 
 <script src="js/mustache.min.js"></script>
 <script src="js/mditor.min.js"></script>
+<script src="js/async.min.js"></script>
 <script src="js/form.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/jquery@3.4.1/dist/jquery.min.js"></script>
 <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
@@ -78,7 +79,7 @@
 
     var old_send = ws.send;
     ws.send = function (d) {
-        console.log('<<<', JSON.parse(d));
+        // console.log('<<<', JSON.parse(d));
         old_send.apply(this, arguments);
     };
 
@@ -90,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 () {

+ 86 - 14
wsrepl/html/js/form.js

@@ -25,6 +25,26 @@
         return JSON.parse(JSON.stringify(obj));
     }
 
+    function Lock(func) {
+        this.func = func;
+        this.func_lock = false;
+        this.func_call_requests = [];
+
+        this.mutex_run = function (that, args) {
+            if (this.func_lock) {
+                this.func_call_requests.push(args);
+            } else {
+                this.func_lock = true;
+                this.func.call(that, args);
+                while (this.func_call_requests.length) {
+                    this.func.call(that, this.func_call_requests.pop());
+                }
+                this.func_lock = false;
+            }
+
+        }
+    }
+
     function LRUMap() {
         this.keys = [];
         this.map = {};
@@ -95,14 +115,43 @@
         this._activate_form = function (coro_id, old_ctrl) {
             var ctrls = this.form_ctrls.get_value(coro_id);
             var ctrl = ctrls[ctrls.length - 1];
-            if (ctrl === old_ctrl || old_ctrl === undefined)
-                return ctrl.element.show(100);
+            if (ctrl === old_ctrl || old_ctrl === undefined) {
+                console.log('开:%s', ctrl.spec.label);
+                return ctrl.element.show(200, function () {
+                    // 有时候autofocus属性不生效,手动激活一下
+                    $('input[autofocus]').focus();
+                });
+            }
             this.form_ctrls.move_to_top(coro_id);
+            var that = this;
             old_ctrl.element.hide(100, () => {
-                ctrl.element.show(100);
+                // ctrl.element.show(100);
+                // 需要在回调中重新获取当前前置表单元素,因为100ms内可能有变化
+                var t = that.form_ctrls.get_top();
+                if (t) t[t.length - 1].element.show(200, function () {
+                    // 有时候autofocus属性不生效,手动激活一下
+                    $('input[autofocus]').focus();
+                });
             });
         };
 
+        var that = this;
+        this.msg_queue = async.queue((msg) => {
+            that.consume_message(msg)
+        }, 1);
+
+        var l = new Lock(this.consume_message);
+
+        this.handle_message_ = function (msg) {
+            // this.msg_queue.push(msg);
+            // l.mutex_run(that, msg);
+            // console.log('start handle_message %s %s', msg.command, msg.spec.label);
+            this.consume_message(msg);
+            // console.log('end handle_message %s %s', msg.command, msg.spec.label);
+
+        };
+
+
         /*
         * 每次函数调用返回后,this.form_ctrls.get_top()的栈顶对应的表单为当前活跃表单
         * */
@@ -127,8 +176,8 @@
                     return console.error('No form to current message. coro_id:%s', msg.coro_id);
                 }
                 target_ctrls[target_ctrls.length - 1].dispatch_ctrl_message(msg.spec);
-                // 表单前置
-                this._activate_form(msg.coro_id, old_ctrl);
+                // 表单前置 removed
+                // this._activate_form(msg.coro_id, old_ctrl);
             } else if (msg.command === 'destroy_form') {
                 if (target_ctrls.length === 0) {
                     return console.error('No form to current message. coro_id:%s', msg.coro_id);
@@ -141,12 +190,16 @@
                 if (old_ctrls === target_ctrls) {
                     var that = this;
                     deleted.element.hide(100, () => {
+                        deleted.element.remove();
                         var t = that.form_ctrls.get_top();
-                        if (t) t[t.length - 1].element.show(100);
+                        if (t) t[t.length - 1].element.show(200, function () {
+                            $('input[autofocus]').focus();
+                        });
                     });
+                } else {
+                    deleted.element.remove();
                 }
             }
-            // todo: 如果当前栈顶key is not coro_id, hide show, move to top
         }
     }
 
@@ -207,23 +260,21 @@
         var that = this;
         this.element.on('submit', 'form', function (e) {
             e.preventDefault(); // avoid to execute the actual submit of the form.
-            var inputs = $(this).serializeArray();
             var data = {};
-            $.each(inputs, (idx, item) => {
-                if (data[item.name] === undefined) data[item.name] = [];
-                data[item.name].push(item.value);
+            $.each(that.input_controllers, (name, ctrl) => {
+                data[name] = ctrl.get_value();
             });
             ws.send(JSON.stringify({
                 event: "from_submit",
                 coro_id: that.coro_id,
                 data: data
             }));
-        })
+        });
     };
 
     FormController.prototype.dispatch_ctrl_message = function (spec) {
         if (!(spec.target_name in this.input_controllers)) {
-            return console.error('Can\'t find input[name=%s] element in curr form!' , spec.target_name);
+            return console.error('Can\'t find input[name=%s] element in curr form!', spec.target_name);
         }
 
         this.input_controllers[spec.target_name].update_input(spec);
@@ -330,6 +381,10 @@
         this.update_input_helper(-1, attributes);
     };
 
+    CommonInputController.prototype.get_value = function () {
+        return this.element.find('input').val();
+    };
+
     function CheckboxRadioController(ws_client, coro_id, spec) {
         FormItemController.apply(this, arguments);
 
@@ -369,7 +424,8 @@
         for (idx = 0; idx < this.spec.options.length; idx++) {
             var input_elem = elem.find('#' + id_name_prefix + '-' + idx);
             // blur事件时,发送当前值到服务器
-            input_elem.on('blur', this.send_value_listener);
+            // checkbox_radio 不产生blur事件
+            // input_elem.on('blur', this.send_value_listener);
 
             // 将额外的html参数加到input标签上
             for (var key in this.spec.options[idx]) {
@@ -393,6 +449,22 @@
         this.update_input_helper(idx, attributes);
     };
 
+    CheckboxRadioController.prototype.get_value = function () {
+        if (this.spec.type === 'radio') {
+            return this.element.find('input').val();
+        } else {
+            var value_arr = this.element.find('input').serializeArray();
+            var res = [];
+            var that = this;
+            $.each(value_arr, function (idx, val) {
+                if (val.name === that.spec.name)
+                    res.push(val.value);
+            });
+            return res;
+        }
+    };
+
+
 
     function WSREPLController(ws_client, output_container_elem, input_container_elem) {
         this.output_ctrl = new OutputController(ws_client, output_container_elem);

+ 236 - 15
wsrepl/interact.py

@@ -2,13 +2,18 @@ import tornado.websocket
 import time, json
 from collections import defaultdict
 from .framework import Future, Msg, Global
+from collections.abc import Iterable, Mapping, Sequence
+
+import logging
+
+logger = logging.getLogger(__name__)
 
 
 def run_async(coro):
     Global.active_ws.inactive_coro_instances.append(coro)
 
 
-def send_msg(cmd, spec):
+def send_msg(cmd, spec=None):
     msg = dict(command=cmd, spec=spec, coro_id=Global.active_coro_id)
     Global.active_ws.write_message(json.dumps(msg))
 
@@ -19,20 +24,237 @@ def get_response(cmd, spec):
     return response_msg
 
 
-# 非阻塞协程工具库
-def text_input_coro(prompt):
-    data = yield from get_response("input_group", spec={
-        "label": prompt,
-        "inputs": [{
-            'name': 'name',
-            'type': 'text',
-            'label': prompt,
-            'help_text': 'help_text',
+TEXT = 'text'
+NUMBER = "number"
+PASSWORD = "password"
+CHECKBOX = 'checkbox'
+RADIO = 'radio'
+SELECT = 'select'
+
+
+def _input_event_handle(valid_funcs, whole_valid_func=None):
+    """
+    根据提供的校验函数处理表单事件
+    :param valid_funcs: map(name -> valid_func)  valid_func 为 None 时,不进行验证
+                        valid_func: callback(data) -> error_msg
+    :param whole_valid_func: callback(data) -> (name, error_msg)
+    :return:
+    """
+    while True:
+        event = yield
+        event_name, event_data = event['event'], event['data']
+        if event_name == 'input_event':
+            input_event = event_data['event_name']
+            if input_event == 'on_blur':
+                onblur_name = event_data['name']
+                valid_func = valid_funcs.get(onblur_name)
+                if valid_func is None:
+                    continue
+                error_msg = valid_func(event_data['value'])
+                if error_msg is not None:
+                    send_msg('update_input', dict(target_name=onblur_name, attributes={
+                        'valid_status': False,
+                        'invalid_feedback': error_msg
+                    }))
+        elif event_name == 'from_submit':
+            all_valid = True
+
+            # 调用输入项验证函数进行校验
+            for name, valid_func in valid_funcs.items():
+                if valid_func is None:
+                    continue
+                error_msg = valid_func(event_data[name])
+                if error_msg is not None:
+                    all_valid = False
+                    send_msg('update_input', dict(target_name=name, attributes={
+                        'valid_status': False,
+                        'invalid_feedback': error_msg
+                    }))
+
+            # 调用表单验证函数进行校验
+            if whole_valid_func:
+                v_res = whole_valid_func(event_data)
+                if v_res is not None:
+                    all_valid = False
+                    onblur_name, error_msg = v_res
+                    send_msg('update_input', dict(target_name=onblur_name, attributes={
+                        'valid_status': False,
+                        'invalid_feedback': error_msg
+                    }))
+
+            if all_valid:
+                break
+
+    return event['data']
+
+
+def _make_input_spec(label, type, name, valid_func=None, multiple=None, inline=None, other_html_attrs=None,
+                     **other_kwargs):
+    """
+    校验传入input函数和select函数的参数
+    生成input_group消息中spec inputs参数列表项
+    支持的input类型 TEXT, NUMBER, PASSWORD, CHECKBOX, RADIO, SELECT
+    """
+    allowed_type = {TEXT, NUMBER, PASSWORD, CHECKBOX, RADIO, SELECT}
+    assert type in allowed_type, 'Input type not allowed.'
+
+    input_item = other_kwargs
+    input_item.update(other_html_attrs or {})
+    input_item.update(dict(label=label, type=type, name=name))
+
+    if valid_func is not None and type in (CHECKBOX, RADIO):  # CHECKBOX, RADIO 不支持valid_func参数
+        logger.warning('valid_func can\'t be used when type in (CHECKBOX, RADIO)')
+
+    if inline is not None and type not in {CHECKBOX, RADIO}:
+        logger.warning('inline 只能用于 CHECKBOX, RADIO type')
+    elif inline is not None:
+        input_item['inline'] = inline
+
+    if multiple is not None and type != SELECT:
+        logger.warning('multiple 参数只能用于SELECT type')
+    elif multiple is not None:
+        input_item['multiple'] = multiple
+
+    if type in {CHECKBOX, RADIO, SELECT}:
+        assert 'options' in input_item, 'Input type not allowed.'
+        assert isinstance(input_item['options'], Iterable), 'options must be list type'
+        # option 可用形式:
+        # {value:, label:, [checked:,] [disabled:]}
+        # (value, label, [checked,] [disabled])
+        # value 单值,label等于value
+        opts = input_item['options']
+        opts_res = []
+        for opt in opts:
+            if isinstance(opt, Mapping):
+                assert 'value' in opt and 'label' in opt, 'options item must have value and label key'
+            elif isinstance(opt, list):
+                assert len(opt) > 1 and len(opt) <= 4, 'options item format error'
+                opt = dict(zip(('value', 'label', 'checked', 'disabled'), opt))
+            else:
+                opt = dict(value=opt, label=opt)
+            opts_res.append(opt)
+
+        input_item['options'] = opts_res
+
+    # todo spec参数中,为默认值的可以不发送
+    return input_item
+
+
+def input(label, type=TEXT, *, valid_func=None, name='data', value='', placeholder='', required=False, readonly=False,
+          disabled=False, **other_html_attrs):
+    input_kwargs = dict(locals())
+    input_kwargs['label'] = ''
+    input_kwargs['__name__'] = input.__name__
+    input_kwargs.setdefault('autofocus', True)  # 如果没有设置autofocus参数,则开启参数
+
+    # 参数检查
+    allowed_type = {TEXT, NUMBER, PASSWORD}
+    assert type in allowed_type, 'Input type not allowed.'
+
+    data = yield from input_group(label=label, inputs=[input_kwargs])
+    return data[name]
+
+
+def select(label, options, type=SELECT, *, multiple=False, valid_func=None, name='data', value='', placeholder='',
+           required=False, readonly=False, disabled=False, inline=False, **other_html_attrs):
+    input_kwargs = dict(locals())
+    input_kwargs['label'] = ''
+    input_kwargs['__name__'] = select.__name__
+
+    allowed_type = {CHECKBOX, RADIO, SELECT}
+    assert type in allowed_type, 'Input type not allowed.'
+
+    data = yield from input_group(label=label, inputs=[input_kwargs])
+    return data[name]
+
+
+def _make_actions_input_spec(label, actions, name):
+    """
+    :param label:
+    :param actions: action 列表
+    action 可用形式:
+        {value:, label:, [disabled:]}
+        (value, label, [disabled])
+        value 单值,label等于value
+    :return:
+    """
+    act_res = []
+    for act in actions:
+        if isinstance(act, Mapping):
+            assert 'value' in act and 'label' in act, 'actions item must have value and label key'
+        elif isinstance(act, Sequence):
+            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='buttons', label=label, name=name, actions=actions)
+    return input_item
+
+
+def actions(label, actions, name='data'):
+    """
+    选择一个动作。UI为多个按钮,点击后会将整个表单提交
+    :param label:
+    :param actions: action 列表
+    action 可用形式:
+        {value:, label:, [disabled:]}
+        (value, label, [disabled])
+        value 单值,label等于value
+
+    实现方式:
+    多个type=submit的input组成
+    [
+        <button data-name value>label</button>,
+        ...
+    ]
+    """
+    input_kwargs = dict(label='', actions=actions, name=name)
+    input_kwargs['__name__'] = select.__name__
+    data = yield from input_group(label=label, inputs=[input_kwargs])
+    return data[name]
+
+
+def input_group(label, inputs, valid_func=None):
+    """
+    :param label:
+    :param inputs: list of generator or dict, dict的话,需要多加一项 __name__ 为当前函数名
+    :param valid_func: callback(data) -> (name, error_msg)
+    :return:
+    """
+    make_spec_funcs = {
+        actions.__name__: _make_actions_input_spec,
+        input.__name__: _make_input_spec,
+        select.__name__: _make_input_spec
+    }
+
+    item_valid_funcs = {}
+    spec_inputs = []
+    for input_g in inputs:
+        if isinstance(input_g, dict):
+            func_name = input_g.pop('__name__')
+            input_kwargs = input_g
+        else:
+            input_kwargs = dict(input_g.gi_frame.f_locals)  # 拷贝一份,不可以对locals进行修改
+            func_name = input_g.__name__
+
+        input_name = input_kwargs['name']
+        item_valid_funcs[input_name] = input_kwargs['valid_func']
+        input_item = make_spec_funcs[func_name](**input_kwargs)
+        spec_inputs.append(input_item)
+
+    if all('autofocus' not in i for i in spec_inputs):  # 每一个输入项都没有设置autofocus参数
+        for i in spec_inputs:
+            text_inputs = {TEXT, NUMBER, PASSWORD}  # todo update
+            if i.get('type') in text_inputs:
+                i['autofocus'] = True
+                break
 
-        }]
-    })
-    input_text = data['data']
-    return input_text['name']
+    send_msg('input_group', dict(label=label, inputs=spec_inputs))
+    data = yield from _input_event_handle(item_valid_funcs, valid_func)
+    send_msg('destroy_form')
+    return data
 
 
 def ctrl_coro(ctrl_info):
@@ -41,7 +263,6 @@ def ctrl_coro(ctrl_info):
 
 
 def text_print(text, *, ws=None):
-    print('text_print', Global.active_ws, text)
     msg = dict(command="output", spec=dict(content=text, type='text'))
     (ws or Global.active_ws).write_message(json.dumps(msg))
 

+ 26 - 10
wsrepl/ioloop.py

@@ -5,6 +5,8 @@ from .framework import Global, Msg, Task
 from os.path import abspath, dirname
 from tornado.web import StaticFileHandler
 from tornado.gen import coroutine, sleep
+from tornado.log import gen_log
+import logging
 
 project_dir = dirname(abspath(__file__))
 
@@ -29,11 +31,16 @@ def start_ioloop(coro_func, port=8080):
             self.mark2id = {}  # mark_name -> mark_id
 
             self.inactive_coro_instances = []  # 待激活的协程实例列表
-            self.tornado_coro_instances = []  # 待执行的tornado coro列表
+            # self.tornado_coro_instances = []  # 待执行的tornado coro列表
 
             task = Task(coro_func(), ws=self)
             self.coros[task.coro_id] = task
 
+            yield task.step()
+            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
@@ -42,9 +49,14 @@ def start_ioloop(coro_func, port=8080):
                 coro = self.inactive_coro_instances.pop()
                 task = Task(coro, ws=self)
                 self.coros[task.coro_id] = task
+                yield 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()
+            # while self.tornado_coro_instances:
+            #     yield self.tornado_coro_instances.pop()
 
         @coroutine
         def on_message(self, message):
@@ -52,17 +64,19 @@ def start_ioloop(coro_func, port=8080):
             # { event:, coro_id:, data: }
             data = json.loads(message)
             coro_id = data['coro_id']
+            coro = self.coros.get(coro_id)
+            if not coro_id:
+                gen_log.error('coro not found, coro_id:%s', coro_id)
+                return
 
-            res = self.coros[coro_id].step(data)
-            while res is not None:
-                r = yield res
-                res = self.coros[coro_id].step(r)
-
-            yield self.after_step()
+            yield coro.step(data)
 
-            if self.coros[coro_id].task_finished:
+            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()
 
@@ -74,6 +88,8 @@ def start_ioloop(coro_func, port=8080):
                  {"path": '%s/html/' % project_dir,
                   'default_filename': 'index.html'})]
 
+    gen_log.setLevel(logging.DEBUG)
+
     app = tornado.web.Application(handlers=handlers, debug=True)
     http_server = tornado.httpserver.HTTPServer(app)
     http_server.listen(port)