]> asedeno.scripts.mit.edu Git - PuTTY.git/commitdiff
New program 'osxlaunch', to use as an OS X bundle launcher.
authorSimon Tatham <anakin@pobox.com>
Wed, 23 Mar 2016 22:13:30 +0000 (22:13 +0000)
committerSimon Tatham <anakin@pobox.com>
Wed, 23 Mar 2016 22:22:48 +0000 (22:22 +0000)
The big problem with making an OS X application out of a GTK program
is that it won't start unless DYLD_LIBRARY_PATH and several other
environment variables point at all the GTK machinery. So your app
bundle has to contain two programs: a launcher to set up that
environment, and then the real main program that the launcher execs
once it's done so.

But in our case, we also need pterm to start subprocesses _without_
all that stuff in the environment - so our launcher has to be more
complicated than the usual one, because it's also got to save every
detail of how the environment was when it started up. So this is the
launcher program I'm going to use. Comments in the header explain in
more detail how it'll work.

Also in this commit, I add the other end of the same machinery to
gtkapp.c and uxpty.c: the former catches an extra command-line
argument that the launcher used to indicate how it had munged the
environment, and stores it in a global variable where the latter can
pick it up after fork() and use to actually undo the munging.

.gitignore
Recipe
unix/gtkapp.c
unix/osxlaunch.c [new file with mode: 0644]
unix/uxpty.c
unix/uxputty.c

index 7ba79838fe7cfbc84e405c752d6aeacf60298b49..39501cf78a89297a0d0a09f80f7584071bacf321 100644 (file)
@@ -37,6 +37,7 @@
 /pterm
 /puttyapp
 /ptermapp
+/osxlaunch
 /fuzzterm
 /testbn
 /*.DSA
diff --git a/Recipe b/Recipe
index a6689285c5450b0b1bf3ed196382cba69edc4548..f944c62e21699c95410ed2383e032652b374a29b 100644 (file)
--- a/Recipe
+++ b/Recipe
@@ -328,6 +328,8 @@ ptermapp : [XT] GTKTERM uxmisc misc ldisc settings uxpty uxsel BE_NONE uxstore
 puttyapp : [XT] GTKTERM uxmisc misc ldisc settings uxsel U_BE_ALL uxstore
          + uxsignal CHARSET uxputty NONSSH UXSSH UXMISC ux_x11 xpmputty
          + xpmpucfg gtkapp
+osxlaunch : [UT] osxlaunch
+
 fuzzterm : [UT] UXTERM CHARSET misc uxmisc uxucs fuzzterm time settings
         + uxstore be_none
 testbn   : [UT] testbn sshbn misc conf tree234 uxmisc
index 73cdfacae09d80f8a430d475db697e2270b12aa0..3c299270c0dfc7d748d2c3500d8d0e1db0f590bf 100644 (file)
@@ -148,6 +148,11 @@ int main(int argc, char **argv)
         setup(FALSE);     /* FALSE means we are not a one-session process */
     }
 
+    if (argc > 1) {
+        extern char *pty_osx_envrestore_prefix;
+        pty_osx_envrestore_prefix = argv[--argc];
+    }
+
     {
         const char *home = getenv("HOME");
         if (home) {
diff --git a/unix/osxlaunch.c b/unix/osxlaunch.c
new file mode 100644 (file)
index 0000000..2627c64
--- /dev/null
@@ -0,0 +1,414 @@
+/*
+ * Launcher program for OS X application bundles of PuTTY.
+ */
+
+/*
+ * The 'gtk-mac-bundler' utility arranges to build an OS X application
+ * bundle containing a program compiled against the Quartz GTK
+ * backend. It does this by including all the necessary GTK shared
+ * libraries and data files inside the bundle as well as the binary.
+ *
+ * But the GTK program won't start up unless all those shared
+ * libraries etc are already pointed to by environment variables like
+ * DYLD_LIBRARY_PATH, which won't be set up when the bundle is
+ * launched.
+ *
+ * Hence, gtk-mac-bundler expects to install the program in the bundle
+ * under a name like 'Contents/MacOS/Program-bin'; and the file called
+ * 'Contents/MacOS/Program', which is the one actually executed when
+ * the bundle is launched, is a wrapper script that sets up the
+ * environment before running the actual GTK-using program.
+ *
+ * In our case, however, that's not good enough. pterm will want to
+ * launch subprocesses with general-purpose shell sessions in them,
+ * and those subprocesses _won't_ want the random stuff dumped in the
+ * environment by the gtk-mac-bundler standard wrapper script. So I
+ * have to provide my own wrapper, which has a more complicated job:
+ * not only setting up the environment for the GTK app, but also
+ * preserving all details of the _previous_ environment, so that when
+ * pterm forks off a subprocess to run in a terminal session, it can
+ * restore the environment that was in force before the wrapper
+ * started messing about. This source file implements that wrapper,
+ * and does it in C so as to make string processing more reliable and
+ * less annoying.
+ *
+ * My strategy for saving the old environment is to pick a prefix
+ * that's unused by anything currently in the environment; let's
+ * suppose it's "P" for this discussion. Any environment variable I
+ * overwrite, say "VAR", I will either set "PsVAR=old value", or
+ * "PuVAR=" ("s" and "u" for "set" and "unset"). Then I pass the
+ * prefix itself as a command-line argument to the main GTK
+ * application binary, which then knows how to restore the original
+ * environment in pterm subprocesses.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef __APPLE__
+/* When we're not compiling for OS X, it's easier to just turn this
+ * program into a trivial hello-world by ifdef in the source than it
+ * is to remove it in the makefile edifice. */
+int main(int argc, char **argv)
+{
+    fprintf(stderr, "launcher does nothing on non-OSX platforms\n");
+    return 1;
+}
+#else /* __APPLE__ */
+
+#include <unistd.h>
+#include <libgen.h>
+#include <mach-o/dyld.h>
+
+/* ----------------------------------------------------------------------
+ * Find an alphabetic prefix unused by any environment variable name.
+ */
+
+/*
+ * This linked-list based system is a bit overkill, but I enjoy an
+ * algorithmic challenge. We essentially do an incremental radix sort
+ * of all the existing environment variable names: initially divide
+ * them into 26 buckets by their first letter (discarding those that
+ * don't have a letter at that position), then subdivide each bucket
+ * in turn into 26 sub-buckets, and so on. We maintain each bucket as
+ * a linked list, and link their heads together into a secondary list
+ * that functions as a queue (meaning that we go breadth-first,
+ * processing all the buckets of a given depth before moving on to the
+ * next depth down). At any stage, if we find one of our 26
+ * sub-buckets is empty, that's our unused prefix.
+ *
+ * The running time is O(number of strings * length of output), and I
+ * doubt it's possible to do better.
+ */
+
+#define FANOUT 26
+int char_index(int ch)
+{
+    if (ch >= 'A' && ch <= 'Z')
+        return ch - 'A';
+    else if (ch >= 'a' && ch <= 'z')
+        return ch - 'a';
+    else
+        return -1;
+}
+
+struct bucket {
+    int prefixlen;
+    struct bucket *next_bucket;
+    struct node *first_node;
+};
+
+struct node {
+    const char *string;
+    int len, prefixlen;
+    struct node *next;
+};
+
+struct node *new_node(struct node *prev_head, const char *string, int len)
+{
+    struct node *ret = (struct node *)malloc(sizeof(struct node));
+
+    if (!ret) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+
+    ret->next = prev_head;
+    ret->string = string;
+    ret->len = len;
+
+    return ret;
+}
+
+char *get_unused_env_prefix(void)
+{
+    struct bucket *qhead, *qtail;
+    extern char **environ;
+    char **e;
+
+    qhead = (struct bucket *)malloc(sizeof(struct bucket));
+    qhead->prefixlen = 0;
+    if (!qhead) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    for (e = environ; *e; e++)
+        qhead->first_node = new_node(qhead->first_node, *e, strcspn(*e, "="));
+
+    qtail = qhead;
+    while (1) {
+        struct bucket *buckets[FANOUT];
+        struct node *bucketnode;
+        int i, index;
+
+        for (i = 0; i < FANOUT; i++) {
+            buckets[i] = (struct bucket *)malloc(sizeof(struct bucket));
+            if (!buckets[i]) {
+                fprintf(stderr, "out of memory\n");
+                exit(1);
+            }
+            buckets[i]->prefixlen = qhead->prefixlen + 1;
+            qtail->next_bucket = buckets[i];
+            qtail = buckets[i];
+        }
+        qtail->next_bucket = NULL;
+
+        bucketnode = qhead->first_node;
+        while (bucketnode) {
+            struct node *node = bucketnode;
+            bucketnode = bucketnode->next;
+
+            if (node->len <= qhead->prefixlen)
+                continue;
+            index = char_index(node->string[qhead->prefixlen]);
+            if (!(index >= 0 && index < FANOUT))
+                continue;
+            node->prefixlen++;
+            node->next = buckets[index]->first_node;
+            buckets[index]->first_node = node;
+        }
+
+        for (i = 0; i < FANOUT; i++) {
+            if (!buckets[i]->first_node) {
+                char *ret = malloc(qhead->prefixlen + 2);
+                if (!ret) {
+                    fprintf(stderr, "out of memory\n");
+                    exit(1);
+                }
+                memcpy(ret, qhead->first_node->string, qhead->prefixlen);
+                ret[qhead->prefixlen] = i + 'A';
+                ret[qhead->prefixlen + 1] = '\0';
+
+                /* This would be where we freed everything, if we
+                 * didn't know it didn't matter because we were
+                 * imminently going to exec another program */
+                return ret;
+            }
+        }
+
+        qhead = qhead->next_bucket;
+    }
+}
+
+/* ----------------------------------------------------------------------
+ * Get the pathname of this executable, so we can locate the rest of
+ * the app bundle relative to it.
+ */
+
+/*
+ * There are several ways to try to retrieve the pathname to the
+ * running executable:
+ *
+ * (a) Declare main() as taking four arguments int main(int argc, char
+ * **argv, char **envp, char **apple); and look at apple[0].
+ *
+ * (b) Use sysctl(KERN_PROCARGS) to get the process arguments for the
+ * current pid. This involves two steps:
+ *  - sysctl(mib, 2, &argmax, &argmax_size, NULL, 0)
+ *     + mib is an array[2] of int containing
+ *       { CTL_KERN, KERN_ARGMAX }
+ *     + argmax is an int
+ *     + argmax_size is a size_t initialised to sizeof(argmax)
+ *     + returns in argmax the amount of memory you need for the next
+ *       call.
+ *  - sysctl(mib, 3, procargs, &procargs_size, NULL, 0)
+ *     + mib is an array[3] of int containing
+ *       { CTL_KERN, KERN_PROCARGS, current pid }
+ *     + procargs is a buffer of size 'argmax'
+ *     + procargs_size is a size_t initialised to argmax
+ *     + returns in the procargs buffer a collection of
+ *       zero-terminated strings of which the first is the program
+ *       name.
+ *
+ * (c) Call _NSGetExecutablePath, once to find out the needed buffer
+ * size and again to fetch the actual path.
+ *
+ * (d) Use Objective-C and Cocoa and call
+ * [[[NSProcessInfo processInfo] arguments] objectAtIndex: 0].
+ *
+ * So, how do those work in various cases? Experiments show:
+ *
+ *  - if you run the program as 'binary' (or whatever you called it)
+ *    and rely on the shell to search your PATH, all four methods
+ *    return a sensible-looking absolute pathname.
+ *
+ *  - if you run the program as './binary', (a) and (b) return just
+ *    "./binary", which has a particularly bad race condition if you
+ *    try to convert it into an absolute pathname using realpath(3).
+ *    (c) returns "/full/path/to/./binary", which still needs
+ *    realpath(3)ing to get rid of that ".", but at least it's
+ *    _trying_ to be fully qualified. (d) returns
+ *    "/full/path/to/binary" - full marks!
+ *     + Similar applies if you run it via a more interesting relative
+ *       path such as one with a ".." in: (c) gives you an absolute
+ *       path containing a ".." element, whereas (d) has sorted that
+ *       out.
+ *
+ *  - if you run the program via a path with a symlink on, _none_ of
+ *    these options successfully returns a path without the symlink.
+ *
+ * That last point suggests that even (d) is not a perfect solution on
+ * its own, and you'll have to realpath() whatever you get back from
+ * it regardless.
+ *
+ * And (d) is extra inconvenient because it returns an NSString, which
+ * is implicitly Unicode, so it's not clear how you turn that back
+ * into a char * representing a correct Unix pathname (what charset
+ * should you interpret it in?). Also because you have to bring in all
+ * of ObjC and Cocoa, which for a low-level Unix API client like this
+ * seems like overkill.
+ *
+ * So my conclusion is that (c) is most practical for these purposes.
+ */
+
+char *get_program_path(void)
+{
+    char *our_path;
+    uint32_t pathlen = 0;
+    _NSGetExecutablePath(NULL, &pathlen);
+    our_path = malloc(pathlen);
+    if (!our_path) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    if (_NSGetExecutablePath(our_path, &pathlen)) {
+        fprintf(stderr, "unable to get launcher executable path\n");
+        exit(1);
+    }
+
+    /* OS X guarantees to malloc the return value if we pass NULL */
+    char *our_real_path = realpath(our_path, NULL);
+    if (!our_real_path) {
+        fprintf(stderr, "realpath failed\n");
+        exit(1);
+    }
+
+    free(our_path);
+    return our_real_path;
+}
+
+/* ----------------------------------------------------------------------
+ * Wrapper on dirname(3) which mallocs its return value to whatever
+ * size is needed.
+ */
+
+char *dirname_wrapper(const char *path)
+{
+    char *path_copy = malloc(strlen(path) + 1);
+    if (!path_copy) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    strcpy(path_copy, path);
+    char *ret_orig = dirname(path_copy);
+    char *ret = malloc(strlen(ret_orig) + 1);
+    if (!ret) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    strcpy(ret, ret_orig);
+    free(path_copy);
+    return ret;
+}
+
+/* ----------------------------------------------------------------------
+ * mallocing string concatenation function.
+ */
+
+char *alloc_cat(const char *str1, const char *str2)
+{
+    int len1 = strlen(str1), len2 = strlen(str2);
+    char *ret = malloc(len1 + len2 + 1);
+    if (!ret) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    strcpy(ret, str1);
+    strcpy(ret + len1, str2);
+    return ret;
+}
+
+/* ----------------------------------------------------------------------
+ * Overwrite an environment variable, preserving the old one for the
+ * real app to restore.
+ */
+char *prefix, *prefixset, *prefixunset;
+void overwrite_env(const char *name, const char *value)
+{
+    const char *oldvalue = getenv(name);
+    if (oldvalue) {
+        setenv(alloc_cat(prefixset, name), oldvalue, 1);
+    } else {
+        setenv(alloc_cat(prefixunset, name), "", 1);
+    }
+    if (value)
+        setenv(name, value, 1);
+    else
+        unsetenv(name);
+}
+
+/* ----------------------------------------------------------------------
+ * Main program.
+ */
+
+int main(int argc, char **argv)
+{
+    prefix = get_unused_env_prefix();
+    prefixset = alloc_cat(prefix, "s");
+    prefixunset = alloc_cat(prefix, "u");
+
+    char *prog_path = get_program_path(); // <bundle>/Contents/MacOS/<filename>
+    char *macos = dirname_wrapper(prog_path); // <bundle>/Contents/MacOS
+    char *contents = dirname_wrapper(macos);  // <bundle>/Contents
+//    char *bundle = dirname_wrapper(contents); // <bundle>
+    char *resources = alloc_cat(contents, "/Resources");
+//    char *bin = alloc_cat(resources, "/bin");
+    char *etc = alloc_cat(resources, "/etc");
+    char *lib = alloc_cat(resources, "/lib");
+    char *share = alloc_cat(resources, "/share");
+    char *xdg = alloc_cat(etc, "/xdg");
+//    char *gtkrc = alloc_cat(etc, "/gtk-2.0/gtkrc");
+    char *locale = alloc_cat(share, "/locale");
+    char *realbin = alloc_cat(prog_path, "-bin");
+
+    overwrite_env("DYLD_LIBRARY_PATH", lib);
+    overwrite_env("XDG_CONFIG_DIRS", xdg);
+    overwrite_env("XDG_DATA_DIRS", share);
+    overwrite_env("GTK_DATA_PREFIX", resources);
+    overwrite_env("GTK_EXE_PREFIX", resources);
+    overwrite_env("GTK_PATH", resources);
+    overwrite_env("PANGO_LIBDIR", lib);
+    overwrite_env("PANGO_SYSCONFDIR", etc);
+    overwrite_env("I18NDIR", locale);
+    overwrite_env("LANG", NULL);
+    overwrite_env("LC_MESSAGES", NULL);
+    overwrite_env("LC_MONETARY", NULL);
+    overwrite_env("LC_COLLATE", NULL);
+
+    char **new_argv = malloc((argc + 16) * sizeof(const char *));
+    if (!new_argv) {
+        fprintf(stderr, "out of memory\n");
+        exit(1);
+    }
+    int j = 0;
+    new_argv[j++] = realbin;
+    {
+        int i = 1;
+        if (i < argc && !strncmp(argv[i], "-psn_", 5))
+            i++;
+
+        for (; i < argc; i++)
+            new_argv[j++] = argv[i];
+    }
+    new_argv[j++] = prefix;
+    new_argv[j++] = NULL;
+
+    execv(realbin, new_argv);
+    perror("execv");
+    return 127;
+}
+
+#endif /* __APPLE__ */
index e96ff3568e7abe89a3cae137c7e0844fab23f8e1..39f96f126afd9a347f3c46caf2bd7c8fed914015 100644 (file)
@@ -180,6 +180,8 @@ static struct utmpx utmp_entry;
  */
 char **pty_argv;
 
+char *pty_osx_envrestore_prefix;
+
 static void pty_close(Pty pty);
 static void pty_try_write(Pty pty);
 
@@ -809,6 +811,34 @@ static const char *pty_init(void *frontend, void **backend_handle, Conf *conf,
         * We are the child.
         */
 
+        if (pty_osx_envrestore_prefix) {
+            int plen = strlen(pty_osx_envrestore_prefix);
+            extern char **environ;
+            char **ep;
+
+          restart_osx_env_restore:
+            for (ep = environ; *ep; ep++) {
+                char *e = *ep;
+
+                if (!strncmp(e, pty_osx_envrestore_prefix, plen)) {
+                    int unset = (e[plen] == 'u');
+                    char *pname = dupprintf("%.*s", (int)strcspn(e, "="), e);
+                    char *name = pname + plen + 1;
+                    char *value = e + strcspn(e, "=");
+                    if (*value) value++;
+                    value = dupstr(value);
+                    if (unset)
+                        unsetenv(name);
+                    else
+                        setenv(name, value, 1);
+                    unsetenv(pname);
+                    sfree(pname);
+                    sfree(value);
+                    goto restart_osx_env_restore;
+                }
+            }
+        }
+
        slavefd = pty_open_slave(pty);
        if (slavefd < 0) {
            perror("slave pty: open");
index 7933130fd9abc6689d14c676570a634bc49221e2..4de1583d2a27cf5ad573ae5443b0f015e80ac697 100644 (file)
@@ -20,6 +20,7 @@
  */
 const int use_pty_argv = FALSE;
 char **pty_argv;                      /* never used */
+char *pty_osx_envrestore_prefix;
 
 /*
  * Clean up and exit.