From 6737d57d8f6507bcd59446afe2c046da56917237 Mon Sep 17 00:00:00 2001 From: Petr Baudis Date: Tue, 9 Jun 2009 04:25:57 +0200 Subject: [PATCH] Initial commit - v0.1 All basic functionality is present. --- LICENSE | 21 +++++++ Makefile | 9 +++ README | 46 ++++++++++++++ screenenv.c | 197 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ screenenv.h | 1 + sessions.c | 165 ++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 439 insertions(+) create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README create mode 100644 screenenv.c create mode 100644 screenenv.h create mode 100644 sessions.c diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..151e8e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2009 Petr Baudis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6700048 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +CFLAGS=-Wall -O3 + +screenenv.so: screenenv.c sessions.c + $(CC) $(CFLAGS) $(LDFLAGS) -shared -fPIC -fvisibility=internal -o $@ $^ -ldl + +screenenv.c sessions.c: screenenv.h + +clean: + rm -f screenenv.so diff --git a/README b/README new file mode 100644 index 0000000..b2cde2f --- /dev/null +++ b/README @@ -0,0 +1,46 @@ +screenenv-0.1 - Physical session environment accessible within screen + + +screenenv is a simple gadget to be used together with GNU Screen. +It will make applications running within screen see some environment +variables from the currently active screen client, not based on the +first attached screen client. This enables applications to use +the appropriate X display, SSH agent forwarding settings, etc. + +Consider the situation: You have a screen session, originally started +on a local desktop; later, you attach it over ssh as well (with X +forwarding enabled). Now you do your happy mutt mail-reading, but +stumble over an image attachment. If you try to open it over ssh, +the picture will show up on your local display, not the ssh-forwarded +one. + +But if you preload screenenv, applications trying to use X will +automatically choose the appropriate $DISPLAY based on the session +with the least idle time! + + +Installation instructions: + + $ make # ;-) + +Now, you should test if it is not totally broken, by testing e.g. +the above scenario within your screen session, running: + + $ LD_PRELOAD=./screenenv.so display picture.png + +This should work properly when run from the either attached screen. +You do not need to restart your screens or anything else. Do not +forget to ssh in using `ssh -X`. + +Once you are reasonably confident with screenenv, you can make +all your executables use it by default by adding it (with full path) +to /etc/ld.so.preload. Applications running outside of screen will +continue to work fine. But be very careful when editing the file! +If it ends up containing invalid entries, your entire system may +become unusable; in emergency, `>/etc/ld.so.preload` within already +running shell should resolve the situation. + +Have fun! And give me feedback at pasky@suse.cz. + + +Thanks thement for inspiration for the implementation. diff --git a/screenenv.c b/screenenv.c new file mode 100644 index 0000000..20ea6a2 --- /dev/null +++ b/screenenv.c @@ -0,0 +1,197 @@ +/* The customized getenv() function itself. */ + +/* We keep a cached environment for the latest client, with only + * the interesting variables picked out. */ +/* FIXME: We should keep the old clients' environments as well, + * in case the app uses stale getenv() pointers! */ + +#define _GNU_SOURCE 1 +#include +#include +#include +#include +#include +#include + +#include "screenenv.h" + +/* Just add more if you need. */ +const static char *grabenv[] = { + "DISPLAY", + "SSH_AUTH_SOCK", + "SSH_CLIENT", + "SSH_CONNECTION", + "SSH_TTY", + "XDG_SESSION_COOKIE", + NULL +}; + + +/* The cached environment. */ + +struct env { + pid_t pid; + char *vars[sizeof(grabenv) / sizeof(*grabenv)]; + char buf_[4096]; +}; + +static struct env env0; + + +/* Parse environment in /proc/.../environ and save it in *env. */ + +int loadenv(pid_t pid, struct env *env) +{ + env->pid = pid; + + char envname[128]; + snprintf(envname, 128, "/proc/%d/environ", pid); + int fd = open(envname, O_RDONLY); + if (fd < 0) return -1; + + char *envp = env->buf_; + + char buf[8192], *bufp; + size_t bl = 0; + + /* What do we do in this read() iteration; we keep enough context + * necessary in the buffer from the previous iteration; if the + * variable is not interesting or is too long for our taste, we + * just skip to next one. */ + enum { VARNAME_LOAD, VALUE_LOAD, VAR_SKIP } state = VARNAME_LOAD; + int var; + + while (1) { + int l; +next_data: + l = read(fd, buf + bl, sizeof(buf) - bl); + if (l < 0) { + close(fd); + return -1; + } + bl += l; + if (bl <= 0) break; + bufp = buf; + + while (bl > 0) { + char *tok; + switch (state) { + case VARNAME_LOAD: + tok = memchr(bufp, '=', bl); + if (!tok) { + if (buf == bufp) { + state = VAR_SKIP; + bl = 0; + } else { + memmove(buf, bufp, bl); + } + goto next_data; + } + *tok++ = 0; + for (var = 0; grabenv[var]; var++) + if (!strcmp(bufp, grabenv[var])) + break; + bl -= tok - bufp; bufp = tok; + if (grabenv[var]) { + state = VALUE_LOAD; + } else { + state = VAR_SKIP; + } + break; + case VAR_SKIP: + tok = memchr(bufp, 0, bl); + if (!tok) { + bl = 0; + goto next_data; + } + tok++; + bl -= tok - bufp; bufp = tok; + state = VARNAME_LOAD; + break; + case VALUE_LOAD: + tok = memchr(bufp, 0, bl); + if (!tok) { + if (buf == bufp) { + state = VAR_SKIP; + bl = 0; + } else { + memmove(buf, bufp, bl); + } + goto next_data; + } + tok++; + int vallen = tok - bufp; + if (sizeof(env->buf_) - (envp - env->buf_) < vallen) { + /* Environment space full. */ + close(fd); + return -1; + } + memcpy(envp, bufp, vallen); + // printf("[%d] `%s'\n", var, envp); + env->vars[var] = envp; envp += vallen; + bl -= tok - bufp; bufp = tok; + state = VARNAME_LOAD; + break; + } + } + } + + close(fd); + return 0; +} + + +/* The getenv() wrapper itself! */ + +__attribute__((visibility("default"))) char * +getenv(const char *env) +{ + static char *(*up_getenv)(const char *); + if (__builtin_expect(!up_getenv, 0)) + up_getenv = dlsym(RTLD_NEXT, "getenv"); + + /* The fast way out, not to hog non-screen processes. */ + static int in_screen = -1; + if (__builtin_expect(in_screen < 0, 0)) + in_screen = !strcmp(up_getenv("TERM"), "screen"); + if (__builtin_expect(!in_screen, 1)) + return up_getenv(env); + + /* TERM=screen - so probably we will get client pid? */ + pid_t pid = get_client_pid(getpid()); + if (__builtin_expect(pid <= 0, 0)) + return up_getenv(env); + + /* Do we look for a hijacked variable? */ + int var; + for (var = 0; grabenv[var]; var++) + if (__builtin_expect(!strcmp(env, grabenv[var]), 0)) + break; + if (__builtin_expect(!grabenv[var], 1)) + return up_getenv(env); + + /* Should we reload the environment? */ + if (env0.pid != pid) { + memset(&env0, 0, sizeof(env0)); + if (loadenv(pid, &env0) < 0) + return up_getenv(env); + } + + return env0.vars[var]; +} + +#if 0 +void putsn(char *str) +{ + puts(str ? str : "(null)"); +} + +int main(void) +{ + putsn(getenv("USER")); + putsn(getenv("DISPLAY")); + putsn(getenv("TERM")); + putsn(getenv("XDG_SESSION_COOKIE")); + return 0; +} +#endif diff --git a/screenenv.h b/screenenv.h new file mode 100644 index 0000000..98d9b93 --- /dev/null +++ b/screenenv.h @@ -0,0 +1 @@ +pid_t get_client_pid(pid_t base_pid); diff --git a/sessions.c b/sessions.c new file mode 100644 index 0000000..092d196 --- /dev/null +++ b/sessions.c @@ -0,0 +1,165 @@ +/* Examine all the clients attached to our screen and find the one + * with the least idle time. */ + +/* Inconsistent results may appear in case of nested screens on the + * same system. */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "screenenv.h" + +struct procinfo { + pid_t pid, ppid; + char *name; + char buf_[1024]; +}; + +/* Load procinfo for given process. */ +static void get_procinfo(pid_t pid, struct procinfo *pi) +{ + pi->pid = pid; + pi->name = NULL; + + char statfname[64]; + snprintf(statfname, sizeof(statfname), "/proc/%d/stat", pid); + int fd = open(statfname, O_RDONLY); + if (fd < 0) return; + + ssize_t r = read(fd, pi->buf_, sizeof(pi->buf_) - 1); + if (r <= 0) return; + pi->buf_[r] = 0; + close(fd); + + pi->name = strchr(pi->buf_, '(') + 1; + char *nameend = strrchr(pi->buf_, ')'); + *nameend = 0; + pi->ppid = atol(nameend + 4); +} + +/* Find master screen process. */ +static pid_t get_screen_pid(pid_t base_pid) +{ + struct procinfo pi = { .ppid = base_pid }; + do { + get_procinfo(pi.ppid, &pi); + } while (pi.name && strcmp(pi.name, "screen") && pi.ppid > 0); + if (!pi.name || pi.ppid <= 0) + return -1; + return pi.pid; +} + + +/* Find pts of a process with the least idle time. */ +static dev_t get_active_pts(pid_t pid) +{ + char fddname[64]; + snprintf(fddname, sizeof(fddname), "/proc/%d/fd", pid); + DIR *fdd = opendir(fddname); + if (!fdd) return 0; + + dev_t bestpts = 0; + time_t bestatime = 0; + + struct dirent *de, *res; + de = alloca(sizeof(*de) + 4096); + while (1) { + if (readdir_r(fdd, de, &res) != 0) + return 0; + if (!res) + break; + + /* Pick files we can see and are pts devices */ + struct stat s; + if (fstatat(dirfd(fdd), de->d_name, &s, 0) != 0) + continue; + if (major(s.st_rdev) != 136) + continue; + /* and choose the one with the least idletime. */ + if (s.st_atime < bestatime) + continue; + bestpts = s.st_rdev; + bestatime = s.st_atime; + } + + closedir(fdd); + return bestatime > 0 ? bestpts : 0; +} + + +/* Scan all processes for a screen with the given pts as stdin. */ +static pid_t get_client_by_pts(dev_t pts) +{ + DIR *procd = opendir("/proc"); + if (!procd) return 0; + + struct dirent *de, *res; + de = alloca(sizeof(*de) + 4096); + while (1) { + if (readdir_r(procd, de, &res) != 0) + return 0; + if (!res) + break; + int namelen = strlen(de->d_name); + + /* Pick numerical directories */ + if (de->d_type != DT_DIR || !isdigit(de->d_name[0])) + continue; + + /* Peek at fd 0 */ + char name[namelen + 7]; + stpcpy(stpcpy(name, de->d_name), "/fd/0"); + + struct stat s; + if (fstatat(dirfd(procd), name, &s, 0) != 0) + continue; + if (s.st_rdev != pts) + continue; + + /* Is this screen? */ + pid_t pid = atol(de->d_name); + struct procinfo pi; + get_procinfo(pid, &pi); + if (pi.name == NULL || strcmp(pi.name, "screen")) + continue; + + closedir(procd); + return pid; + } + + closedir(procd); + return -1; +} + + +/* Find the client screen process watching base_pid with the least + * idle time. */ +pid_t get_client_pid(pid_t base_pid) +{ + static pid_t screen_pid; + if (screen_pid < 0) return -1; + if (screen_pid == 0) { + screen_pid = get_screen_pid(base_pid); + if (screen_pid < 0) return -1; + } + dev_t client_pts = get_active_pts(screen_pid); + if (client_pts == 0) return -1; + return get_client_by_pts(client_pts); +} + + +#if 0 +int main(void) +{ + printf("%d\n", get_client_pid(getpid())); +} +#endif -- 2.11.4.GIT