diff --git a/Filelist b/Filelist index b7fb61bdf7..e0eb8826d4 100644 --- a/Filelist +++ b/Filelist @@ -91,6 +91,7 @@ SRC_ALL = \ src/testdir/README.txt \ src/testdir/Make_all.mak \ src/testdir/*.in \ + src/testdir/*.py \ src/testdir/sautest/autoload/*.vim \ src/testdir/runtest.vim \ src/testdir/test[0-9]*.ok \ diff --git a/src/Makefile b/src/Makefile index 4b5f2b1d85..d9a2a5fdbb 100644 --- a/src/Makefile +++ b/src/Makefile @@ -2029,6 +2029,7 @@ test_arglist \ test_assert \ test_backspace_opt \ test_cdo \ + test_channel \ test_cursor_func \ test_delete \ test_expand \ @@ -2569,6 +2570,7 @@ shadow: runtime pixmaps ../../testdir/Make_all.mak \ ../../testdir/*.in \ ../../testdir/*.vim \ + ../../testdir/*.py \ ../../testdir/python* \ ../../testdir/sautest \ ../../testdir/test83-tags? \ diff --git a/src/channel.c b/src/channel.c index bb896c50c5..9a74d07da4 100644 --- a/src/channel.c +++ b/src/channel.c @@ -136,22 +136,25 @@ FILE *debugfd = NULL; add_channel(void) { int idx; - channel_T *new_channels; channel_T *ch; if (channels != NULL) + { for (idx = 0; idx < channel_count; ++idx) if (channels[idx].ch_fd < 0) /* re-use a closed channel slot */ return idx; - if (channel_count == MAX_OPEN_CHANNELS) - return -1; - new_channels = (channel_T *)alloc(sizeof(channel_T) * (channel_count + 1)); - if (new_channels == NULL) - return -1; - if (channels != NULL) - mch_memmove(new_channels, channels, sizeof(channel_T) * channel_count); - channels = new_channels; + if (channel_count == MAX_OPEN_CHANNELS) + return -1; + } + else + { + channels = (channel_T *)alloc((int)sizeof(channel_T) + * MAX_OPEN_CHANNELS); + if (channels == NULL) + return -1; + } + ch = &channels[channel_count]; (void)vim_memset(ch, 0, sizeof(channel_T)); @@ -716,17 +719,21 @@ channel_exe_cmd(int idx, char_u *cmd, typval_T *arg2, typval_T *arg3) { int is_eval = cmd[1] == 'v'; - if (is_eval && arg3->v_type != VAR_NUMBER) + if (is_eval && (arg3 == NULL || arg3->v_type != VAR_NUMBER)) { if (p_verbose > 2) EMSG("E904: third argument for eval must be a number"); } else { - typval_T *tv = eval_expr(arg, NULL); + typval_T *tv; typval_T err_tv; char_u *json; + /* Don't pollute the display with errors. */ + ++emsg_skip; + tv = eval_expr(arg, NULL); + --emsg_skip; if (is_eval) { if (tv == NULL) @@ -739,7 +746,8 @@ channel_exe_cmd(int idx, char_u *cmd, typval_T *arg2, typval_T *arg3) channel_send(idx, json, "eval"); vim_free(json); } - free_tv(tv); + if (tv != &err_tv) + free_tv(tv); } } else if (p_verbose > 2) @@ -791,7 +799,7 @@ may_invoke_callback(int idx) typval_T *arg3 = NULL; char_u *cmd = typetv->vval.v_string; - /* ["cmd", arg] */ + /* ["cmd", arg] or ["cmd", arg, arg] */ if (list->lv_len == 3) arg3 = &list->lv_last->li_tv; channel_exe_cmd(idx, cmd, &argv[1], arg3); @@ -1144,7 +1152,8 @@ channel_read_json_block(int ch_idx, int id, typval_T **rettv) /* Wait for up to 2 seconds. * TODO: use timeout set on the channel. */ - if (channel_wait(channels[ch_idx].ch_fd, 2000) == FAIL) + if (channels[ch_idx].ch_fd < 0 + || channel_wait(channels[ch_idx].ch_fd, 2000) == FAIL) break; channel_read(ch_idx); } diff --git a/src/testdir/test_channel.py b/src/testdir/test_channel.py old mode 100755 new mode 100644 index a706243e43..fb75938c1f --- a/src/testdir/test_channel.py +++ b/src/testdir/test_channel.py @@ -7,12 +7,6 @@ # Then Vim can send requests to the server: # :let response = ch_sendexpr(handle, 'hello!') # -# And you can control Vim by typing a JSON message here, e.g.: -# ["ex","echo 'hi there'"] -# -# There is no prompt, just type a line and press Enter. -# To exit cleanly type "quit". -# # See ":help channel-demo" in Vim. # # This requires Python 2.6 or later. @@ -30,58 +24,95 @@ except ImportError: # Python 2 import SocketServer as socketserver -thesocket = None - class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): print("=== socket opened ===") - global thesocket - thesocket = self.request while True: try: - data = self.request.recv(4096).decode('utf-8') + received = self.request.recv(4096).decode('utf-8') except socket.error: print("=== socket error ===") break except IOError: print("=== socket closed ===") break - if data == '': + if received == '': print("=== socket closed ===") break - print("received: {}".format(data)) - try: - decoded = json.loads(data) - except ValueError: - print("json decoding failed") - decoded = [-1, ''] + print("received: {}".format(received)) - # Send a response if the sequence number is positive. - # Negative numbers are used for "eval" responses. - if decoded[0] >= 0: - if decoded[1] == 'hello!': - # simply send back a string - response = "got it" - elif decoded[1] == 'make change': - # Send two ex commands at the same time, before replying to - # the request. - cmd = '["ex","call append(\\"$\\",\\"added1\\")"]' - cmd += '["ex","call append(\\"$\\",\\"added2\\")"]' - print("sending: {}".format(cmd)) - thesocket.sendall(cmd.encode('utf-8')) - response = "ok" - elif decoded[1] == '!quit!': - # we're done - sys.exit(0) + # We may receive two messages at once. Take the part up to the + # matching "]" (recognized by finding "]["). + todo = received + while todo != '': + splitidx = todo.find('][') + if splitidx < 0: + used = todo + todo = '' else: - response = "what?" + used = todo[:splitidx + 1] + todo = todo[splitidx + 1:] + if used != received: + print("using: {}".format(used)) - encoded = json.dumps([decoded[0], response]) - print("sending: {}".format(encoded)) - thesocket.sendall(encoded.encode('utf-8')) + try: + decoded = json.loads(used) + except ValueError: + print("json decoding failed") + decoded = [-1, ''] - thesocket = None + # Send a response if the sequence number is positive. + if decoded[0] >= 0: + if decoded[1] == 'hello!': + # simply send back a string + response = "got it" + elif decoded[1] == 'make change': + # Send two ex commands at the same time, before + # replying to the request. + cmd = '["ex","call append(\\"$\\",\\"added1\\")"]' + cmd += '["ex","call append(\\"$\\",\\"added2\\")"]' + print("sending: {}".format(cmd)) + self.request.sendall(cmd.encode('utf-8')) + response = "ok" + elif decoded[1] == 'eval-works': + # Send an eval request. We ignore the response. + cmd = '["eval","\\"foo\\" . 123", -1]' + print("sending: {}".format(cmd)) + self.request.sendall(cmd.encode('utf-8')) + response = "ok" + elif decoded[1] == 'eval-fails': + # Send an eval request that will fail. + cmd = '["eval","xxx", -2]' + print("sending: {}".format(cmd)) + self.request.sendall(cmd.encode('utf-8')) + response = "ok" + elif decoded[1] == 'eval-bad': + # Send an eval request missing the third argument. + cmd = '["eval","xxx"]' + print("sending: {}".format(cmd)) + self.request.sendall(cmd.encode('utf-8')) + response = "ok" + elif decoded[1] == 'eval-result': + # Send back the last received eval result. + response = last_eval + elif decoded[1] == '!quit!': + # we're done + self.server.shutdown() + break + elif decoded[1] == '!crash!': + # Crash! + 42 / 0 + else: + response = "what?" + + encoded = json.dumps([decoded[0], response]) + print("sending: {}".format(encoded)) + self.request.sendall(encoded.encode('utf-8')) + + # Negative numbers are used for "eval" responses. + elif decoded[0] < 0: + last_eval = decoded class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass @@ -97,7 +128,6 @@ if __name__ == "__main__": server_thread = threading.Thread(target=server.serve_forever) # Exit the server thread when the main thread terminates - server_thread.daemon = True server_thread.start() # Write the port number in Xportnr, so that the test knows it. @@ -105,6 +135,7 @@ if __name__ == "__main__": f.write("{}".format(port)) f.close() - # Block here print("Listening on port {}".format(port)) - server.serve_forever() + + # Main thread terminates, but the server continues running + # until server.shutdown() is called. diff --git a/src/testdir/test_channel.vim b/src/testdir/test_channel.vim index 140691c398..3caf5d21d9 100644 --- a/src/testdir/test_channel.vim +++ b/src/testdir/test_channel.vim @@ -2,14 +2,32 @@ scriptencoding utf-8 " This requires the Python command to run the test server. -" This most likely only works on Unix. -if !has('unix') || !executable('python') +" This most likely only works on Unix and Windows console. +if has('unix') + " We also need the pkill command to make sure the server can be stopped. + if !executable('python') || !executable('pkill') + finish + endif +elseif has('win32') && !has('gui_win32') + " Use Python Launcher for Windows (py.exe). + if !executable('py') + finish + endif +else finish endif -func Test_communicate() +let s:port = -1 + +func s:start_server() " The Python program writes the port number in Xportnr. - silent !./test_channel.py& + call delete("Xportnr") + + if has('win32') + silent !start cmd /c start "test_channel" py test_channel.py + else + silent !python test_channel.py& + endif " Wait for up to 2 seconds for the port number to be there. let cnt = 20 @@ -29,11 +47,29 @@ func Test_communicate() if len(l) == 0 " Can't make the connection, give up. - call system("killall test_channel.py") + call s:kill_server() + call assert_false(1, "Can't start test_channel.py") + return -1 + endif + let s:port = l[0] + + let handle = ch_open('localhost:' . s:port, 'json') + return handle +endfunc + +func s:kill_server() + if has('win32') + call system('taskkill /IM py.exe /T /F /FI "WINDOWTITLE eq test_channel"') + else + call system("pkill -f test_channel.py") + endif +endfunc + +func Test_communicate() + let handle = s:start_server() + if handle < 0 return endif - let port = l[0] - let handle = ch_open('localhost:' . port, 'json') " Simple string request and reply. call assert_equal('got it', ch_sendexpr(handle, 'hello!')) @@ -46,8 +82,51 @@ func Test_communicate() call assert_equal('added1', getline(line('$') - 1)) call assert_equal('added2', getline('$')) + " Send an eval request that works. + call assert_equal('ok', ch_sendexpr(handle, 'eval-works')) + call assert_equal([-1, 'foo123'], ch_sendexpr(handle, 'eval-result')) + + " Send an eval request that fails. + call assert_equal('ok', ch_sendexpr(handle, 'eval-fails')) + call assert_equal([-2, 'ERROR'], ch_sendexpr(handle, 'eval-result')) + + " Send a bad eval request. There will be no response. + call assert_equal('ok', ch_sendexpr(handle, 'eval-bad')) + call assert_equal([-2, 'ERROR'], ch_sendexpr(handle, 'eval-result')) + " make the server quit, can't check if this works, should not hang. call ch_sendexpr(handle, '!quit!', 0) - call system("killall test_channel.py") + call s:kill_server() +endfunc + +" Test that we can open two channels. +func Test_two_channels() + let handle = s:start_server() + if handle < 0 + return + endif + call assert_equal('got it', ch_sendexpr(handle, 'hello!')) + + let newhandle = ch_open('localhost:' . s:port, 'json') + call assert_equal('got it', ch_sendexpr(newhandle, 'hello!')) + call assert_equal('got it', ch_sendexpr(handle, 'hello!')) + + call ch_close(handle) + call assert_equal('got it', ch_sendexpr(newhandle, 'hello!')) + + call s:kill_server() +endfunc + +" Test that a server crash is handled gracefully. +func Test_server_crash() + let handle = s:start_server() + if handle < 0 + return + endif + call ch_sendexpr(handle, '!crash!') + + " kill the server in case if failed to crash + sleep 10m + call s:kill_server() endfunc diff --git a/src/version.c b/src/version.c index 42adbe9072..7399fe98f6 100644 --- a/src/version.c +++ b/src/version.c @@ -757,6 +757,26 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1256, +/**/ + 1255, +/**/ + 1254, +/**/ + 1253, +/**/ + 1252, +/**/ + 1251, +/**/ + 1250, +/**/ + 1249, +/**/ + 1248, +/**/ + 1247, /**/ 1246, /**/