Coverage Report

Created: 2025-08-24 07:01

/src/tmux/spawn.c
Line
Count
Source (jump to first uncovered line)
1
/* $OpenBSD$ */
2
3
/*
4
 * Copyright (c) 2019 Nicholas Marriott <nicholas.marriott@gmail.com>
5
 *
6
 * Permission to use, copy, modify, and distribute this software for any
7
 * purpose with or without fee is hereby granted, provided that the above
8
 * copyright notice and this permission notice appear in all copies.
9
 *
10
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14
 * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
15
 * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
16
 * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
 */
18
19
#include <sys/types.h>
20
21
#include <errno.h>
22
#include <signal.h>
23
#include <stdlib.h>
24
#include <string.h>
25
#include <unistd.h>
26
27
#include "tmux.h"
28
29
/*
30
 * Set up the environment and create a new window and pane or a new pane.
31
 *
32
 * We need to set up the following items:
33
 *
34
 * - history limit, comes from the session;
35
 *
36
 * - base index, comes from the session;
37
 *
38
 * - current working directory, may be specified - if it isn't it comes from
39
 *   either the client or the session;
40
 *
41
 * - PATH variable, comes from the client if any, otherwise from the session
42
 *   environment;
43
 *
44
 * - shell, comes from default-shell;
45
 *
46
 * - termios, comes from the session;
47
 *
48
 * - remaining environment, comes from the session.
49
 */
50
51
static void
52
spawn_log(const char *from, struct spawn_context *sc)
53
0
{
54
0
  struct session    *s = sc->s;
55
0
  struct winlink    *wl = sc->wl;
56
0
  struct window_pane  *wp0 = sc->wp0;
57
0
  const char    *name = cmdq_get_name(sc->item);
58
0
  char       tmp[128];
59
60
0
  log_debug("%s: %s, flags=%#x", from, name, sc->flags);
61
62
0
  if (wl != NULL && wp0 != NULL)
63
0
    xsnprintf(tmp, sizeof tmp, "wl=%d wp0=%%%u", wl->idx, wp0->id);
64
0
  else if (wl != NULL)
65
0
    xsnprintf(tmp, sizeof tmp, "wl=%d wp0=none", wl->idx);
66
0
  else if (wp0 != NULL)
67
0
    xsnprintf(tmp, sizeof tmp, "wl=none wp0=%%%u", wp0->id);
68
0
  else
69
0
    xsnprintf(tmp, sizeof tmp, "wl=none wp0=none");
70
0
  log_debug("%s: s=$%u %s idx=%d", from, s->id, tmp, sc->idx);
71
0
  log_debug("%s: name=%s", from, sc->name == NULL ? "none" : sc->name);
72
0
}
73
74
struct winlink *
75
spawn_window(struct spawn_context *sc, char **cause)
76
0
{
77
0
  struct cmdq_item  *item = sc->item;
78
0
  struct client   *c = cmdq_get_client(item);
79
0
  struct session    *s = sc->s;
80
0
  struct window   *w;
81
0
  struct window_pane  *wp;
82
0
  struct winlink    *wl;
83
0
  int      idx = sc->idx;
84
0
  u_int      sx, sy, xpixel, ypixel;
85
86
0
  spawn_log(__func__, sc);
87
88
  /*
89
   * If the window already exists, we are respawning, so destroy all the
90
   * panes except one.
91
   */
92
0
  if (sc->flags & SPAWN_RESPAWN) {
93
0
    w = sc->wl->window;
94
0
    if (~sc->flags & SPAWN_KILL) {
95
0
      TAILQ_FOREACH(wp, &w->panes, entry) {
96
0
        if (wp->fd != -1)
97
0
          break;
98
0
      }
99
0
      if (wp != NULL) {
100
0
        xasprintf(cause, "window %s:%d still active",
101
0
            s->name, sc->wl->idx);
102
0
        return (NULL);
103
0
      }
104
0
    }
105
106
0
    sc->wp0 = TAILQ_FIRST(&w->panes);
107
0
    TAILQ_REMOVE(&w->panes, sc->wp0, entry);
108
109
0
    layout_free(w);
110
0
    window_destroy_panes(w);
111
112
0
    TAILQ_INSERT_HEAD(&w->panes, sc->wp0, entry);
113
0
    window_pane_resize(sc->wp0, w->sx, w->sy);
114
115
0
    layout_init(w, sc->wp0);
116
0
    w->active = NULL;
117
0
    window_set_active_pane(w, sc->wp0, 0);
118
0
  }
119
120
  /*
121
   * Otherwise we have no window so we will need to create one. First
122
   * check if the given index already exists and destroy it if so.
123
   */
124
0
  if ((~sc->flags & SPAWN_RESPAWN) && idx != -1) {
125
0
    wl = winlink_find_by_index(&s->windows, idx);
126
0
    if (wl != NULL && (~sc->flags & SPAWN_KILL)) {
127
0
      xasprintf(cause, "index %d in use", idx);
128
0
      return (NULL);
129
0
    }
130
0
    if (wl != NULL) {
131
      /*
132
       * Can't use session_detach as it will destroy session
133
       * if this makes it empty.
134
       */
135
0
      wl->flags &= ~WINLINK_ALERTFLAGS;
136
0
      notify_session_window("window-unlinked", s, wl->window);
137
0
      winlink_stack_remove(&s->lastw, wl);
138
0
      winlink_remove(&s->windows, wl);
139
140
0
      if (s->curw == wl) {
141
0
        s->curw = NULL;
142
0
        sc->flags &= ~SPAWN_DETACHED;
143
0
      }
144
0
    }
145
0
  }
146
147
  /* Then create a window if needed. */
148
0
  if (~sc->flags & SPAWN_RESPAWN) {
149
0
    if (idx == -1)
150
0
      idx = -1 - options_get_number(s->options, "base-index");
151
0
    if ((sc->wl = winlink_add(&s->windows, idx)) == NULL) {
152
0
      xasprintf(cause, "couldn't add window %d", idx);
153
0
      return (NULL);
154
0
    }
155
0
    default_window_size(sc->tc, s, NULL, &sx, &sy, &xpixel, &ypixel,
156
0
        -1);
157
0
    if ((w = window_create(sx, sy, xpixel, ypixel)) == NULL) {
158
0
      winlink_remove(&s->windows, sc->wl);
159
0
      xasprintf(cause, "couldn't create window %d", idx);
160
0
      return (NULL);
161
0
    }
162
0
    if (s->curw == NULL)
163
0
      s->curw = sc->wl;
164
0
    sc->wl->session = s;
165
0
    w->latest = sc->tc;
166
0
    winlink_set_window(sc->wl, w);
167
0
  } else
168
0
    w = NULL;
169
0
  sc->flags |= SPAWN_NONOTIFY;
170
171
  /* Spawn the pane. */
172
0
  wp = spawn_pane(sc, cause);
173
0
  if (wp == NULL) {
174
0
    if (~sc->flags & SPAWN_RESPAWN)
175
0
      winlink_remove(&s->windows, sc->wl);
176
0
    return (NULL);
177
0
  }
178
179
  /* Set the name of the new window. */
180
0
  if (~sc->flags & SPAWN_RESPAWN) {
181
0
    free(w->name);
182
0
    if (sc->name != NULL) {
183
0
      w->name = format_single(item, sc->name, c, s, NULL,
184
0
          NULL);
185
0
      options_set_number(w->options, "automatic-rename", 0);
186
0
    } else
187
0
      w->name = default_window_name(w);
188
0
  }
189
190
  /* Switch to the new window if required. */
191
0
  if (~sc->flags & SPAWN_DETACHED)
192
0
    session_select(s, sc->wl->idx);
193
194
  /* Fire notification if new window. */
195
0
  if (~sc->flags & SPAWN_RESPAWN)
196
0
    notify_session_window("window-linked", s, w);
197
198
0
  session_group_synchronize_from(s);
199
0
  return (sc->wl);
200
0
}
201
202
struct window_pane *
203
spawn_pane(struct spawn_context *sc, char **cause)
204
0
{
205
0
  struct cmdq_item   *item = sc->item;
206
0
  struct cmd_find_state  *target = cmdq_get_target(item);
207
0
  struct client    *c = cmdq_get_client(item);
208
0
  struct session     *s = sc->s;
209
0
  struct window    *w = sc->wl->window;
210
0
  struct window_pane   *new_wp;
211
0
  struct environ     *child;
212
0
  struct environ_entry   *ee;
213
0
  char      **argv, *cp, **argvp, *argv0, *cwd, *new_cwd;
214
0
  const char     *cmd, *tmp;
215
0
  int       argc;
216
0
  u_int       idx;
217
0
  struct termios      now;
218
0
  u_int       hlimit;
219
0
  struct winsize      ws;
220
0
  sigset_t      set, oldset;
221
0
  key_code      key;
222
223
0
  spawn_log(__func__, sc);
224
225
  /*
226
   * Work out the current working directory. If respawning, use
227
   * the pane's stored one unless specified.
228
   */
229
0
  if (sc->cwd != NULL) {
230
0
    cwd = format_single(item, sc->cwd, c, target->s, NULL, NULL);
231
0
    if (*cwd != '/') {
232
0
      xasprintf(&new_cwd, "%s%s%s",
233
0
          server_client_get_cwd(c, target->s),
234
0
          *cwd != '\0' ? "/" : "", cwd);
235
0
      free(cwd);
236
0
      cwd = new_cwd;
237
0
    }
238
0
  } else if (~sc->flags & SPAWN_RESPAWN)
239
0
    cwd = xstrdup(server_client_get_cwd(c, target->s));
240
0
  else
241
0
    cwd = NULL;
242
243
  /*
244
   * If we are respawning then get rid of the old process. Otherwise
245
   * either create a new cell or assign to the one we are given.
246
   */
247
0
  hlimit = options_get_number(s->options, "history-limit");
248
0
  if (sc->flags & SPAWN_RESPAWN) {
249
0
    if (sc->wp0->fd != -1 && (~sc->flags & SPAWN_KILL)) {
250
0
      window_pane_index(sc->wp0, &idx);
251
0
      xasprintf(cause, "pane %s:%d.%u still active",
252
0
          s->name, sc->wl->idx, idx);
253
0
      free(cwd);
254
0
      return (NULL);
255
0
    }
256
0
    if (sc->wp0->fd != -1) {
257
0
      bufferevent_free(sc->wp0->event);
258
0
      close(sc->wp0->fd);
259
0
    }
260
0
    window_pane_reset_mode_all(sc->wp0);
261
0
    screen_reinit(&sc->wp0->base);
262
0
    input_free(sc->wp0->ictx);
263
0
    sc->wp0->ictx = NULL;
264
0
    new_wp = sc->wp0;
265
0
    new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
266
0
  } else if (sc->lc == NULL) {
267
0
    new_wp = window_add_pane(w, NULL, hlimit, sc->flags);
268
0
    layout_init(w, new_wp);
269
0
  } else {
270
0
    new_wp = window_add_pane(w, sc->wp0, hlimit, sc->flags);
271
0
    if (sc->flags & SPAWN_ZOOM)
272
0
      layout_assign_pane(sc->lc, new_wp, 1);
273
0
    else
274
0
      layout_assign_pane(sc->lc, new_wp, 0);
275
0
  }
276
277
  /*
278
   * Now we have a pane with nothing running in it ready for the new
279
   * process. Work out the command and arguments and store the working
280
   * directory.
281
   */
282
0
  if (sc->argc == 0 && (~sc->flags & SPAWN_RESPAWN)) {
283
0
    cmd = options_get_string(s->options, "default-command");
284
0
    if (cmd != NULL && *cmd != '\0') {
285
0
      argc = 1;
286
0
      argv = (char **)&cmd;
287
0
    } else {
288
0
      argc = 0;
289
0
      argv = NULL;
290
0
    }
291
0
  } else {
292
0
    argc = sc->argc;
293
0
    argv = sc->argv;
294
0
  }
295
0
  if (cwd != NULL) {
296
0
    free(new_wp->cwd);
297
0
    new_wp->cwd = cwd;
298
0
  }
299
300
  /*
301
   * Replace the stored arguments if there are new ones. If not, the
302
   * existing ones will be used (they will only exist for respawn).
303
   */
304
0
  if (argc > 0) {
305
0
    cmd_free_argv(new_wp->argc, new_wp->argv);
306
0
    new_wp->argc = argc;
307
0
    new_wp->argv = cmd_copy_argv(argc, argv);
308
0
  }
309
310
  /* Create an environment for this pane. */
311
0
  child = environ_for_session(s, 0);
312
0
  if (sc->environ != NULL)
313
0
    environ_copy(sc->environ, child);
314
0
  environ_set(child, "TMUX_PANE", 0, "%%%u", new_wp->id);
315
316
  /*
317
   * Then the PATH environment variable. The session one is replaced from
318
   * the client if there is one because otherwise running "tmux new
319
   * myprogram" wouldn't work if myprogram isn't in the session's path.
320
   */
321
0
  if (c != NULL && c->session == NULL) { /* only unattached clients */
322
0
    ee = environ_find(c->environ, "PATH");
323
0
    if (ee != NULL)
324
0
      environ_set(child, "PATH", 0, "%s", ee->value);
325
0
  }
326
0
  if (environ_find(child, "PATH") == NULL)
327
0
    environ_set(child, "PATH", 0, "%s", _PATH_DEFPATH);
328
329
  /* Then the shell. If respawning, use the old one. */
330
0
  if (~sc->flags & SPAWN_RESPAWN) {
331
0
    tmp = options_get_string(s->options, "default-shell");
332
0
    if (!checkshell(tmp))
333
0
      tmp = _PATH_BSHELL;
334
0
    free(new_wp->shell);
335
0
    new_wp->shell = xstrdup(tmp);
336
0
  }
337
0
  environ_set(child, "SHELL", 0, "%s", new_wp->shell);
338
339
  /* Log the arguments we are going to use. */
340
0
  log_debug("%s: shell=%s", __func__, new_wp->shell);
341
0
  if (new_wp->argc != 0) {
342
0
    cp = cmd_stringify_argv(new_wp->argc, new_wp->argv);
343
0
    log_debug("%s: cmd=%s", __func__, cp);
344
0
    free(cp);
345
0
  }
346
0
  log_debug("%s: cwd=%s", __func__, new_wp->cwd);
347
0
  cmd_log_argv(new_wp->argc, new_wp->argv, "%s", __func__);
348
0
  environ_log(child, "%s: environment ", __func__);
349
350
  /* Initialize the window size. */
351
0
  memset(&ws, 0, sizeof ws);
352
0
  ws.ws_col = screen_size_x(&new_wp->base);
353
0
  ws.ws_row = screen_size_y(&new_wp->base);
354
0
  ws.ws_xpixel = w->xpixel * ws.ws_col;
355
0
  ws.ws_ypixel = w->ypixel * ws.ws_row;
356
357
  /* Block signals until fork has completed. */
358
0
  sigfillset(&set);
359
0
  sigprocmask(SIG_BLOCK, &set, &oldset);
360
361
  /* If the command is empty, don't fork a child process. */
362
0
  if (sc->flags & SPAWN_EMPTY) {
363
0
    new_wp->flags |= PANE_EMPTY;
364
0
    new_wp->base.mode &= ~MODE_CURSOR;
365
0
    new_wp->base.mode |= MODE_CRLF;
366
0
    goto complete;
367
0
  }
368
369
  /* Fork the new process. */
370
0
  new_wp->pid = fdforkpty(ptm_fd, &new_wp->fd, new_wp->tty, NULL, &ws);
371
0
  if (new_wp->pid == -1) {
372
0
    xasprintf(cause, "fork failed: %s", strerror(errno));
373
0
    new_wp->fd = -1;
374
0
    if (~sc->flags & SPAWN_RESPAWN) {
375
0
      server_client_remove_pane(new_wp);
376
0
      layout_close_pane(new_wp);
377
0
      window_remove_pane(w, new_wp);
378
0
    }
379
0
    sigprocmask(SIG_SETMASK, &oldset, NULL);
380
0
    environ_free(child);
381
0
    return (NULL);
382
0
  }
383
384
  /* In the parent process, everything is done now. */
385
0
  if (new_wp->pid != 0) {
386
0
    goto complete;
387
0
  }
388
389
#if defined(HAVE_SYSTEMD) && defined(ENABLE_CGROUPS)
390
  /*
391
   * Move the child process into a new cgroup for systemd-oomd isolation.
392
   */
393
  if (systemd_move_to_new_cgroup(cause) < 0) {
394
    log_debug("%s: moving pane to new cgroup failed: %s",
395
        __func__, *cause);
396
    free (*cause);
397
  }
398
#endif
399
  /*
400
   * Child process. Change to the working directory or home if that
401
   * fails.
402
   */
403
0
  if (chdir(new_wp->cwd) == 0)
404
0
    environ_set(child, "PWD", 0, "%s", new_wp->cwd);
405
0
  else if ((tmp = find_home()) != NULL && chdir(tmp) == 0)
406
0
    environ_set(child, "PWD", 0, "%s", tmp);
407
0
  else if (chdir("/") == 0)
408
0
    environ_set(child, "PWD", 0, "/");
409
0
  else
410
0
    fatal("chdir failed");
411
412
  /*
413
   * Update terminal escape characters from the session if available and
414
   * force VERASE to tmux's backspace.
415
   */
416
0
  if (tcgetattr(STDIN_FILENO, &now) != 0)
417
0
    _exit(1);
418
0
  if (s->tio != NULL)
419
0
    memcpy(now.c_cc, s->tio->c_cc, sizeof now.c_cc);
420
0
  key = options_get_number(global_options, "backspace");
421
0
  if (key >= 0x7f)
422
0
    now.c_cc[VERASE] = '\177';
423
0
  else
424
0
    now.c_cc[VERASE] = key;
425
0
#ifdef IUTF8
426
0
  now.c_iflag |= IUTF8;
427
0
#endif
428
0
  if (tcsetattr(STDIN_FILENO, TCSANOW, &now) != 0)
429
0
    _exit(1);
430
431
  /* Clean up file descriptors and signals and update the environment. */
432
0
  proc_clear_signals(server_proc, 1);
433
0
  closefrom(STDERR_FILENO + 1);
434
0
  sigprocmask(SIG_SETMASK, &oldset, NULL);
435
0
  log_close();
436
0
  environ_push(child);
437
438
  /*
439
   * If given multiple arguments, use execvp(). Copy the arguments to
440
   * ensure they end in a NULL.
441
   */
442
0
  if (new_wp->argc != 0 && new_wp->argc != 1) {
443
0
    argvp = cmd_copy_argv(new_wp->argc, new_wp->argv);
444
0
    execvp(argvp[0], argvp);
445
0
    _exit(1);
446
0
  }
447
448
  /*
449
   * If one argument, pass it to $SHELL -c. Otherwise create a login
450
   * shell.
451
   */
452
0
  cp = strrchr(new_wp->shell, '/');
453
0
  if (new_wp->argc == 1) {
454
0
    tmp = new_wp->argv[0];
455
0
    if (cp != NULL && cp[1] != '\0')
456
0
      xasprintf(&argv0, "%s", cp + 1);
457
0
    else
458
0
      xasprintf(&argv0, "%s", new_wp->shell);
459
0
    execl(new_wp->shell, argv0, "-c", tmp, (char *)NULL);
460
0
    _exit(1);
461
0
  }
462
0
  if (cp != NULL && cp[1] != '\0')
463
0
    xasprintf(&argv0, "-%s", cp + 1);
464
0
  else
465
0
    xasprintf(&argv0, "-%s", new_wp->shell);
466
0
  execl(new_wp->shell, argv0, (char *)NULL);
467
0
  _exit(1);
468
469
0
complete:
470
#ifdef HAVE_UTEMPTER
471
  if (~new_wp->flags & PANE_EMPTY) {
472
    xasprintf(&cp, "tmux(%lu).%%%u", (long)getpid(), new_wp->id);
473
    utempter_add_record(new_wp->fd, cp);
474
    kill(getpid(), SIGCHLD);
475
    free(cp);
476
  }
477
#endif
478
479
0
  new_wp->flags &= ~PANE_EXITED;
480
481
0
  sigprocmask(SIG_SETMASK, &oldset, NULL);
482
0
  window_pane_set_event(new_wp);
483
484
0
  environ_free(child);
485
486
0
  if (sc->flags & SPAWN_RESPAWN)
487
0
    return (new_wp);
488
0
  if ((~sc->flags & SPAWN_DETACHED) || w->active == NULL) {
489
0
    if (sc->flags & SPAWN_NONOTIFY)
490
0
      window_set_active_pane(w, new_wp, 0);
491
0
    else
492
0
      window_set_active_pane(w, new_wp, 1);
493
0
  }
494
0
  if (~sc->flags & SPAWN_NONOTIFY)
495
0
    notify_window("window-layout-changed", w);
496
0
  return (new_wp);
497
0
}