qio: general non-default GMainContext support
v3 cover qio: general non-default GMainContext support
V1: http://lists.nongnu.org/archive/html/qemu-devel/2018-02/msg06972.html
V2: http://lists.gnu.org/archive/html/qemu-devel/2018-03/msg00016.html
Quoted 1 line
From this version, I'll split the old series into two: one QIO series
and one CHARDEV series. This is the QIO part.
Hopefully I have addressed every comments in v2. Please shoot if I missed any of them. Thanks,
v3:
- add r-bs
- move the migration patch out of the series [Dan]
- in comments, mention "%NULL" case for contexts [Paolo]
- do similar thing to listen_async and dgram_async to allow context parameter [Dan]
- cache TCPChardevTelnetInit properly in telnet patch, and fixes around that new change in the patch [Paolo]
- drop patch 5 [Dan]
- drop the helper tcp_chr_net_listener_setup() [Dan]
v2:
- collect r-bs
- qio_channel_add_watch_full() should still return the same thing as the old one, and introduced qio_channel_add_watch_full() to return a GSource pointer. [Dan]
- Fix commit message on RDMA. It's using QIO, but still, I am not touching it. [Dan]
- use qio_net_listener_set_client_func_full() directly, and avoid introducing new API. [Dan]
Please review. Thanks.
Peter Xu (6):
qio: rename qio_task_thread_result
qio: introduce qio_channel_add_watch_{full|source}
qio: store gsources for net listeners
qio: non-default context for threaded qtask
qio: non-default context for async conn
qio: non-default context for TLS handshake
chardev/char-socket.c | 5 ++-- include/io/channel-socket.h | 15 ++++++++--- include/io/channel-tls.h | 5 +++- include/io/channel.h | 44 ++++++++++++++++++++++++++++++++ include/io/net-listener.h | 22 ++++++++++++++-- include/io/task.h | 7 +++-- io/channel-socket.c | 18 ++++++++----- io/channel-tls.c | 45 ++++++++++++++++++++++++-------- io/channel.c | 40 ++++++++++++++++++++++++----- io/dns-resolver.c | 3 ++- io/net-listener.c | 58 ++++++++++++++++++++++++++---------------- io/task.c | 22 +++++++++++++--- migration/socket.c | 3 ++- migration/tls.c | 2 ++ nbd/client.c | 1 + nbd/server.c | 1 + tests/test-io-channel-socket.c | 4 +-- tests/test-io-channel-tls.c | 2 ++ tests/test-io-task.c | 2 ++ ui/vnc-auth-vencrypt.c | 1 + ui/vnc-ws.c | 1 + 21 files changed, 239 insertions(+), 62 deletions(-)
Signature · 1 line
2.14.3
PATCH v3 1/6 qio: rename qio_task_thread_result
It is strange that it was called gio_task_thread_result. Rename it to follow the naming rule of the file.
--- io/task.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-)
io/task.c +2 -2
@@ -80,7 +80,7 @@ struct QIOTaskThreadData { }; -static gboolean gio_task_thread_result (gpointer opaque) +static gboolean qio_task_thread_result (gpointer opaque) {struct QIOTaskThreadData *data = opaque;
@@ -110,7 +110,7 @@ static gpointer qio_task_thread_worker(gpointer opaque)* the worker results */ trace_qio_task_thread_exit (data->task); -g_idle_add (gio_task_thread_result, data); +g_idle_add (qio_task_thread_result, data);return NULL ; }
Signature · 1 line
2.14.3
PATCH v3 2/6 qio: introduce qio_channel_add_watch_{full|source}
In reply to Peter XuFirstly, introduce an internal qio_channel_add_watch_full(), which enhances qio_channel_add_watch() that context can be specified.
Then add a new API wrapper qio_channel_add_watch_source() to return a GSource pointer rather than a tag ID.
Note that the _source() call will keep a reference of GSource so that callers need to unref them explicitly when finished using the GSource.
--- include/io/channel.h | 44 ++++++++++++++++++++++++++++++++++++++++++++ io/channel.c | 40 ++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 6 deletions(-)
include/io/channel.h +44 -0
@@ -648,6 +648,50 @@ guint qio_channel_add_watch(QIOChannel *ioc,gpointer user_data, GDestroyNotify notify); +/** +* qio_channel_add_watch_full: +* @ioc: the channel object +* @condition: the I/O condition to monitor +* @func: callback to invoke when the source becomes ready +* @user_data: opaque data to pass to @func +* @notify: callback to free @user_data +* @context: the context to run the watch source +* +* Similar as qio_channel_add_watch(), but allows to specify context +* to run the watch source. +* +* Returns: the source ID +*/ +guint qio_channel_add_watch_full (QIOChannel *ioc, +GIOCondition condition, +QIOChannelFunc func, +gpointer user_data, +GDestroyNotify notify, +GMainContext *context); + +/** +* qio_channel_add_watch_source: +* @ioc: the channel object +* @condition: the I/O condition to monitor +* @func: callback to invoke when the source becomes ready +* @user_data: opaque data to pass to @func +* @notify: callback to free @user_data +* @context: gcontext to bind the source to +* +* Similar as qio_channel_add_watch(), but allows to specify context +* to run the watch source, meanwhile return the GSource object +* instead of tag ID, with the GSource referenced already. +* +* Note: callers is responsible to unref the source when not needed. +* +* Returns: the source pointer +*/ +GSource *qio_channel_add_watch_source (QIOChannel *ioc, +GIOCondition condition, +QIOChannelFunc func, +gpointer user_data, +GDestroyNotify notify, +GMainContext *context);/** * qio_channel_attach_aio_context:
io/channel.c +34 -6
@@ -299,11 +299,12 @@ void qio_channel_set_aio_fd_handler(QIOChannel *ioc, klass->io_set_aio_fd_handler (ioc, ctx, io_read, io_write, opaque); } -guint qio_channel_add_watch (QIOChannel *ioc, -GIOCondition condition, -QIOChannelFunc func, -gpointer user_data, -GDestroyNotify notify) +guint qio_channel_add_watch_full (QIOChannel *ioc, +GIOCondition condition, +QIOChannelFunc func, +gpointer user_data, +GDestroyNotify notify, +GMainContext *context) {GSource *source;guint id;
@@ -312,12 +313,39 @@ guint qio_channel_add_watch(QIOChannel *ioc,g_source_set_callback (source, (GSourceFunc )func, user_data, notify); - id =g_source_attach (source,NULL ); + id =g_source_attach (source, context);g_source_unref (source);return id; } +guint qio_channel_add_watch (QIOChannel *ioc, +GIOCondition condition, +QIOChannelFunc func, +gpointer user_data, +GDestroyNotify notify) +{ +return qio_channel_add_watch_full (ioc, condition, func, + user_data, notify,NULL ); +} + +GSource *qio_channel_add_watch_source (QIOChannel *ioc, +GIOCondition condition, +QIOChannelFunc func, +gpointer user_data, +GDestroyNotify notify, +GMainContext *context) +{ +GSource *source; +guint id; + + id =qio_channel_add_watch_full (ioc, condition, func, + user_data, notify, context); + source =g_main_context_find_source_by_id (context, id); +g_source_ref (source); +return source; +} + int qio_channel_shutdown(QIOChannel *ioc, QIOChannelShutdown how,
Signature · 1 line
2.14.3
PATCH v3 3/6 qio: store gsources for net listeners
In reply to Peter XuOriginally we were storing the GSources tag IDs. That'll be not enough if we are going to support non-default gcontext for QIO code. Switch to GSources without changing anything real. Now we still always pass in NULL, which means the default gcontext.
--- include/io/net-listener.h | 22 ++++++++++++++++-- io/net-listener.c | 58 +++++++++++++++++++++++++++++------------------ 2 files changed, 56 insertions(+), 24 deletions(-)
include/io/net-listener.h +20 -2
@@ -53,7 +53,7 @@ struct QIONetListener {char *name;QIOChannelSocket **sioc; -gulong *io_tag; +GSource **io_source;size_t nsioc;bool connected;
@@ -120,17 +120,35 @@ void qio_net_listener_add(QIONetListener *listener, QIOChannelSocket *sioc);/** -* qio_net_listener_set_client_func: +* qio_net_listener_set_client_func_full: * @listener: the network listener object * @func: the callback function * @data: opaque data to pass to @func * @notify: callback to free @data +* @context: the context that the sources will be bound to. If %NULL, +* the default context will be used. * * Register @func to be invoked whenever a new client * connects to the listener. @func will be invoked * passing in the QIOChannelSocket instance for the * client. */ +void qio_net_listener_set_client_func_full (QIONetListener *listener, +QIONetListenerClientFunc func, +gpointer data, +GDestroyNotify notify, +GMainContext *context); + +/** +* qio_net_listener_set_client_func: +* @listener: the network listener object +* @func: the callback function +* @data: opaque data to pass to @func +* @notify: callback to free @data +* +* Wrapper of qio_net_listener_set_client_func_full(), only that the +* sources will always be bound to default main context. +*/ void qio_net_listener_set_client_func(QIONetListener *listener, QIONetListenerClientFunc func, gpointer data,
io/net-listener.c +36 -22
123 lines not on this page · raw message
Signature · 1 line
2.14.3
PATCH v3 4/6 qio: non-default context for threaded qtask
In reply to Peter Xuqio_task_run_in_thread() allows main thread to run blocking operations in the background. However it has an assumption on that it's always working with the default context. This patch tries to allow the threaded QIO task framework to run with non-default gcontext.
Currently no functional change so far, so the QIOTasks are still always running on main context.
--- include/io/task.h | 7 +++++-- io/channel-socket.c | 9 ++++++--- io/dns-resolver.c | 3 ++- io/task.c | 20 ++++++++++++++++++-- tests/test-io-task.c | 2 ++ 5 files changed, 33 insertions(+), 8 deletions(-)
include/io/task.h +5 -2
@@ -227,15 +227,18 @@ QIOTask *qio_task_new(Object *source,* @worker: the function to invoke in a thread * @opaque: opaque data to pass to @worker * @destroy: function to free @opaque +* @context: the context to run the complete hook. If %NULL, the +* default context will be used. * * Run a task in a background thread. When @worker * returns it will call qio_task_complete() in -* the main event thread context. +* the event thread context that provided. */ void qio_task_run_in_thread (QIOTask *task,QIOTaskWorker worker,gpointer opaque, -GDestroyNotify destroy); +GDestroyNotify destroy, +GMainContext *context);/** * qio_task_complete:
io/channel-socket.c +6 -3
@@ -187,7 +187,8 @@ void qio_channel_socket_connect_async(QIOChannelSocket *ioc,qio_task_run_in_thread (task, qio_channel_socket_connect_worker, addrCopy, - (GDestroyNotify )qapi_free_SocketAddress); + (GDestroyNotify )qapi_free_SocketAddress, +NULL ); }
@@ -245,7 +246,8 @@ void qio_channel_socket_listen_async(QIOChannelSocket *ioc,qio_task_run_in_thread (task, qio_channel_socket_listen_worker, addrCopy, - (GDestroyNotify )qapi_free_SocketAddress); + (GDestroyNotify )qapi_free_SocketAddress, +NULL ); }
@@ -321,7 +323,8 @@ void qio_channel_socket_dgram_async(QIOChannelSocket *ioc,qio_task_run_in_thread (task, qio_channel_socket_dgram_worker, data, - qio_channel_socket_dgram_worker_free); + qio_channel_socket_dgram_worker_free, +NULL ); }
io/dns-resolver.c +2 -1
@@ -233,7 +233,8 @@ void qio_dns_resolver_lookup_async(QIODNSResolver *resolver,qio_task_run_in_thread (task, qio_dns_resolver_lookup_worker, data, - qio_dns_resolver_lookup_data_free); + qio_dns_resolver_lookup_data_free, +NULL ); }
io/task.c +18 -2
@@ -77,6 +77,7 @@ struct QIOTaskThreadData {QIOTaskWorker worker;gpointer opaque;GDestroyNotify destroy; +GMainContext *context; };
@@ -91,6 +92,10 @@ static gboolean qio_task_thread_result(gpointer opaque) data->destroy (data->opaque); } +if (data->context) { +g_main_context_unref (data->context); + } +g_free (data);return FALSE;
@@ -100,6 +105,7 @@ static gboolean qio_task_thread_result(gpointer opaque)static gpointer qio_task_thread_worker (gpointer opaque) {struct QIOTaskThreadData *data = opaque; +GSource *idle;trace_qio_task_thread_run (data->task); data->worker (data->task, data->opaque);
@@ -110,7 +116,11 @@ static gpointer qio_task_thread_worker(gpointer opaque)* the worker results */ trace_qio_task_thread_exit (data->task); -g_idle_add (qio_task_thread_result, data); + + idle =g_idle_source_new (); +g_source_set_callback (idle, qio_task_thread_result, data,NULL ); +g_source_attach (idle, data->context); +return NULL ; }
@@ -118,15 +128,21 @@ static gpointer qio_task_thread_worker(gpointer opaque)void qio_task_run_in_thread (QIOTask *task,QIOTaskWorker worker,gpointer opaque, -GDestroyNotify destroy) +GDestroyNotify destroy, +GMainContext *context) { struct QIOTaskThreadData *data = g_new0(struct QIOTaskThreadData, 1);QemuThread thread; +if (context) { +g_main_context_ref (context); + } + data->task = task; data->worker = worker; data->opaque = opaque; data->destroy = destroy; + data->context = context;trace_qio_task_thread_start (task, worker, opaque); qemu_thread_create(&thread,
tests/test-io-task.c +2 -0
@@ -187,6 +187,7 @@ static void test_task_thread_complete(void)qio_task_run_in_thread (task, test_task_thread_worker, &data, +NULL ,NULL );g_main_loop_run (data.loop);
@@ -228,6 +229,7 @@ static void test_task_thread_failure(void)qio_task_run_in_thread (task, test_task_thread_worker, &data, +NULL ,NULL );g_main_loop_run (data.loop);
Signature · 1 line
2.14.3
PATCH v3 5/6 qio: non-default context for async conn
In reply to Peter XuWe have worked on qio_task_run_in_thread() already. Further, let all the qio channel APIs use that context.
--- chardev/char-socket.c | 4 ++-- include/io/channel-socket.h | 15 ++++++++++++--- io/channel-socket.c | 15 +++++++++------ migration/socket.c | 3 ++- tests/test-io-channel-socket.c | 4 ++-- 5 files changed, 27 insertions(+), 14 deletions(-)
chardev/char-socket.c +2 -2
@@ -866,7 +866,7 @@ static gboolean socket_reconnect_timeout(gpointer opaque)tcp_chr_set_client_ioc_name (chr, sioc);qio_channel_socket_connect_async (sioc, s->addr, qemu_chr_socket_connected, - chr,NULL ); + chr,NULL ,NULL );return false; }
@@ -950,7 +950,7 @@ static void qmp_chardev_open_socket(Chardev *chr,tcp_chr_set_client_ioc_name (chr, sioc);qio_channel_socket_connect_async (sioc, s->addr, qemu_chr_socket_connected, - chr,NULL ); + chr,NULL ,NULL ); }else {if (s->is_listen) {char *name;
include/io/channel-socket.h +12 -3
@@ -101,6 +101,8 @@ int qio_channel_socket_connect_sync(QIOChannelSocket *ioc,* @callback: the function to invoke on completion * @opaque: user data to pass to @callback * @destroy: the function to free @opaque +* @context: the context to run the async task. If %NULL, the default +* context will be used. * * Attempt to connect to the address @addr. This method * will run in the background so the caller will regain
@@ -113,7 +115,8 @@ void qio_channel_socket_connect_async(QIOChannelSocket *ioc,SocketAddress *addr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy); + GDestroyNotify destroy, + GMainContext *context);/**
@@ -138,6 +141,8 @@ int qio_channel_socket_listen_sync(QIOChannelSocket *ioc,* @callback: the function to invoke on completion * @opaque: user data to pass to @callback * @destroy: the function to free @opaque +* @context: the context to run the async task. If %NULL, the default +* context will be used. * * Attempt to listen to the address @addr. This method * will run in the background so the caller will regain
@@ -150,7 +155,8 @@ void qio_channel_socket_listen_async(QIOChannelSocket *ioc,SocketAddress *addr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy); + GDestroyNotify destroy, + GMainContext *context);/**
@@ -179,6 +185,8 @@ int qio_channel_socket_dgram_sync(QIOChannelSocket *ioc,* @callback: the function to invoke on completion * @opaque: user data to pass to @callback * @destroy: the function to free @opaque +* @context: the context to run the async task. If %NULL, the default +* context will be used. * * Attempt to initialize a datagram socket bound to * @localAddr and communicating with peer @remoteAddr.
@@ -194,7 +202,8 @@ void qio_channel_socket_dgram_async(QIOChannelSocket *ioc,SocketAddress *remoteAddr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy); + GDestroyNotify destroy, + GMainContext *context);/**
io/channel-socket.c +9 -6
@@ -173,7 +173,8 @@ void qio_channel_socket_connect_async(QIOChannelSocket *ioc,SocketAddress *addr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy) + GDestroyNotify destroy, + GMainContext *context) { QIOTask *task = qio_task_new(OBJECT (ioc), callback, opaque, destroy);
@@ -188,7 +189,7 @@ void qio_channel_socket_connect_async(QIOChannelSocket *ioc, qio_channel_socket_connect_worker, addrCopy, (GDestroyNotify )qapi_free_SocketAddress, - NULL); + context); }
@@ -233,7 +234,8 @@ void qio_channel_socket_listen_async(QIOChannelSocket *ioc,SocketAddress *addr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy) + GDestroyNotify destroy, + GMainContext *context) { QIOTask *task = qio_task_new(OBJECT (ioc), callback, opaque, destroy);
@@ -247,7 +249,7 @@ void qio_channel_socket_listen_async(QIOChannelSocket *ioc, qio_channel_socket_listen_worker, addrCopy, (GDestroyNotify )qapi_free_SocketAddress, - NULL); + context); }
@@ -309,7 +311,8 @@ void qio_channel_socket_dgram_async(QIOChannelSocket *ioc,SocketAddress *remoteAddr, QIOTaskFunc callback, gpointer opaque, - GDestroyNotify destroy) + GDestroyNotify destroy, + GMainContext *context) { QIOTask *task = qio_task_new(OBJECT (ioc), callback, opaque, destroy);
@@ -324,7 +327,7 @@ void qio_channel_socket_dgram_async(QIOChannelSocket *ioc, qio_channel_socket_dgram_worker, data, qio_channel_socket_dgram_worker_free, - NULL); + context); }
migration/socket.c +2 -1
@@ -103,7 +103,8 @@ static void socket_start_outgoing_migration(MigrationState *s, saddr, socket_outgoing_migration, data, - socket_connect_data_free); + socket_connect_data_free, + NULL);qapi_free_SocketAddress (saddr); }
tests/test-io-channel-socket.c +2 -2
@@ -179,7 +179,7 @@ static void test_io_channel_setup_async(SocketAddress *listen_addr, lioc =qio_channel_socket_new ();qio_channel_socket_listen_async ( lioc, listen_addr, - test_io_channel_complete, &data,NULL ); + test_io_channel_complete, &data,NULL ,NULL );g_main_loop_run (data.loop);g_main_context_iteration (g_main_context_default (), FALSE);
@@ -200,7 +200,7 @@ static void test_io_channel_setup_async(SocketAddress *listen_addr,qio_channel_socket_connect_async (QIO_CHANNEL_SOCKET (*src), connect_addr, - test_io_channel_complete, &data,NULL ); + test_io_channel_complete, &data,NULL ,NULL );g_main_loop_run (data.loop);g_main_context_iteration (g_main_context_default (), FALSE);
Signature · 1 line
2.14.3
PATCH v3 6/6 qio: non-default context for TLS handshake
In reply to Peter XuA new parameter "context" is added to qio_channel_tls_handshake() is to allow the TLS to be run on a non-default context. Still, no functional change.
--- chardev/char-socket.c | 1 + include/io/channel-tls.h | 5 ++++- io/channel-tls.c | 45 ++++++++++++++++++++++++++++++++++----------- migration/tls.c | 2 ++ nbd/client.c | 1 + nbd/server.c | 1 + tests/test-io-channel-tls.c | 2 ++ ui/vnc-auth-vencrypt.c | 1 + ui/vnc-ws.c | 1 + 9 files changed, 47 insertions(+), 12 deletions(-)
chardev/char-socket.c +1 -0
@@ -702,6 +702,7 @@ static void tcp_chr_tls_init(Chardev *chr)qio_channel_tls_handshake (tioc, tcp_chr_tls_handshake, chr, +NULL ,NULL ); }
include/io/channel-tls.h +4 -1
@@ -116,6 +116,8 @@ qio_channel_tls_new_client(QIOChannel *master,* @func: the callback to invoke when completed * @opaque: opaque data to pass to @func * @destroy: optional callback to free @opaque +* @context: the context that TLS handshake will run with. If %NULL, +* the default context will be used * * Perform the TLS session handshake. This method * will return immediately and the handshake will
@@ -126,7 +128,8 @@ qio_channel_tls_new_client(QIOChannel *master,void qio_channel_tls_handshake (QIOChannelTLS *ioc,QIOTaskFunc func,gpointer opaque, -GDestroyNotify destroy); +GDestroyNotify destroy, +GMainContext *context);/** * qio_channel_tls_get_session:
io/channel-tls.c +34 -11
90 lines not on this page · raw message
migration/tls.c +2 -0
@@ -105,6 +105,7 @@ void migration_tls_channel_process_incoming(MigrationState *s,qio_channel_tls_handshake (tioc, migration_tls_incoming_handshake,NULL , +NULL ,NULL ); }
@@ -159,5 +160,6 @@ void migration_tls_channel_connect(MigrationState *s,qio_channel_tls_handshake (tioc, migration_tls_outgoing_handshake, s, +NULL ,NULL ); }
nbd/client.c +1 -0
@@ -579,6 +579,7 @@ static QIOChannel *nbd_receive_starttls(QIOChannel *ioc,qio_channel_tls_handshake (tioc, nbd_tls_handshake, &data, +NULL ,NULL );if (!data.complete) {
nbd/server.c +1 -0
@@ -599,6 +599,7 @@ static QIOChannel *nbd_negotiate_handle_starttls(NBDClient *client,qio_channel_tls_handshake (tioc, nbd_tls_handshake, &data, +NULL ,NULL );if (!data.complete) {
tests/test-io-channel-tls.c +2 -0
@@ -203,10 +203,12 @@ static void test_io_channel_tls(const void *opaque)qio_channel_tls_handshake (clientChanTLS, test_tls_handshake_done, &clientHandshake, +NULL ,NULL );qio_channel_tls_handshake (serverChanTLS, test_tls_handshake_done, &serverHandshake, +NULL ,NULL );/*
ui/vnc-auth-vencrypt.c +1 -0
@@ -128,6 +128,7 @@ static int protocol_client_vencrypt_auth(VncState *vs, uint8_t *data, size_t lenqio_channel_tls_handshake (tls, vnc_tls_handshake_done, vs, +NULL ,NULL ); }return 0 ;
ui/vnc-ws.c +1 -0
@@ -81,6 +81,7 @@ gboolean vncws_tls_handshake_io(QIOChannel *ioc G_GNUC_UNUSED,qio_channel_tls_handshake (tls, vncws_tls_handshake_done, vs, +NULL ,NULL );return TRUE;
Signature · 1 line
2.14.3
Quoted 8 lines · Peter Xu
V1: http://lists.nongnu.org/archive/html/qemu-devel/2018-02/msg06972.html
V2: http://lists.gnu.org/archive/html/qemu-devel/2018-03/msg00016.htmlFrom this version, I'll split the old series into two: one QIO series and one CHARDEV series. This is the QIO part.
Hopefully I have addressed every comments in v2. Please shoot if I missed any of them. Thanks,
Thanks, I'm queuing this series and will send a pull req for it assuming it passes the usual tests.
Regards,
Daniel
Signature · 3 lines
|: https://berrange.com -o- https://www.flickr.com/photos/dberrange :| |: https://libvirt.org -o- https://fstop138.berrange.com :| |: https://entangle-photo.org -o- https://www.instagram.com/dberrange :|
Quoted 4 lines · Daniel P. Berrangé
Quoted 8 lines
Quoted 5 lines
From this version, I'll split the old series into two: one QIO series and one CHARDEV series. This is the QIO part.
Hopefully I have addressed every comments in v2. Please shoot if I missed any of them. Thanks,
Thanks, I'm queuing this series and will send a pull req for it assuming it passes the usual tests.
That'll be great. Thanks, Dan.
Signature · 1 line
Peter Xu