mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	fix(rpc)!: preseve files when stdio channel is closed (#22137)
BREAKING CHANGE: Unsaved changes are now preserved rather than discarded when stdio channel is closed.
This commit is contained in:
		| @@ -47,6 +47,9 @@ The following changes may require adaptations in user config or plugins. | |||||||
|  |  | ||||||
| • libiconv is now a required build dependency. | • libiconv is now a required build dependency. | ||||||
|  |  | ||||||
|  | • Unsaved changes are now preserved rather than discarded when |channel-stdio| | ||||||
|  |   is closed. | ||||||
|  |  | ||||||
| ============================================================================== | ============================================================================== | ||||||
| NEW FEATURES                                                    *news-features* | NEW FEATURES                                                    *news-features* | ||||||
|  |  | ||||||
|   | |||||||
| @@ -422,7 +422,12 @@ static void exit_event(void **argv) | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (!exiting) { |   if (!exiting) { | ||||||
|  |     if (ui_client_channel_id) { | ||||||
|       os_exit(status); |       os_exit(status); | ||||||
|  |     } else { | ||||||
|  |       assert(status == 0);  // Called from rpc_close(), which passes 0 as status. | ||||||
|  |       preserve_exit(NULL); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -674,6 +674,7 @@ void os_exit(int r) | |||||||
| void getout(int exitval) | void getout(int exitval) | ||||||
|   FUNC_ATTR_NORETURN |   FUNC_ATTR_NORETURN | ||||||
| { | { | ||||||
|  |   assert(!ui_client_channel_id); | ||||||
|   exiting = true; |   exiting = true; | ||||||
|  |  | ||||||
|   // On error during Ex mode, exit with a non-zero code. |   // On error during Ex mode, exit with a non-zero code. | ||||||
| @@ -794,6 +795,7 @@ void getout(int exitval) | |||||||
| } | } | ||||||
|  |  | ||||||
| /// Preserve files, print contents of `errmsg`, and exit 1. | /// Preserve files, print contents of `errmsg`, and exit 1. | ||||||
|  | /// @param errmsg  If NULL, this function will not print anything. | ||||||
| /// | /// | ||||||
| /// May be called from deadly_signal(). | /// May be called from deadly_signal(). | ||||||
| void preserve_exit(const char *errmsg) | void preserve_exit(const char *errmsg) | ||||||
| @@ -819,19 +821,21 @@ void preserve_exit(const char *errmsg) | |||||||
|     // For TUI: exit alternate screen so that the error messages can be seen. |     // For TUI: exit alternate screen so that the error messages can be seen. | ||||||
|     ui_client_stop(); |     ui_client_stop(); | ||||||
|   } |   } | ||||||
|  |   if (errmsg != NULL) { | ||||||
|     os_errmsg(errmsg); |     os_errmsg(errmsg); | ||||||
|     os_errmsg("\n"); |     os_errmsg("\n"); | ||||||
|  |   } | ||||||
|   if (ui_client_channel_id) { |   if (ui_client_channel_id) { | ||||||
|     os_exit(1); |     os_exit(1); | ||||||
|   } |   } | ||||||
|   ui_flush(); |  | ||||||
|  |  | ||||||
|   ml_close_notmod();                // close all not-modified buffers |   ml_close_notmod();                // close all not-modified buffers | ||||||
|  |  | ||||||
|   FOR_ALL_BUFFERS(buf) { |   FOR_ALL_BUFFERS(buf) { | ||||||
|     if (buf->b_ml.ml_mfp != NULL && buf->b_ml.ml_mfp->mf_fname != NULL) { |     if (buf->b_ml.ml_mfp != NULL && buf->b_ml.ml_mfp->mf_fname != NULL) { | ||||||
|  |       if (errmsg != NULL) { | ||||||
|         os_errmsg("Vim: preserving files...\r\n"); |         os_errmsg("Vim: preserving files...\r\n"); | ||||||
|       ui_flush(); |       } | ||||||
|       ml_sync_all(false, false, true);  // preserve all swap files |       ml_sync_all(false, false, true);  // preserve all swap files | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -839,7 +843,9 @@ void preserve_exit(const char *errmsg) | |||||||
|  |  | ||||||
|   ml_close_all(false);              // close all memfiles, without deleting |   ml_close_all(false);              // close all memfiles, without deleting | ||||||
|  |  | ||||||
|  |   if (errmsg != NULL) { | ||||||
|     os_errmsg("Vim: Finished.\r\n"); |     os_errmsg("Vim: Finished.\r\n"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   getout(1); |   getout(1); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -550,6 +550,10 @@ void rpc_close(Channel *channel) | |||||||
|  |  | ||||||
|   if (channel->streamtype == kChannelStreamStdio |   if (channel->streamtype == kChannelStreamStdio | ||||||
|       || (channel->id == ui_client_channel_id && channel->streamtype != kChannelStreamProc)) { |       || (channel->id == ui_client_channel_id && channel->streamtype != kChannelStreamProc)) { | ||||||
|  |     if (channel->streamtype == kChannelStreamStdio) { | ||||||
|  |       // Avoid hanging when there are no other UIs and a prompt is triggered on exit. | ||||||
|  |       remote_ui_disconnect(channel->id); | ||||||
|  |     } | ||||||
|     exit_from_channel(0); |     exit_from_channel(0); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2,6 +2,8 @@ local Screen = require('test.functional.ui.screen') | |||||||
| local helpers = require('test.functional.helpers')(after_each) | local helpers = require('test.functional.helpers')(after_each) | ||||||
|  |  | ||||||
| local clear = helpers.clear | local clear = helpers.clear | ||||||
|  | local command = helpers.command | ||||||
|  | local expect_exit = helpers.expect_exit | ||||||
| local buf, eq, feed_command = helpers.curbufmeths, helpers.eq, helpers.feed_command | local buf, eq, feed_command = helpers.curbufmeths, helpers.eq, helpers.feed_command | ||||||
| local feed, poke_eventloop = helpers.feed, helpers.poke_eventloop | local feed, poke_eventloop = helpers.feed, helpers.poke_eventloop | ||||||
| local ok = helpers.ok | local ok = helpers.ok | ||||||
| @@ -19,6 +21,7 @@ describe(':oldfiles', function() | |||||||
|   before_each(_clear) |   before_each(_clear) | ||||||
|  |  | ||||||
|   after_each(function() |   after_each(function() | ||||||
|  |     expect_exit(command, 'qall!') | ||||||
|     os.remove(shada_file) |     os.remove(shada_file) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
| @@ -42,6 +45,7 @@ describe(':oldfiles', function() | |||||||
|                                                                                                           | |                                                                                                           | | ||||||
|       Press ENTER or type command to continue^                                                             | |       Press ENTER or type command to continue^                                                             | | ||||||
|     ]]) |     ]]) | ||||||
|  |     feed('<CR>') | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   it('can be filtered with :filter', function() |   it('can be filtered with :filter', function() | ||||||
| @@ -107,6 +111,7 @@ describe(':browse oldfiles', function() | |||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   after_each(function() |   after_each(function() | ||||||
|  |     expect_exit(command, 'qall!') | ||||||
|     os.remove(shada_file) |     os.remove(shada_file) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ describe(':profile', function() | |||||||
|   before_each(helpers.clear) |   before_each(helpers.clear) | ||||||
|  |  | ||||||
|   after_each(function() |   after_each(function() | ||||||
|  |     helpers.expect_exit(command, 'qall!') | ||||||
|     if lfs.attributes(tempfile, 'uid') ~= nil then |     if lfs.attributes(tempfile, 'uid') ~= nil then | ||||||
|       os.remove(tempfile) |       os.remove(tempfile) | ||||||
|     end |     end | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ local nvim_prog = helpers.nvim_prog | |||||||
| local ok = helpers.ok | local ok = helpers.ok | ||||||
| local rmdir = helpers.rmdir | local rmdir = helpers.rmdir | ||||||
| local new_argv = helpers.new_argv | local new_argv = helpers.new_argv | ||||||
|  | local new_pipename = helpers.new_pipename | ||||||
| local pesc = helpers.pesc | local pesc = helpers.pesc | ||||||
| local os_kill = helpers.os_kill | local os_kill = helpers.os_kill | ||||||
| local set_session = helpers.set_session | local set_session = helpers.set_session | ||||||
| @@ -37,19 +38,8 @@ describe(':recover', function() | |||||||
|  |  | ||||||
| end) | end) | ||||||
|  |  | ||||||
| describe(':preserve', function() | describe("preserve and (R)ecover with custom 'directory'", function() | ||||||
|   local swapdir = lfs.currentdir()..'/Xtest_recover_dir' |   local swapdir = lfs.currentdir()..'/Xtest_recover_dir' | ||||||
|   before_each(function() |  | ||||||
|     clear() |  | ||||||
|     rmdir(swapdir) |  | ||||||
|     lfs.mkdir(swapdir) |  | ||||||
|   end) |  | ||||||
|   after_each(function() |  | ||||||
|     command('%bwipeout!') |  | ||||||
|     rmdir(swapdir) |  | ||||||
|   end) |  | ||||||
|  |  | ||||||
|   it("saves to custom 'directory' and (R)ecovers #1836", function() |  | ||||||
|   local testfile = 'Xtest_recover_file1' |   local testfile = 'Xtest_recover_file1' | ||||||
|   -- Put swapdir at the start of the 'directory' list. #1836 |   -- Put swapdir at the start of the 'directory' list. #1836 | ||||||
|   -- Note: `set swapfile` *must* go after `set directory`: otherwise it may |   -- Note: `set swapfile` *must* go after `set directory`: otherwise it may | ||||||
| @@ -59,15 +49,27 @@ describe(':preserve', function() | |||||||
|     set swapfile fileformat=unix undolevels=-1 |     set swapfile fileformat=unix undolevels=-1 | ||||||
|   ]] |   ]] | ||||||
|  |  | ||||||
|  |   local nvim1 | ||||||
|  |   before_each(function() | ||||||
|  |     nvim1 = spawn(new_argv()) | ||||||
|  |     set_session(nvim1) | ||||||
|  |     rmdir(swapdir) | ||||||
|  |     lfs.mkdir(swapdir) | ||||||
|  |   end) | ||||||
|  |   after_each(function() | ||||||
|  |     command('%bwipeout!') | ||||||
|  |     rmdir(swapdir) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|  |   local function setup_swapname() | ||||||
|     exec(init) |     exec(init) | ||||||
|     command('edit! '..testfile) |     command('edit! '..testfile) | ||||||
|     feed('isometext<esc>') |     feed('isometext<esc>') | ||||||
|     command('preserve') |  | ||||||
|     exec('redir => g:swapname | silent swapname | redir END') |     exec('redir => g:swapname | silent swapname | redir END') | ||||||
|  |     return eval('g:swapname') | ||||||
|  |   end | ||||||
|  |  | ||||||
|     local swappath1 = eval('g:swapname') |   local function test_recover(swappath1) | ||||||
|  |  | ||||||
|     os_kill(eval('getpid()')) |  | ||||||
|     -- Start another Nvim instance. |     -- Start another Nvim instance. | ||||||
|     local nvim2 = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed'}, |     local nvim2 = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed'}, | ||||||
|                                 true) |                                 true) | ||||||
| @@ -90,6 +92,35 @@ describe(':preserve', function() | |||||||
|     -- Verify that :swapname was not truncated (:help 'shortmess'). |     -- Verify that :swapname was not truncated (:help 'shortmess'). | ||||||
|     ok(nil == string.find(swappath1, '%.%.%.')) |     ok(nil == string.find(swappath1, '%.%.%.')) | ||||||
|     ok(nil == string.find(swappath2, '%.%.%.')) |     ok(nil == string.find(swappath2, '%.%.%.')) | ||||||
|  |   end | ||||||
|  |  | ||||||
|  |   it('with :preserve and SIGKILL', function() | ||||||
|  |     local swappath1 = setup_swapname() | ||||||
|  |     command('preserve') | ||||||
|  |     os_kill(eval('getpid()')) | ||||||
|  |     test_recover(swappath1) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|  |   it('closing stdio channel without :preserve #22096', function() | ||||||
|  |     local swappath1 = setup_swapname() | ||||||
|  |     nvim1:close() | ||||||
|  |     test_recover(swappath1) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|  |   it('killing TUI process without :preserve #22096', function() | ||||||
|  |     helpers.skip(helpers.is_os('win')) | ||||||
|  |     local screen = Screen.new() | ||||||
|  |     screen:attach() | ||||||
|  |     local child_server = new_pipename() | ||||||
|  |     funcs.termopen({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--listen', child_server}) | ||||||
|  |     screen:expect({any = pesc('[No Name]')})  -- Wait for the child process to start. | ||||||
|  |     local child_session = helpers.connect(child_server) | ||||||
|  |     set_session(child_session) | ||||||
|  |     local swappath1 = setup_swapname() | ||||||
|  |     set_session(nvim1) | ||||||
|  |     command('call chanclose(&channel)')  -- Kill the child process. | ||||||
|  |     screen:expect({any = pesc('[Process exited 1]')})  -- Wait for the child process to stop. | ||||||
|  |     test_recover(swappath1) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
| end) | end) | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ local function reset(o) | |||||||
| end | end | ||||||
|  |  | ||||||
| local clear = function() | local clear = function() | ||||||
|  |   helpers.expect_exit(helpers.command, 'qall!') | ||||||
|   os.remove(tmpname) |   os.remove(tmpname) | ||||||
| end | end | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,7 +32,6 @@ describe('ShaDa support code', function() | |||||||
|     nvim_command('rshada') |     nvim_command('rshada') | ||||||
|     eq('" Test 2', funcs.histget(':', -1)) |     eq('" Test 2', funcs.histget(':', -1)) | ||||||
|     eq('" Test', funcs.histget(':', -2)) |     eq('" Test', funcs.histget(':', -2)) | ||||||
|     expect_exit(nvim_command, 'qall') |  | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   it('respects &history when dumping', |   it('respects &history when dumping', | ||||||
|   | |||||||
| @@ -136,24 +136,6 @@ describe('ShaDa support code', function() | |||||||
|     eq(#msgpack, found) |     eq(#msgpack, found) | ||||||
|   end) |   end) | ||||||
|  |  | ||||||
|   it('does not write NONE file', function() |  | ||||||
|     local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', |  | ||||||
|                            '--headless', '--cmd', 'qall'}, true) |  | ||||||
|     session:close() |  | ||||||
|     eq(nil, lfs.attributes('NONE')) |  | ||||||
|     eq(nil, lfs.attributes('NONE.tmp.a')) |  | ||||||
|   end) |  | ||||||
|  |  | ||||||
|   it('does not read NONE file', function() |  | ||||||
|     write_file('NONE', '\005\001\015\131\161na\162rX\194\162rc\145\196\001-') |  | ||||||
|     local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', |  | ||||||
|                            '--headless'}, true) |  | ||||||
|     set_session(session) |  | ||||||
|     eq('', funcs.getreg('a')) |  | ||||||
|     session:close() |  | ||||||
|     os.remove('NONE') |  | ||||||
|   end) |  | ||||||
|  |  | ||||||
|   local marklike = {[7]=true, [8]=true, [10]=true, [11]=true} |   local marklike = {[7]=true, [8]=true, [10]=true, [11]=true} | ||||||
|   local find_file = function(fname) |   local find_file = function(fname) | ||||||
|     local found = {} |     local found = {} | ||||||
| @@ -263,3 +245,23 @@ describe('ShaDa support code', function() | |||||||
|     meths.set_option('shada', '') |     meths.set_option('shada', '') | ||||||
|   end) |   end) | ||||||
| end) | end) | ||||||
|  |  | ||||||
|  | describe('ShaDa support code', function() | ||||||
|  |   it('does not write NONE file', function() | ||||||
|  |     local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', | ||||||
|  |                            '--headless', '--cmd', 'qall'}, true) | ||||||
|  |     session:close() | ||||||
|  |     eq(nil, lfs.attributes('NONE')) | ||||||
|  |     eq(nil, lfs.attributes('NONE.tmp.a')) | ||||||
|  |   end) | ||||||
|  |  | ||||||
|  |   it('does not read NONE file', function() | ||||||
|  |     write_file('NONE', '\005\001\015\131\161na\162rX\194\162rc\145\196\001-') | ||||||
|  |     local session = spawn({nvim_prog, '-u', 'NONE', '-i', 'NONE', '--embed', | ||||||
|  |                            '--headless'}, true) | ||||||
|  |     set_session(session) | ||||||
|  |     eq('', funcs.getreg('a')) | ||||||
|  |     session:close() | ||||||
|  |     os.remove('NONE') | ||||||
|  |   end) | ||||||
|  | end) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 zeertzjq
					zeertzjq