Skip to content

Commit 7df915d

Browse files
committed
patch 8.0.0087
Problem: When the channel callback gets job info the job may already have been deleted. (lifepillar) Solution: Do not delete the job when the channel is still useful. (ichizok, closes #1242, closes #1245)
1 parent c0514bf commit 7df915d

7 files changed

Lines changed: 122 additions & 59 deletions

File tree

src/channel.c

Lines changed: 87 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4433,19 +4433,66 @@ job_free(job_T *job)
44334433
}
44344434
}
44354435

4436+
#if defined(EXITFREE) || defined(PROTO)
4437+
void
4438+
job_free_all(void)
4439+
{
4440+
while (first_job != NULL)
4441+
job_free(first_job);
4442+
}
4443+
#endif
4444+
4445+
/*
4446+
* Return TRUE if we need to check if the process of "job" has ended.
4447+
*/
4448+
static int
4449+
job_need_end_check(job_T *job)
4450+
{
4451+
return job->jv_status == JOB_STARTED
4452+
&& (job->jv_stoponexit != NULL || job->jv_exit_cb != NULL);
4453+
}
4454+
4455+
/*
4456+
* Return TRUE if the channel of "job" is still useful.
4457+
*/
4458+
static int
4459+
job_channel_still_useful(job_T *job)
4460+
{
4461+
return job->jv_channel != NULL && channel_still_useful(job->jv_channel);
4462+
}
4463+
4464+
/*
4465+
* Return TRUE if the job should not be freed yet. Do not free the job when
4466+
* it has not ended yet and there is a "stoponexit" flag, an exit callback
4467+
* or when the associated channel will do something with the job output.
4468+
*/
4469+
static int
4470+
job_still_useful(job_T *job)
4471+
{
4472+
return job_need_end_check(job) || job_channel_still_useful(job);
4473+
}
4474+
4475+
/*
4476+
* NOTE: Must call job_cleanup() only once right after the status of "job"
4477+
* changed to JOB_ENDED (i.e. after job_status() returned "dead" first or
4478+
* mch_detect_ended_job() returned non-NULL).
4479+
*/
44364480
static void
44374481
job_cleanup(job_T *job)
44384482
{
44394483
if (job->jv_status != JOB_ENDED)
44404484
return;
44414485

4486+
/* Ready to cleanup the job. */
4487+
job->jv_status = JOB_FINISHED;
4488+
44424489
if (job->jv_exit_cb != NULL)
44434490
{
44444491
typval_T argv[3];
44454492
typval_T rettv;
44464493
int dummy;
44474494

4448-
/* invoke the exit callback; make sure the refcount is > 0 */
4495+
/* Invoke the exit callback. Make sure the refcount is > 0. */
44494496
++job->jv_refcount;
44504497
argv[0].v_type = VAR_JOB;
44514498
argv[0].vval.v_job = job;
@@ -4458,42 +4505,18 @@ job_cleanup(job_T *job)
44584505
--job->jv_refcount;
44594506
channel_need_redraw = TRUE;
44604507
}
4461-
if (job->jv_refcount == 0)
4508+
4509+
/* Do not free the job in case the close callback of the associated channel
4510+
* isn't invoked yet and may get information by job_info(). */
4511+
if (job->jv_refcount == 0 && !job_channel_still_useful(job))
44624512
{
4463-
/* The job was already unreferenced, now that it ended it can be
4464-
* freed. Careful: caller must not use "job" after this! */
4513+
/* The job was already unreferenced and the associated channel was
4514+
* detached, now that it ended it can be freed. Careful: caller must
4515+
* not use "job" after this! */
44654516
job_free(job);
44664517
}
44674518
}
44684519

4469-
#if defined(EXITFREE) || defined(PROTO)
4470-
void
4471-
job_free_all(void)
4472-
{
4473-
while (first_job != NULL)
4474-
job_free(first_job);
4475-
}
4476-
#endif
4477-
4478-
/*
4479-
* Return TRUE if the job should not be freed yet. Do not free the job when
4480-
* it has not ended yet and there is a "stoponexit" flag, an exit callback
4481-
* or when the associated channel will do something with the job output.
4482-
*/
4483-
static int
4484-
job_still_useful(job_T *job)
4485-
{
4486-
return (job->jv_stoponexit != NULL || job->jv_exit_cb != NULL
4487-
|| (job->jv_channel != NULL
4488-
&& channel_still_useful(job->jv_channel)));
4489-
}
4490-
4491-
static int
4492-
job_still_alive(job_T *job)
4493-
{
4494-
return (job->jv_status == JOB_STARTED) && job_still_useful(job);
4495-
}
4496-
44974520
/*
44984521
* Mark references in jobs that are still useful.
44994522
*/
@@ -4505,7 +4528,7 @@ set_ref_in_job(int copyID)
45054528
typval_T tv;
45064529

45074530
for (job = first_job; job != NULL; job = job->jv_next)
4508-
if (job_still_alive(job))
4531+
if (job_still_useful(job))
45094532
{
45104533
tv.v_type = VAR_JOB;
45114534
tv.vval.v_job = job;
@@ -4514,26 +4537,33 @@ set_ref_in_job(int copyID)
45144537
return abort;
45154538
}
45164539

4540+
/*
4541+
* Dereference "job". Note that after this "job" may have been freed.
4542+
*/
45174543
void
45184544
job_unref(job_T *job)
45194545
{
45204546
if (job != NULL && --job->jv_refcount <= 0)
45214547
{
4522-
/* Do not free the job when it has not ended yet and there is a
4523-
* "stoponexit" flag or an exit callback. */
4524-
if (!job_still_alive(job))
4548+
/* Do not free the job if there is a channel where the close callback
4549+
* may get the job info. */
4550+
if (!job_channel_still_useful(job))
45254551
{
4526-
job_free(job);
4527-
}
4528-
else if (job->jv_channel != NULL
4529-
&& !channel_still_useful(job->jv_channel))
4530-
{
4531-
/* Do remove the link to the channel, otherwise it hangs
4532-
* around until Vim exits. See job_free() for refcount. */
4533-
ch_log(job->jv_channel, "detaching channel from job");
4534-
job->jv_channel->ch_job = NULL;
4535-
channel_unref(job->jv_channel);
4536-
job->jv_channel = NULL;
4552+
/* Do not free the job when it has not ended yet and there is a
4553+
* "stoponexit" flag or an exit callback. */
4554+
if (!job_need_end_check(job))
4555+
{
4556+
job_free(job);
4557+
}
4558+
else if (job->jv_channel != NULL)
4559+
{
4560+
/* Do remove the link to the channel, otherwise it hangs
4561+
* around until Vim exits. See job_free() for refcount. */
4562+
ch_log(job->jv_channel, "detaching channel from job");
4563+
job->jv_channel->ch_job = NULL;
4564+
channel_unref(job->jv_channel);
4565+
job->jv_channel = NULL;
4566+
}
45374567
}
45384568
}
45394569
}
@@ -4546,7 +4576,7 @@ free_unused_jobs_contents(int copyID, int mask)
45464576

45474577
for (job = first_job; job != NULL; job = job->jv_next)
45484578
if ((job->jv_copyID & mask) != (copyID & mask)
4549-
&& !job_still_alive(job))
4579+
&& !job_still_useful(job))
45504580
{
45514581
/* Free the channel and ordinary items it contains, but don't
45524582
* recurse into Lists, Dictionaries etc. */
@@ -4566,7 +4596,7 @@ free_unused_jobs(int copyID, int mask)
45664596
{
45674597
job_next = job->jv_next;
45684598
if ((job->jv_copyID & mask) != (copyID & mask)
4569-
&& !job_still_alive(job))
4599+
&& !job_still_useful(job))
45704600
{
45714601
/* Free the job struct itself. */
45724602
job_free_job(job);
@@ -4660,8 +4690,7 @@ has_pending_job(void)
46604690
/* Only should check if the channel has been closed, if the channel is
46614691
* open the job won't exit. */
46624692
if (job->jv_status == JOB_STARTED && job->jv_exit_cb != NULL
4663-
&& (job->jv_channel == NULL
4664-
|| !channel_still_useful(job->jv_channel)))
4693+
&& !job_channel_still_useful(job))
46654694
return TRUE;
46664695
return FALSE;
46674696
}
@@ -4676,14 +4705,18 @@ job_check_ended(void)
46764705
{
46774706
int i;
46784707

4708+
if (first_job == NULL)
4709+
return;
4710+
46794711
for (i = 0; i < MAX_CHECK_ENDED; ++i)
46804712
{
4713+
/* NOTE: mch_detect_ended_job() must only return a job of which the
4714+
* status was just set to JOB_ENDED. */
46814715
job_T *job = mch_detect_ended_job(first_job);
46824716

46834717
if (job == NULL)
46844718
break;
4685-
if (job_still_useful(job))
4686-
job_cleanup(job); /* may free "job" */
4719+
job_cleanup(job); /* may free "job" */
46874720
}
46884721

46894722
if (channel_need_redraw)
@@ -4897,7 +4930,7 @@ job_status(job_T *job)
48974930
{
48984931
char *result;
48994932

4900-
if (job->jv_status == JOB_ENDED)
4933+
if (job->jv_status >= JOB_ENDED)
49014934
/* No need to check, dead is dead. */
49024935
result = "dead";
49034936
else if (job->jv_status == JOB_FAILED)

src/eval.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7290,7 +7290,7 @@ get_tv_string_buf_chk(typval_T *varp, char_u *buf)
72907290
if (job == NULL)
72917291
return (char_u *)"no process";
72927292
status = job->jv_status == JOB_FAILED ? "fail"
7293-
: job->jv_status == JOB_ENDED ? "dead"
7293+
: job->jv_status >= JOB_ENDED ? "dead"
72947294
: "run";
72957295
# ifdef UNIX
72967296
vim_snprintf((char *)buf, NUMBUFLEN,

src/os_unix.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5354,7 +5354,7 @@ mch_job_status(job_T *job)
53545354
return "run";
53555355

53565356
return_dead:
5357-
if (job->jv_status != JOB_ENDED)
5357+
if (job->jv_status < JOB_ENDED)
53585358
{
53595359
ch_log(job->jv_channel, "Job ended");
53605360
job->jv_status = JOB_ENDED;
@@ -5398,7 +5398,7 @@ mch_detect_ended_job(job_T *job_list)
53985398
job->jv_exitval = WEXITSTATUS(status);
53995399
else if (WIFSIGNALED(status))
54005400
job->jv_exitval = -1;
5401-
if (job->jv_status != JOB_ENDED)
5401+
if (job->jv_status < JOB_ENDED)
54025402
{
54035403
ch_log(job->jv_channel, "Job ended");
54045404
job->jv_status = JOB_ENDED;

src/os_win32.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4978,7 +4978,7 @@ mch_job_status(job_T *job)
49784978
|| dwExitCode != STILL_ACTIVE)
49794979
{
49804980
job->jv_exitval = (int)dwExitCode;
4981-
if (job->jv_status != JOB_ENDED)
4981+
if (job->jv_status < JOB_ENDED)
49824982
{
49834983
ch_log(job->jv_channel, "Job ended");
49844984
job->jv_status = JOB_ENDED;

src/structs.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1421,11 +1421,13 @@ struct partial_S
14211421
dict_T *pt_dict; /* dict for "self" */
14221422
};
14231423

1424+
/* Status of a job. Order matters! */
14241425
typedef enum
14251426
{
14261427
JOB_FAILED,
14271428
JOB_STARTED,
1428-
JOB_ENDED
1429+
JOB_ENDED, /* detected job done */
1430+
JOB_FINISHED /* job done and cleanup done */
14291431
} jobstatus_T;
14301432

14311433
/*

src/testdir/test_channel.vim

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1232,6 +1232,32 @@ func Test_out_cb_lambda()
12321232
endtry
12331233
endfunc
12341234

1235+
func Test_close_and_exit_cb()
1236+
if !has('job')
1237+
return
1238+
endif
1239+
call ch_log('Test_close_and_exit_cb')
1240+
1241+
let dict = {'ret': {}}
1242+
func dict.close_cb(ch) dict
1243+
let self.ret['close_cb'] = job_status(ch_getjob(a:ch))
1244+
endfunc
1245+
func dict.exit_cb(job, status) dict
1246+
let self.ret['exit_cb'] = job_status(a:job)
1247+
endfunc
1248+
1249+
let g:job = job_start('echo', {
1250+
\ 'close_cb': dict.close_cb,
1251+
\ 'exit_cb': dict.exit_cb,
1252+
\ })
1253+
call assert_equal('run', job_status(g:job))
1254+
unlet g:job
1255+
call WaitFor('len(dict.ret) >= 2')
1256+
call assert_equal(2, len(dict.ret))
1257+
call assert_match('^\%(dead\|run\)', dict.ret['close_cb'])
1258+
call assert_equal('dead', dict.ret['exit_cb'])
1259+
endfunc
1260+
12351261
""""""""""
12361262

12371263
let g:Ch_unletResponse = ''

src/version.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,8 @@ static char *(features[]) =
764764

765765
static int included_patches[] =
766766
{ /* Add new patch number below this line */
767+
/**/
768+
87,
767769
/**/
768770
86,
769771
/**/

0 commit comments

Comments
 (0)