Coverage Report

Created: 2026-05-30 06:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tmux/spawn.c
Line
Count
Source
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
0
  char      *name;
86
87
0
  spawn_log(__func__, sc);
88
89
  /*
90
   * If the window already exists, we are respawning, so destroy all the
91
   * panes except one.
92
   */
93
0
  if (sc->flags & SPAWN_RESPAWN) {
94
0
    w = sc->wl->window;
95
0
    if (~sc->flags & SPAWN_KILL) {
96
0
      TAILQ_FOREACH(wp, &w->panes, entry) {
97
0
        if (wp->fd != -1)
98
0
          break;
99
0
      }
100
0
      if (wp != NULL) {
101
0
        xasprintf(cause, "window %s:%d still active",
102
0
            s->name, sc->wl->idx);
103
0
        return (NULL);
104
0
      }
105
0
    }
106
107
0
    sc->wp0 = TAILQ_FIRST(&w->panes);
108
0
    TAILQ_REMOVE(&w->panes, sc->wp0, entry);
109
110
0
    layout_free(w);
111
0
    window_destroy_panes(w);
112
113
0
    TAILQ_INSERT_HEAD(&w->panes, sc->wp0, entry);
114
0
    window_pane_resize(sc->wp0, w->sx, w->sy);
115
116
0
    layout_init(w, sc->wp0);
117
0
    w->active = NULL;
118
0
    window_set_active_pane(w, sc->wp0, 0);
119
0
  }
120
121
  /*
122
   * Otherwise we have no window so we will need to create one. First
123
   * check if the given index already exists and destroy it if so.
124
   */
125
0
  if ((~sc->flags & SPAWN_RESPAWN) && idx != -1) {
126
0
    wl = winlink_find_by_index(&s->windows, idx);
127
0
    if (wl != NULL && (~sc->flags & SPAWN_KILL)) {
128
0
      xasprintf(cause, "index %d in use", idx);
129
0
      return (NULL);
130
0
    }
131
0
    if (wl != NULL) {
132
      /*
133
       * Can't use session_detach as it will destroy session
134
       * if this makes it empty.
135
       */
136
0
      wl->flags &= ~WINLINK_ALERTFLAGS;
137
0
      notify_session_window("window-unlinked", s, wl->window);
138
0
      winlink_stack_remove(&s->lastw, wl);
139
0
      winlink_remove(&s->windows, wl);
140
141
0
      if (s->curw == wl) {
142
0
        s->curw = NULL;
143
0
        sc->flags &= ~SPAWN_DETACHED;
144
0
      }
145
0
    }
146
0
  }
147
148
  /* Then create a window if needed. */
149
0
  if (~sc->flags & SPAWN_RESPAWN) {
150
0
    if (idx == -1)
151
0
      idx = -1 - options_get_number(s->options, "base-index");
152
0
    if ((sc->wl = winlink_add(&s->windows, idx)) == NULL) {
153
0
      xasprintf(cause, "couldn't add window %d", idx);
154
0
      return (NULL);
155
0
    }
156
0
    default_window_size(sc->tc, s, NULL, &sx, &sy, &xpixel, &ypixel,
157
0
        -1);
158
0
    if ((w = window_create(sx, sy, xpixel, ypixel)) == NULL) {
159
0
      winlink_remove(&s->windows, sc->wl);
160
0
      xasprintf(cause, "couldn't create window %d", idx);
161
0
      return (NULL);
162
0
    }
163
0
    if (s->curw == NULL)
164
0
      s->curw = sc->wl;
165
0
    sc->wl->session = s;
166
0
    w->latest = sc->tc;
167
0
    winlink_set_window(sc->wl, w);
168
0
  } else
169
0
    w = NULL;
170
0
  sc->flags |= SPAWN_NONOTIFY;
171
172
  /* Spawn the pane. */
173
0
  wp = spawn_pane(sc, cause);
174
0
  if (wp == NULL) {
175
0
    if (~sc->flags & SPAWN_RESPAWN)
176
0
      winlink_remove(&s->windows, sc->wl);
177
0
    return (NULL);
178
0
  }
179
180
  /* Set the name of the new window. */
181
0
  if (~sc->flags & SPAWN_RESPAWN) {
182
0
    free(w->name);
183
0
    if (sc->name != NULL) {
184
0
      name = format_single(item, sc->name, c, s, NULL, NULL);
185
0
      w->name = clean_name(name, "#");
186
0
      free(name);
187
0
      if (w->name == NULL)
188
0
        w->name = xstrdup("");
189
0
      options_set_number(w->options, "automatic-rename", 0);
190
0
    } else
191
0
      w->name = default_window_name(w);
192
0
  }
193
194
  /* Switch to the new window if required. */
195
0
  if (~sc->flags & SPAWN_DETACHED)
196
0
    session_select(s, sc->wl->idx);
197
198
  /* Fire notification if new window. */
199
0
  if (~sc->flags & SPAWN_RESPAWN)
200
0
    notify_session_window("window-linked", s, w);
201
202
0
  session_group_synchronize_from(s);
203
0
  return (sc->wl);
204
0
}
205
206
struct window_pane *
207
spawn_pane(struct spawn_context *sc, char **cause)
208
0
{
209
0
  struct cmdq_item   *item = sc->item;
210
0
  struct cmd_find_state  *target = cmdq_get_target(item);
211
0
  struct client    *c = cmdq_get_client(item);
212
0
  struct session     *s = sc->s;
213
0
  struct window    *w = sc->wl->window;
214
0
  struct window_pane   *new_wp;
215
0
  struct environ     *child;
216
0
  struct environ_entry   *ee;
217
0
  char      **argv, *cp, **argvp, *argv0, *cwd, *new_cwd;
218
0
  char        path[PATH_MAX];
219
0
  const char     *cmd, *tmp, *home = find_home();
220
0
  const char     *actual_cwd = NULL;
221
0
  int       argc;
222
0
  u_int       idx;
223
0
  struct termios      now;
224
0
  u_int       hlimit;
225
0
  struct winsize      ws;
226
0
  sigset_t      set, oldset;
227
0
  key_code      key;
228
229
0
  spawn_log(__func__, sc);
230
231
  /*
232
   * Work out the current working directory. If respawning, use
233
   * the pane's stored one unless specified.
234
   */
235
0
  if (sc->cwd != NULL) {
236
0
    cwd = format_single(item, sc->cwd, c, target->s, NULL, NULL);
237
0
    if (*cwd != '/') {
238
0
      xasprintf(&new_cwd, "%s%s%s",
239
0
          server_client_get_cwd(c, target->s),
240
0
          *cwd != '\0' ? "/" : "", cwd);
241
0
      free(cwd);
242
0
      cwd = new_cwd;
243
0
    }
244
0
  } else if (~sc->flags & SPAWN_RESPAWN)
245
0
    cwd = xstrdup(server_client_get_cwd(c, target->s));
246
0
  else
247
0
    cwd = NULL;
248
249
  /*
250
   * If we are respawning then get rid of the old process. Otherwise
251
   * either create a new cell or assign to the one we are given.
252
   */
253
0
  hlimit = options_get_number(s->options, "history-limit");
254
0
  if (sc->flags & SPAWN_RESPAWN) {
255
0
    if (sc->wp0->fd != -1 && (~sc->flags & SPAWN_KILL)) {
256
0
      window_pane_index(sc->wp0, &idx);
257
0
      xasprintf(cause, "pane %s:%d.%u still active",
258
0
          s->name, sc->wl->idx, idx);
259
0
      free(cwd);
260
0
      return (NULL);
261
0
    }
262
0
    if (sc->wp0->fd != -1) {
263
0
      bufferevent_free(sc->wp0->event);
264
0
      close(sc->wp0->fd);
265
0
    }
266
0
    window_pane_reset_mode_all(sc->wp0);
267
0
    screen_reinit(&sc->wp0->base);
268
0
    input_free(sc->wp0->ictx);
269
0
    sc->wp0->ictx = NULL;
270
0
    new_wp = sc->wp0;
271
0
    new_wp->flags &= ~(PANE_STATUSREADY|PANE_STATUSDRAWN);
272
0
  } else if (sc->lc == NULL) {
273
0
    new_wp = window_add_pane(w, NULL, hlimit, sc->flags);
274
0
    layout_init(w, new_wp);
275
0
  } else {
276
0
    new_wp = window_add_pane(w, sc->wp0, hlimit, sc->flags);
277
0
    if (sc->flags & SPAWN_ZOOM)
278
0
      layout_assign_pane(sc->lc, new_wp, 1);
279
0
    else
280
0
      layout_assign_pane(sc->lc, new_wp, 0);
281
0
  }
282
283
  /*
284
   * If window currently zoomed, window_set_active_pane calls
285
   * window_unzoom which it copies back the saved_layout_cell.
286
   */
287
0
  if (w->flags & WINDOW_ZOOMED)
288
0
    new_wp->saved_layout_cell = new_wp->layout_cell;
289
290
  /*
291
   * Now we have a pane with nothing running in it ready for the new
292
   * process. Work out the command and arguments and store the working
293
   * directory.
294
   */
295
0
  if (sc->argc == 0 && (~sc->flags & SPAWN_RESPAWN)) {
296
0
    cmd = options_get_string(s->options, "default-command");
297
0
    if (cmd != NULL && *cmd != '\0') {
298
0
      argc = 1;
299
0
      argv = (char **)&cmd;
300
0
    } else {
301
0
      argc = 0;
302
0
      argv = NULL;
303
0
    }
304
0
  } else {
305
0
    argc = sc->argc;
306
0
    argv = sc->argv;
307
0
  }
308
0
  if (cwd != NULL) {
309
0
    free(new_wp->cwd);
310
0
    new_wp->cwd = cwd;
311
0
  }
312
313
  /*
314
   * Replace the stored arguments if there are new ones. If not, the
315
   * existing ones will be used (they will only exist for respawn).
316
   */
317
0
  if (argc > 0) {
318
0
    cmd_free_argv(new_wp->argc, new_wp->argv);
319
0
    new_wp->argc = argc;
320
0
    new_wp->argv = cmd_copy_argv(argc, argv);
321
0
  }
322
323
  /* Create an environment for this pane. */
324
0
  child = environ_for_session(s, 0);
325
0
  if (sc->environ != NULL)
326
0
    environ_copy(sc->environ, child);
327
0
  environ_set(child, "TMUX_PANE", 0, "%%%u", new_wp->id);
328
329
  /*
330
   * Then the PATH environment variable. The session one is replaced from
331
   * the client if there is one because otherwise running "tmux new
332
   * myprogram" wouldn't work if myprogram isn't in the session's path.
333
   */
334
0
  if (c != NULL && c->session == NULL) { /* only unattached clients */
335
0
    ee = environ_find(c->environ, "PATH");
336
0
    if (ee != NULL)
337
0
      environ_set(child, "PATH", 0, "%s", ee->value);
338
0
  }
339
0
  if (environ_find(child, "PATH") == NULL)
340
0
    environ_set(child, "PATH", 0, "%s", _PATH_DEFPATH);
341
342
  /* Then the shell. If respawning, use the old one. */
343
0
  if (~sc->flags & SPAWN_RESPAWN) {
344
0
    tmp = options_get_string(s->options, "default-shell");
345
0
    if (!checkshell(tmp))
346
0
      tmp = _PATH_BSHELL;
347
0
    free(new_wp->shell);
348
0
    new_wp->shell = xstrdup(tmp);
349
0
  }
350
0
  environ_set(child, "SHELL", 0, "%s", new_wp->shell);
351
352
  /* Log the arguments we are going to use. */
353
0
  log_debug("%s: shell=%s", __func__, new_wp->shell);
354
0
  if (new_wp->argc != 0) {
355
0
    cp = cmd_stringify_argv(new_wp->argc, new_wp->argv);
356
0
    log_debug("%s: cmd=%s", __func__, cp);
357
0
    free(cp);
358
0
  }
359
0
  log_debug("%s: cwd=%s", __func__, new_wp->cwd);
360
0
  cmd_log_argv(new_wp->argc, new_wp->argv, "%s", __func__);
361
0
  environ_log(child, "%s: environment ", __func__);
362
363
  /* Initialize the window size. */
364
0
  memset(&ws, 0, sizeof ws);
365
0
  ws.ws_col = screen_size_x(&new_wp->base);
366
0
  ws.ws_row = screen_size_y(&new_wp->base);
367
0
  ws.ws_xpixel = w->xpixel * ws.ws_col;
368
0
  ws.ws_ypixel = w->ypixel * ws.ws_row;
369
370
  /* Block signals until fork has completed. */
371
0
  sigfillset(&set);
372
0
  sigprocmask(SIG_BLOCK, &set, &oldset);
373
374
  /* If the command is empty, don't fork a child process. */
375
0
  if (sc->flags & SPAWN_EMPTY) {
376
0
    new_wp->flags |= PANE_EMPTY;
377
0
    new_wp->base.mode &= ~MODE_CURSOR;
378
0
    new_wp->base.mode |= MODE_CRLF;
379
0
    goto complete;
380
0
  }
381
382
  /* Store current working directory and change to new one. */
383
0
  if (getcwd(path, sizeof path) != NULL) {
384
0
    if (chdir(new_wp->cwd) == 0)
385
0
      actual_cwd = new_wp->cwd;
386
0
    else if (home != NULL && chdir(home) == 0)
387
0
      actual_cwd = home;
388
0
    else if (chdir("/") == 0)
389
0
      actual_cwd = "/";
390
0
  }
391
392
  /* Fork the new process. */
393
0
  new_wp->pid = fdforkpty(ptm_fd, &new_wp->fd, new_wp->tty, NULL, &ws);
394
0
  if (new_wp->pid == -1) {
395
0
    xasprintf(cause, "fork failed: %s", strerror(errno));
396
0
    new_wp->fd = -1;
397
0
    if (~sc->flags & SPAWN_RESPAWN) {
398
0
      server_client_remove_pane(new_wp);
399
0
      layout_close_pane(new_wp);
400
0
      window_remove_pane(w, new_wp);
401
0
    }
402
0
    sigprocmask(SIG_SETMASK, &oldset, NULL);
403
0
    environ_free(child);
404
0
    return (NULL);
405
0
  }
406
407
  /*
408
   * In the parent process, everything is done now. Change the working
409
   * directory back.
410
   */
411
0
  if (new_wp->pid != 0) {
412
0
    if (actual_cwd != NULL &&
413
0
        chdir(path) != 0 &&
414
0
        (home == NULL || chdir(home) != 0))
415
0
      chdir("/");
416
0
    goto complete;
417
0
  }
418
419
#if defined(HAVE_SYSTEMD) && defined(ENABLE_CGROUPS)
420
  /*
421
   * Move the child process into a new cgroup for systemd-oomd isolation.
422
   */
423
  if (systemd_move_to_new_cgroup(cause) < 0) {
424
    log_debug("%s: moving pane to new cgroup failed: %s",
425
        __func__, *cause);
426
    free (*cause);
427
  }
428
#endif
429
  /*
430
   * Child process. Set PWD to the working directory.
431
   */
432
0
  if (actual_cwd != NULL)
433
0
    environ_set(child, "PWD", 0, "%s", actual_cwd);
434
435
  /*
436
   * Update terminal escape characters from the session if available and
437
   * force VERASE to tmux's backspace.
438
   */
439
0
  if (tcgetattr(STDIN_FILENO, &now) != 0)
440
0
    _exit(1);
441
0
  if (s->tio != NULL)
442
0
    memcpy(now.c_cc, s->tio->c_cc, sizeof now.c_cc);
443
0
  key = options_get_number(global_options, "backspace");
444
0
  if (key >= 0x7f)
445
0
    now.c_cc[VERASE] = '\177';
446
0
  else
447
0
    now.c_cc[VERASE] = key;
448
0
#ifdef IUTF8
449
0
  now.c_iflag |= IUTF8;
450
0
#endif
451
0
  if (tcsetattr(STDIN_FILENO, TCSANOW, &now) != 0)
452
0
    _exit(1);
453
454
  /* Clean up file descriptors and signals and update the environment. */
455
0
  proc_clear_signals(server_proc, 1);
456
0
  closefrom(STDERR_FILENO + 1);
457
0
  sigprocmask(SIG_SETMASK, &oldset, NULL);
458
0
  log_close();
459
0
  environ_push(child);
460
461
  /*
462
   * If given multiple arguments, use execvp(). Copy the arguments to
463
   * ensure they end in a NULL.
464
   */
465
0
  if (new_wp->argc != 0 && new_wp->argc != 1) {
466
0
    argvp = cmd_copy_argv(new_wp->argc, new_wp->argv);
467
0
    execvp(argvp[0], argvp);
468
0
    _exit(1);
469
0
  }
470
471
  /*
472
   * If one argument, pass it to $SHELL -c. Otherwise create a login
473
   * shell.
474
   */
475
0
  cp = strrchr(new_wp->shell, '/');
476
0
  if (new_wp->argc == 1) {
477
0
    tmp = new_wp->argv[0];
478
0
    if (cp != NULL && cp[1] != '\0')
479
0
      xasprintf(&argv0, "%s", cp + 1);
480
0
    else
481
0
      xasprintf(&argv0, "%s", new_wp->shell);
482
0
    execl(new_wp->shell, argv0, "-c", tmp, (char *)NULL);
483
0
    _exit(1);
484
0
  }
485
0
  if (cp != NULL && cp[1] != '\0')
486
0
    xasprintf(&argv0, "-%s", cp + 1);
487
0
  else
488
0
    xasprintf(&argv0, "-%s", new_wp->shell);
489
0
  execl(new_wp->shell, argv0, (char *)NULL);
490
0
  _exit(1);
491
492
0
complete:
493
#ifdef HAVE_UTEMPTER
494
  if (~new_wp->flags & PANE_EMPTY) {
495
    xasprintf(&cp, "tmux(%lu).%%%u", (long)getpid(), new_wp->id);
496
    utempter_add_record(new_wp->fd, cp);
497
    kill(getpid(), SIGCHLD);
498
    free(cp);
499
  }
500
#endif
501
502
0
  new_wp->flags &= ~PANE_EXITED;
503
504
0
  sigprocmask(SIG_SETMASK, &oldset, NULL);
505
0
  window_pane_set_event(new_wp);
506
507
0
  environ_free(child);
508
509
0
  if (sc->flags & SPAWN_RESPAWN)
510
0
    return (new_wp);
511
0
  if ((~sc->flags & SPAWN_DETACHED) || w->active == NULL) {
512
0
    if (sc->flags & SPAWN_NONOTIFY)
513
0
      window_set_active_pane(w, new_wp, 0);
514
0
    else
515
0
      window_set_active_pane(w, new_wp, 1);
516
0
  }
517
0
  if (~sc->flags & SPAWN_NONOTIFY)
518
0
    notify_window("window-layout-changed", w);
519
0
  return (new_wp);
520
0
}