File: | src/usr.bin/tmux/spawn.c |
Warning: | line 182, column 8 Access to field 'name' results in a dereference of a null pointer (loaded from variable 'w') |
Press '?' to see keyboard shortcuts
Keyboard shortcuts:
1 | /* $OpenBSD: spawn.c,v 1.29 2021/08/23 11:04:21 nicm Exp $ */ | |||
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(*__errno()).h> | |||
22 | #include <paths.h> | |||
23 | #include <signal.h> | |||
24 | #include <stdlib.h> | |||
25 | #include <string.h> | |||
26 | #include <unistd.h> | |||
27 | #include <util.h> | |||
28 | ||||
29 | #include "tmux.h" | |||
30 | ||||
31 | /* | |||
32 | * Set up the environment and create a new window and pane or a new pane. | |||
33 | * | |||
34 | * We need to set up the following items: | |||
35 | * | |||
36 | * - history limit, comes from the session; | |||
37 | * | |||
38 | * - base index, comes from the session; | |||
39 | * | |||
40 | * - current working directory, may be specified - if it isn't it comes from | |||
41 | * either the client or the session; | |||
42 | * | |||
43 | * - PATH variable, comes from the client if any, otherwise from the session | |||
44 | * environment; | |||
45 | * | |||
46 | * - shell, comes from default-shell; | |||
47 | * | |||
48 | * - termios, comes from the session; | |||
49 | * | |||
50 | * - remaining environment, comes from the session. | |||
51 | */ | |||
52 | ||||
53 | static void | |||
54 | spawn_log(const char *from, struct spawn_context *sc) | |||
55 | { | |||
56 | struct session *s = sc->s; | |||
57 | struct winlink *wl = sc->wl; | |||
58 | struct window_pane *wp0 = sc->wp0; | |||
59 | const char *name = cmdq_get_name(sc->item); | |||
60 | char tmp[128]; | |||
61 | ||||
62 | log_debug("%s: %s, flags=%#x", from, name, sc->flags); | |||
63 | ||||
64 | if (wl != NULL((void *)0) && wp0 != NULL((void *)0)) | |||
65 | xsnprintf(tmp, sizeof tmp, "wl=%d wp0=%%%u", wl->idx, wp0->id); | |||
66 | else if (wl != NULL((void *)0)) | |||
67 | xsnprintf(tmp, sizeof tmp, "wl=%d wp0=none", wl->idx); | |||
68 | else if (wp0 != NULL((void *)0)) | |||
69 | xsnprintf(tmp, sizeof tmp, "wl=none wp0=%%%u", wp0->id); | |||
70 | else | |||
71 | xsnprintf(tmp, sizeof tmp, "wl=none wp0=none"); | |||
72 | log_debug("%s: s=$%u %s idx=%d", from, s->id, tmp, sc->idx); | |||
73 | log_debug("%s: name=%s", from, sc->name == NULL((void *)0) ? "none" : sc->name); | |||
74 | } | |||
75 | ||||
76 | struct winlink * | |||
77 | spawn_window(struct spawn_context *sc, char **cause) | |||
78 | { | |||
79 | struct cmdq_item *item = sc->item; | |||
80 | struct client *c = cmdq_get_client(item); | |||
81 | struct session *s = sc->s; | |||
82 | struct window *w; | |||
83 | struct window_pane *wp; | |||
84 | struct winlink *wl; | |||
85 | int idx = sc->idx; | |||
86 | u_int sx, sy, xpixel, ypixel; | |||
87 | ||||
88 | spawn_log(__func__, sc); | |||
89 | ||||
90 | /* | |||
91 | * If the window already exists, we are respawning, so destroy all the | |||
92 | * panes except one. | |||
93 | */ | |||
94 | if (sc->flags & SPAWN_RESPAWN0x4) { | |||
| ||||
95 | w = sc->wl->window; | |||
96 | if (~sc->flags & SPAWN_KILL0x1) { | |||
97 | TAILQ_FOREACH(wp, &w->panes, entry)for((wp) = ((&w->panes)->tqh_first); (wp) != ((void *)0); (wp) = ((wp)->entry.tqe_next)) { | |||
98 | if (wp->fd != -1) | |||
99 | break; | |||
100 | } | |||
101 | if (wp != NULL((void *)0)) { | |||
102 | xasprintf(cause, "window %s:%d still active", | |||
103 | s->name, sc->wl->idx); | |||
104 | return (NULL((void *)0)); | |||
105 | } | |||
106 | } | |||
107 | ||||
108 | sc->wp0 = TAILQ_FIRST(&w->panes)((&w->panes)->tqh_first); | |||
109 | TAILQ_REMOVE(&w->panes, sc->wp0, entry)do { if (((sc->wp0)->entry.tqe_next) != ((void *)0)) (sc ->wp0)->entry.tqe_next->entry.tqe_prev = (sc->wp0 )->entry.tqe_prev; else (&w->panes)->tqh_last = ( sc->wp0)->entry.tqe_prev; *(sc->wp0)->entry.tqe_prev = (sc->wp0)->entry.tqe_next; ; ; } while (0); | |||
110 | ||||
111 | layout_free(w); | |||
112 | window_destroy_panes(w); | |||
113 | ||||
114 | TAILQ_INSERT_HEAD(&w->panes, sc->wp0, entry)do { if (((sc->wp0)->entry.tqe_next = (&w->panes )->tqh_first) != ((void *)0)) (&w->panes)->tqh_first ->entry.tqe_prev = &(sc->wp0)->entry.tqe_next; else (&w->panes)->tqh_last = &(sc->wp0)->entry .tqe_next; (&w->panes)->tqh_first = (sc->wp0); ( sc->wp0)->entry.tqe_prev = &(&w->panes)-> tqh_first; } while (0); | |||
115 | window_pane_resize(sc->wp0, w->sx, w->sy); | |||
116 | ||||
117 | layout_init(w, sc->wp0); | |||
118 | window_set_active_pane(w, sc->wp0, 0); | |||
119 | } | |||
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 | if ((~sc->flags & SPAWN_RESPAWN0x4) && idx != -1) { | |||
126 | wl = winlink_find_by_index(&s->windows, idx); | |||
127 | if (wl != NULL((void *)0) && (~sc->flags & SPAWN_KILL0x1)) { | |||
128 | xasprintf(cause, "index %d in use", idx); | |||
129 | return (NULL((void *)0)); | |||
130 | } | |||
131 | if (wl != NULL((void *)0)) { | |||
132 | /* | |||
133 | * Can't use session_detach as it will destroy session | |||
134 | * if this makes it empty. | |||
135 | */ | |||
136 | wl->flags &= ~WINLINK_ALERTFLAGS(0x1|0x2|0x4); | |||
137 | notify_session_window("window-unlinked", s, wl->window); | |||
138 | winlink_stack_remove(&s->lastw, wl); | |||
139 | winlink_remove(&s->windows, wl); | |||
140 | ||||
141 | if (s->curw == wl) { | |||
142 | s->curw = NULL((void *)0); | |||
143 | sc->flags &= ~SPAWN_DETACHED0x2; | |||
144 | } | |||
145 | } | |||
146 | } | |||
147 | ||||
148 | /* Then create a window if needed. */ | |||
149 | if (~sc->flags & SPAWN_RESPAWN0x4) { | |||
150 | if (idx == -1) | |||
151 | idx = -1 - options_get_number(s->options, "base-index"); | |||
152 | if ((sc->wl = winlink_add(&s->windows, idx)) == NULL((void *)0)) { | |||
153 | xasprintf(cause, "couldn't add window %d", idx); | |||
154 | return (NULL((void *)0)); | |||
155 | } | |||
156 | default_window_size(sc->tc, s, NULL((void *)0), &sx, &sy, &xpixel, &ypixel, | |||
157 | -1); | |||
158 | if ((w = window_create(sx, sy, xpixel, ypixel)) == NULL((void *)0)) { | |||
159 | winlink_remove(&s->windows, sc->wl); | |||
160 | xasprintf(cause, "couldn't create window %d", idx); | |||
161 | return (NULL((void *)0)); | |||
162 | } | |||
163 | if (s->curw == NULL((void *)0)) | |||
164 | s->curw = sc->wl; | |||
165 | sc->wl->session = s; | |||
166 | w->latest = sc->tc; | |||
167 | winlink_set_window(sc->wl, w); | |||
168 | } else | |||
169 | w = NULL((void *)0); | |||
170 | sc->flags |= SPAWN_NONOTIFY0x10; | |||
171 | ||||
172 | /* Spawn the pane. */ | |||
173 | wp = spawn_pane(sc, cause); | |||
174 | if (wp == NULL((void *)0)) { | |||
175 | if (~sc->flags & SPAWN_RESPAWN0x4) | |||
176 | winlink_remove(&s->windows, sc->wl); | |||
177 | return (NULL((void *)0)); | |||
178 | } | |||
179 | ||||
180 | /* Set the name of the new window. */ | |||
181 | if (~sc->flags & SPAWN_RESPAWN0x4) { | |||
182 | free(w->name); | |||
| ||||
183 | if (sc->name != NULL((void *)0)) { | |||
184 | w->name = format_single(item, sc->name, c, s, NULL((void *)0), | |||
185 | NULL((void *)0)); | |||
186 | options_set_number(w->options, "automatic-rename", 0); | |||
187 | } else | |||
188 | w->name = default_window_name(w); | |||
189 | } | |||
190 | ||||
191 | /* Switch to the new window if required. */ | |||
192 | if (~sc->flags & SPAWN_DETACHED0x2) | |||
193 | session_select(s, sc->wl->idx); | |||
194 | ||||
195 | /* Fire notification if new window. */ | |||
196 | if (~sc->flags & SPAWN_RESPAWN0x4) | |||
197 | notify_session_window("window-linked", s, w); | |||
198 | ||||
199 | session_group_synchronize_from(s); | |||
200 | return (sc->wl); | |||
201 | } | |||
202 | ||||
203 | struct window_pane * | |||
204 | spawn_pane(struct spawn_context *sc, char **cause) | |||
205 | { | |||
206 | struct cmdq_item *item = sc->item; | |||
207 | struct cmd_find_state *target = cmdq_get_target(item); | |||
208 | struct client *c = cmdq_get_client(item); | |||
209 | struct session *s = sc->s; | |||
210 | struct window *w = sc->wl->window; | |||
211 | struct window_pane *new_wp; | |||
212 | struct environ *child; | |||
213 | struct environ_entry *ee; | |||
214 | char **argv, *cp, **argvp, *argv0, *cwd; | |||
215 | const char *cmd, *tmp; | |||
216 | int argc; | |||
217 | u_int idx; | |||
218 | struct termios now; | |||
219 | u_int hlimit; | |||
220 | struct winsize ws; | |||
221 | sigset_t set, oldset; | |||
222 | key_code key; | |||
223 | ||||
224 | spawn_log(__func__, sc); | |||
225 | ||||
226 | /* | |||
227 | * Work out the current working directory. If respawning, use | |||
228 | * the pane's stored one unless specified. | |||
229 | */ | |||
230 | if (sc->cwd != NULL((void *)0)) | |||
231 | cwd = format_single(item, sc->cwd, c, target->s, NULL((void *)0), NULL((void *)0)); | |||
232 | else if (~sc->flags & SPAWN_RESPAWN0x4) | |||
233 | cwd = xstrdup(server_client_get_cwd(c, target->s)); | |||
234 | else | |||
235 | cwd = NULL((void *)0); | |||
236 | ||||
237 | /* | |||
238 | * If we are respawning then get rid of the old process. Otherwise | |||
239 | * either create a new cell or assign to the one we are given. | |||
240 | */ | |||
241 | hlimit = options_get_number(s->options, "history-limit"); | |||
242 | if (sc->flags & SPAWN_RESPAWN0x4) { | |||
243 | if (sc->wp0->fd != -1 && (~sc->flags & SPAWN_KILL0x1)) { | |||
244 | window_pane_index(sc->wp0, &idx); | |||
245 | xasprintf(cause, "pane %s:%d.%u still active", | |||
246 | s->name, sc->wl->idx, idx); | |||
247 | free(cwd); | |||
248 | return (NULL((void *)0)); | |||
249 | } | |||
250 | if (sc->wp0->fd != -1) { | |||
251 | bufferevent_free(sc->wp0->event); | |||
252 | close(sc->wp0->fd); | |||
253 | } | |||
254 | window_pane_reset_mode_all(sc->wp0); | |||
255 | screen_reinit(&sc->wp0->base); | |||
256 | input_free(sc->wp0->ictx); | |||
257 | sc->wp0->ictx = NULL((void *)0); | |||
258 | new_wp = sc->wp0; | |||
259 | new_wp->flags &= ~(PANE_STATUSREADY0x200|PANE_STATUSDRAWN0x400); | |||
260 | } else if (sc->lc == NULL((void *)0)) { | |||
261 | new_wp = window_add_pane(w, NULL((void *)0), hlimit, sc->flags); | |||
262 | layout_init(w, new_wp); | |||
263 | } else { | |||
264 | new_wp = window_add_pane(w, sc->wp0, hlimit, sc->flags); | |||
265 | if (sc->flags & SPAWN_ZOOM0x80) | |||
266 | layout_assign_pane(sc->lc, new_wp, 1); | |||
267 | else | |||
268 | layout_assign_pane(sc->lc, new_wp, 0); | |||
269 | } | |||
270 | ||||
271 | /* | |||
272 | * Now we have a pane with nothing running in it ready for the new | |||
273 | * process. Work out the command and arguments and store the working | |||
274 | * directory. | |||
275 | */ | |||
276 | if (sc->argc == 0 && (~sc->flags & SPAWN_RESPAWN0x4)) { | |||
277 | cmd = options_get_string(s->options, "default-command"); | |||
278 | if (cmd != NULL((void *)0) && *cmd != '\0') { | |||
279 | argc = 1; | |||
280 | argv = (char **)&cmd; | |||
281 | } else { | |||
282 | argc = 0; | |||
283 | argv = NULL((void *)0); | |||
284 | } | |||
285 | } else { | |||
286 | argc = sc->argc; | |||
287 | argv = sc->argv; | |||
288 | } | |||
289 | if (cwd != NULL((void *)0)) { | |||
290 | free(new_wp->cwd); | |||
291 | new_wp->cwd = cwd; | |||
292 | } | |||
293 | ||||
294 | /* | |||
295 | * Replace the stored arguments if there are new ones. If not, the | |||
296 | * existing ones will be used (they will only exist for respawn). | |||
297 | */ | |||
298 | if (argc > 0) { | |||
299 | cmd_free_argv(new_wp->argc, new_wp->argv); | |||
300 | new_wp->argc = argc; | |||
301 | new_wp->argv = cmd_copy_argv(argc, argv); | |||
302 | } | |||
303 | ||||
304 | /* Create an environment for this pane. */ | |||
305 | child = environ_for_session(s, 0); | |||
306 | if (sc->environ != NULL((void *)0)) | |||
307 | environ_copy(sc->environ, child); | |||
308 | environ_set(child, "TMUX_PANE", 0, "%%%u", new_wp->id); | |||
309 | ||||
310 | /* | |||
311 | * Then the PATH environment variable. The session one is replaced from | |||
312 | * the client if there is one because otherwise running "tmux new | |||
313 | * myprogram" wouldn't work if myprogram isn't in the session's path. | |||
314 | */ | |||
315 | if (c != NULL((void *)0) && c->session == NULL((void *)0)) { /* only unattached clients */ | |||
316 | ee = environ_find(c->environ, "PATH"); | |||
317 | if (ee != NULL((void *)0)) | |||
318 | environ_set(child, "PATH", 0, "%s", ee->value); | |||
319 | } | |||
320 | if (environ_find(child, "PATH") == NULL((void *)0)) | |||
321 | environ_set(child, "PATH", 0, "%s", _PATH_DEFPATH"/usr/bin:/bin:/usr/sbin:/sbin:/usr/X11R6/bin:/usr/local/bin:/usr/local/sbin"); | |||
322 | ||||
323 | /* Then the shell. If respawning, use the old one. */ | |||
324 | if (~sc->flags & SPAWN_RESPAWN0x4) { | |||
325 | tmp = options_get_string(s->options, "default-shell"); | |||
326 | if (!checkshell(tmp)) | |||
327 | tmp = _PATH_BSHELL"/bin/sh"; | |||
328 | free(new_wp->shell); | |||
329 | new_wp->shell = xstrdup(tmp); | |||
330 | } | |||
331 | environ_set(child, "SHELL", 0, "%s", new_wp->shell); | |||
332 | ||||
333 | /* Log the arguments we are going to use. */ | |||
334 | log_debug("%s: shell=%s", __func__, new_wp->shell); | |||
335 | if (new_wp->argc != 0) { | |||
336 | cp = cmd_stringify_argv(new_wp->argc, new_wp->argv); | |||
337 | log_debug("%s: cmd=%s", __func__, cp); | |||
338 | free(cp); | |||
339 | } | |||
340 | if (cwd != NULL((void *)0)) | |||
341 | log_debug("%s: cwd=%s", __func__, cwd); | |||
342 | cmd_log_argv(new_wp->argc, new_wp->argv, "%s", __func__); | |||
343 | environ_log(child, "%s: environment ", __func__); | |||
344 | ||||
345 | /* Initialize the window size. */ | |||
346 | memset(&ws, 0, sizeof ws); | |||
347 | ws.ws_col = screen_size_x(&new_wp->base)((&new_wp->base)->grid->sx); | |||
348 | ws.ws_row = screen_size_y(&new_wp->base)((&new_wp->base)->grid->sy); | |||
349 | ws.ws_xpixel = w->xpixel * ws.ws_col; | |||
350 | ws.ws_ypixel = w->ypixel * ws.ws_row; | |||
351 | ||||
352 | /* Block signals until fork has completed. */ | |||
353 | sigfillset(&set); | |||
354 | sigprocmask(SIG_BLOCK1, &set, &oldset); | |||
355 | ||||
356 | /* If the command is empty, don't fork a child process. */ | |||
357 | if (sc->flags & SPAWN_EMPTY0x40) { | |||
358 | new_wp->flags |= PANE_EMPTY0x800; | |||
359 | new_wp->base.mode &= ~MODE_CURSOR0x1; | |||
360 | new_wp->base.mode |= MODE_CRLF0x4000; | |||
361 | goto complete; | |||
362 | } | |||
363 | ||||
364 | /* Fork the new process. */ | |||
365 | new_wp->pid = fdforkpty(ptm_fd, &new_wp->fd, new_wp->tty, NULL((void *)0), &ws); | |||
366 | if (new_wp->pid == -1) { | |||
367 | xasprintf(cause, "fork failed: %s", strerror(errno(*__errno()))); | |||
368 | new_wp->fd = -1; | |||
369 | if (~sc->flags & SPAWN_RESPAWN0x4) { | |||
370 | server_client_remove_pane(new_wp); | |||
371 | layout_close_pane(new_wp); | |||
372 | window_remove_pane(w, new_wp); | |||
373 | } | |||
374 | sigprocmask(SIG_SETMASK3, &oldset, NULL((void *)0)); | |||
375 | environ_free(child); | |||
376 | return (NULL((void *)0)); | |||
377 | } | |||
378 | ||||
379 | /* In the parent process, everything is done now. */ | |||
380 | if (new_wp->pid != 0) | |||
381 | goto complete; | |||
382 | ||||
383 | /* | |||
384 | * Child process. Change to the working directory or home if that | |||
385 | * fails. | |||
386 | */ | |||
387 | if (chdir(new_wp->cwd) != 0 && | |||
388 | ((tmp = find_home()) == NULL((void *)0) || chdir(tmp) != 0) && | |||
389 | chdir("/") != 0) | |||
390 | fatal("chdir failed"); | |||
391 | ||||
392 | /* | |||
393 | * Update terminal escape characters from the session if available and | |||
394 | * force VERASE to tmux's backspace. | |||
395 | */ | |||
396 | if (tcgetattr(STDIN_FILENO0, &now) != 0) | |||
397 | _exit(1); | |||
398 | if (s->tio != NULL((void *)0)) | |||
399 | memcpy(now.c_cc, s->tio->c_cc, sizeof now.c_cc); | |||
400 | key = options_get_number(global_options, "backspace"); | |||
401 | if (key >= 0x7f) | |||
402 | now.c_cc[VERASE3] = '\177'; | |||
403 | else | |||
404 | now.c_cc[VERASE3] = key; | |||
405 | if (tcsetattr(STDIN_FILENO0, TCSANOW0, &now) != 0) | |||
406 | _exit(1); | |||
407 | ||||
408 | /* Clean up file descriptors and signals and update the environment. */ | |||
409 | closefrom(STDERR_FILENO2 + 1); | |||
410 | proc_clear_signals(server_proc, 1); | |||
411 | sigprocmask(SIG_SETMASK3, &oldset, NULL((void *)0)); | |||
412 | log_close(); | |||
413 | environ_push(child); | |||
414 | ||||
415 | /* | |||
416 | * If given multiple arguments, use execvp(). Copy the arguments to | |||
417 | * ensure they end in a NULL. | |||
418 | */ | |||
419 | if (new_wp->argc != 0 && new_wp->argc != 1) { | |||
420 | argvp = cmd_copy_argv(new_wp->argc, new_wp->argv); | |||
421 | execvp(argvp[0], argvp); | |||
422 | _exit(1); | |||
423 | } | |||
424 | ||||
425 | /* | |||
426 | * If one argument, pass it to $SHELL -c. Otherwise create a login | |||
427 | * shell. | |||
428 | */ | |||
429 | cp = strrchr(new_wp->shell, '/'); | |||
430 | if (new_wp->argc == 1) { | |||
431 | tmp = new_wp->argv[0]; | |||
432 | if (cp != NULL((void *)0) && cp[1] != '\0') | |||
433 | xasprintf(&argv0, "%s", cp + 1); | |||
434 | else | |||
435 | xasprintf(&argv0, "%s", new_wp->shell); | |||
436 | execl(new_wp->shell, argv0, "-c", tmp, (char *)NULL((void *)0)); | |||
437 | _exit(1); | |||
438 | } | |||
439 | if (cp != NULL((void *)0) && cp[1] != '\0') | |||
440 | xasprintf(&argv0, "-%s", cp + 1); | |||
441 | else | |||
442 | xasprintf(&argv0, "-%s", new_wp->shell); | |||
443 | execl(new_wp->shell, argv0, (char *)NULL((void *)0)); | |||
444 | _exit(1); | |||
445 | ||||
446 | complete: | |||
447 | new_wp->flags &= ~PANE_EXITED0x100; | |||
448 | ||||
449 | sigprocmask(SIG_SETMASK3, &oldset, NULL((void *)0)); | |||
450 | window_pane_set_event(new_wp); | |||
451 | ||||
452 | environ_free(child); | |||
453 | ||||
454 | if (sc->flags & SPAWN_RESPAWN0x4) | |||
455 | return (new_wp); | |||
456 | if ((~sc->flags & SPAWN_DETACHED0x2) || w->active == NULL((void *)0)) { | |||
457 | if (sc->flags & SPAWN_NONOTIFY0x10) | |||
458 | window_set_active_pane(w, new_wp, 0); | |||
459 | else | |||
460 | window_set_active_pane(w, new_wp, 1); | |||
461 | } | |||
462 | if (~sc->flags & SPAWN_NONOTIFY0x10) | |||
463 | notify_window("window-layout-changed", w); | |||
464 | return (new_wp); | |||
465 | } |