diff --git a/pd/nw/index.js b/pd/nw/index.js index 37ae7ba42b8a1b0bdb8d5867f393cd21cb4f5a89..c05d91d606704e73f91678b6a6b58e7ed31ac6f3 100644 --- a/pd/nw/index.js +++ b/pd/nw/index.js @@ -25,7 +25,7 @@ function have_args() { } function set_vars(win) { - var port_no, gui_dir, font_engine_sanity; + var port_no, gui_dir, font_engine_sanity, pd_engine_id; // If the GUI was started by Pd, our port number is going to be // the first argument. If the GUI is supposed to start Pd, we won't // have any arguments and need to set it here. @@ -33,6 +33,8 @@ function set_vars(win) { port_no = gui.App.argv[0]; // fed to us by the Pd process // looks like this is the same as pwd below gui_dir = gui.App.argv[3]; + // address unique to the pd_engine + pd_engine_id = gui.App.argv[4]; } else { // If we're starting Pd, this is the first port number to try. (We'll // increment it if that port happens to be taken. @@ -44,6 +46,7 @@ function set_vars(win) { gui_dir = process.platform === "darwin" ? "bin" : pwd; } pdgui.set_port(port_no); + pdgui.set_pd_engine_id(pd_engine_id); pdgui.set_pwd(pwd); pdgui.set_gui_dir(gui_dir); pdgui.set_pd_window(win); @@ -256,6 +259,14 @@ function add_events() { pdgui.pdsend("pd dsp", dsp_state); } ); + // Opening another file + nw.App.on("open", function(argv_string) { + var port, pd_engine_id; + port = argv_string.split(" ").slice(-5, -4); + pd_engine_id = argv_string.split(" ").slice(-1); + pdgui.connect_as_client_to_secondary_instance("localhost", port, + pd_engine_id); + }); // Browser Window Close gui.Window.get().on("close", function () { pdgui.menu_quit(); diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js index 84bc392d3f74f7a2342935b61c01c3e618afdc61..ef3c25aa76efe3c2e4235fb1de8a116b19a05d62 100644 --- a/pd/nw/pdgui.js +++ b/pd/nw/pdgui.js @@ -4,6 +4,7 @@ var pwd; var gui_dir; var lib_dir; var last_clipboard_data; +var pd_engine_id; exports.set_pwd = function(pwd_string) { pwd = pwd_string; @@ -21,6 +22,10 @@ function defunkify_windows_path(s) { return ret; } +exports.set_pd_engine_id = function (id) { + pd_engine_id = id; +} + exports.defunkify_windows_path = defunkify_windows_path; exports.set_gui_dir = function(dir_string) { @@ -217,8 +222,6 @@ var pd_myversion, // Pd version string popup_coords = [0,0]; - var startup_files = []; // Array of files to be opened at startup (from the command line) - // Keycode vs Charcode: A Primer // ----------------------------- // * keycode is a unique number assigned to a physical key on the keyboard @@ -786,19 +789,6 @@ function open_file(file) { } } -// This doesn't work yet... need to figure out how to send command line args -// (files) to be opened by the unique instance -function gui_open_files_via_unique(filenames) -{ - var i; - length = filenames.length; - if (length != 0) { - for (i = 0; i < length; i++) { - open_file(filenames[i]); - } - } -} - function open_html(target) { nw_open_html(target); } @@ -845,34 +835,29 @@ function external_doc_open(url) { exports.external_doc_open = external_doc_open; -function gui_build_filelist(file) { - startup_files.push(file); +function gui_set_cwd(dummy, cwd) { + post("cwd from secondary instance is " + cwd); + if (cwd !== ".") { + pwd = cwd; + } } // This doesn't work at the moment. Not sure how to feed the command line // filelist to a single instance of node-webkit. -function gui_check_unique (unique) { - // global appname - return; - var final_filenames = new Array, - startup_dir = pwd, - filelist_length, +function gui_open_via_unique (secondary_pd_engine_id, unique, file_array) { + var startup_dir = pwd, i, - file, - dir; - if (unique == 0) { - filelist_length = startup_files.length; - for (i = 0; i < filelist_length; i++) { - file = startup_files[i]; - dir; - if (!pathIsAbsolute(file)) { - file = fs.join(pwd, file); + file; + if (unique == 0 && secondary_pd_engine_id !== pd_engine_id) { + for (i = 0; i < file_array.length; i++) { + file = file_array[i]; + if (!path.isAbsolute(file)) { + file = path.join(pwd, file); } - final_filenames.push(file); + open_file(file); } - gui_open_files_via_unique(final_filenames); + quit_secondary_pd_instance(secondary_pd_engine_id); } - // old tcl follows... see pd.tk for the original code } function gui_startup(version, fontname_from_pd, fontweight_from_pd, @@ -1209,6 +1194,85 @@ exports.set_port = function (port_no) { PORT = port_no; } +var secondary_pd_engines = {}; + +// This is an alarmingly complicated and brittle approach to opening +// files from a secondary instance of Pd in a currently running instance. +// It works something like this: +// 1. User is running an instance of Purr Data. +// 2. User runs another instance of Purr Data from the command line, specifying +// files to be opened as command line args. Or, they click on a file which +// in the desktop or file manager which triggers the same behavior. +// 2. A new Pd process starts-- let's call it a "secondary pd engine". +// 3. The secondary pd engine tries to run a new GUI. +// 4. The secondary GUI forwards an "open" message to the currently running GUI. +// 5. The secondary GUI exits (before spawning any windows). +// 6. The original GUI receives the "open" message, finds the port number +// for the secondary Pd engine, and opens a socket to it. +// 7. The original GUI receives a message to set the working directory to +// whatever the secondary Pd engine thinks it should be. +// 8. The original GUI sends a message to the secondary Pd instance, telling +// it to send a list of files to be opened. +// 9. The original GUI receives a message from the secondary Pd instance +// with the list of files. +// 10.For each file to be opened, the original GUI sends a message to the +// _original_ Pd engine to open the file. +// 11.Once these messages have been sent, the original GUI sends a message +// to the secondary Pd engine to quit. +// 12.The original Pd engine opens the files, and the secondary Pd instance +// quits. +function connect_as_client_to_secondary_instance(host, port, pd_engine_id) { + var client = new net.Socket(), + command_buffer = { + next_command: "" + }; + client.setNoDelay(true); + client.connect(+port, host, function() { + console.log("CONNECTED TO: " + host + ":" + port); + secondary_pd_engines[pd_engine_id] = { + socket: client + } + client.write("pd forward_files_from_secondary_instance;"); + }); + client.on("data", function(data) { + // Terrible thing: + // We're parsing the data as it comes in-- possibly + // from multiple ancillary instances of the Pd engine. + // So to retain some semblance of sanity, we only let the + // parser evaluate commands that we list in the array below-- + // anything else will be discarded. This is of course bad + // because it means simple changes to the code, e.g., changing + // the name of the function "gui_set_cwd" would cause a bug + // if you forget to come here and also change that name in the + // array below. + // Another terrible thing-- gui_set_cwd sets a single, global + // var for the working directory. So if the user does something + // weird like write a script to open files from random directories, + // there would be a race and it might not work. + // Yet another terrible thing-- now we're setting the current + // working directory both in the GUI, and from the secondary instances + // with "gui_set_cwd" below. + perfect_parser(data, command_buffer, [ + "gui_set_cwd", + "gui_open_via_unique" + ]); + }); + client.on("close", function () { + // I guess somebody could script opening patches in an + // installation, so let's go ahead and delete the key here + // (The alternative is just setting it to undefined) + delete secondary_pd_engines[pd_engine_id]; + }); +} + +function quit_secondary_pd_instance (pd_engine_id) { + secondary_pd_engines[pd_engine_id].socket.write("pd quit;"); +} + +// This is called when the running GUI receives an "open" event. +exports.connect_as_client_to_secondary_instance = + connect_as_client_to_secondary_instance; + function connect_as_client() { var client = new net.Socket(); client.setNoDelay(true); @@ -1261,17 +1325,12 @@ exports.connect_as_server = connect_as_server; // data parameter is what the server sent to this socket // We're not receiving FUDI (i.e., Pd) messages. Right now we're just using -// an alarm bell '\a' to signal the end of a message. This is easier than -// checking for unescaped semicolons, since it only requires a check for a -// single byte. Of course this makes it more brittle, so it can be changed -// later if needed. +// the unit separator (ASCII 31) to signal the end of a message. This is +// easier than checking for unescaped semicolons, since it only requires a +// check for a single byte. Of course this makes it more brittle, so it can +// be changed later if needed. -function init_socket_events () { - var next_command = ""; // A not-quite-FUDI command: selector arg1,arg2,etc. - // These are formatted on the C side to be easy - // to parse here in javascript - - var perfect_parser = function(data) { +function perfect_parser(data, cbuf, sel_array) { var i, len, selector, args; len = data.length; for (i = 0; i < len; i++) { @@ -1280,24 +1339,33 @@ function init_socket_events () { // decode next_command try { // This should work for all utf-8 content - next_command = decodeURIComponent(next_command); + cbuf.next_command = + decodeURIComponent(cbuf.next_command); } catch(err) { // This should work for ISO-8859-1 - next_command = unescape(next_command); + cbuf.next_command = unescape(cbuf.next_command); } // Turn newlines into backslash + "n" so // eval will do the right thing with them - next_command = next_command.replace(/\n/g, "\\n"); - next_command = next_command.replace(/\r/g, "\\r"); - selector = next_command.slice(0, next_command.indexOf(" ")); - args = next_command.slice(selector.length + 1); - next_command = ""; + cbuf.next_command = cbuf.next_command.replace(/\n/g, "\\n"); + cbuf.next_command = cbuf.next_command.replace(/\r/g, "\\r"); + selector = cbuf.next_command.slice(0, cbuf.next_command.indexOf(" ")); + args = cbuf.next_command.slice(selector.length + 1); + cbuf.next_command = ""; // Now evaluate it //post("Evaling: " + selector + "(" + args + ");"); - eval(selector + "(" + args + ");"); + // For communicating with a secondary instance, we filter + // incoming messages. A better approach would be to make + // sure that the Pd engine only sends the gui_set_cwd message + // before "gui_startup". Then we could just check the + // Pd engine id in "gui_startup" and branch there, instead of + // fudging with the parser here. + if (!sel_array || sel_array.indexOf(selector) !== -1) { + eval(selector + "(" + args + ");"); + } } else { - next_command += "%" + + cbuf.next_command += "%" + ("0" // leading zero (for rare case of single digit) + data[i].toString(16)) // to hex .slice(-2); // remove extra leading zero @@ -1305,31 +1373,15 @@ function init_socket_events () { } }; - var fast_parser = function(data) { - var i, len, selector, args; - len = data.length; - for (i = 0; i < len; i++) { - // check for end of command: - if (data[i] === String.fromCharCode(7)) { // vertical tab - // we have the next_command... - // Turn newlines into backslash + "n" so - // eval will do the right thing - next_command = next_command.replace(/\n/g, "\\n"); - selector = next_command.slice(0, next_command.indexOf(" ")); - args = next_command.slice(selector.length + 1); - next_command = ""; - // Now evaluate it-- unfortunately the V8 engine can't - // optimize this eval call. - //post("Evaling: " + selector + "(" + args + ");"); - eval(selector + "(" + args + ");"); - } else { - next_command += data[i]; - } - } +function init_socket_events () { + // A not-quite-FUDI command: selector arg1,arg2,etc. These are + // formatted on the C side to be easy to parse here in javascript + var command_buffer = { + next_command: "" }; - - connection.on("data", perfect_parser); - + connection.on("data", function(data) { + perfect_parser(data, command_buffer); + }); connection.on("error", function(e) { console.log("Socket error: " + e.code); nw_app_quit(); diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c index d5d411eb7a05fbf4c18ff83c3084007136bcde99..734084845ebbe3d067162229679a0693632655e7 100644 --- a/pd/src/m_glob.c +++ b/pd/src/m_glob.c @@ -36,6 +36,7 @@ void glob_startup_dialog(t_pd *dummy, t_symbol *s, int argc, t_atom *argv); void glob_ping(t_pd *dummy); void glob_watchdog(t_pd *dummy); void glob_savepreferences(t_pd *dummy); +void glob_forward_files_from_secondary_instance(void); void alsa_resync( void); @@ -111,6 +112,9 @@ void glob_init(void) CLASS_DEFAULT, A_NULL); class_addmethod(glob_pdobject, (t_method)glob_initfromgui, gensym("init"), A_GIMME, 0); + class_addmethod(glob_pdobject, + (t_method)glob_forward_files_from_secondary_instance, + gensym("forward_files_from_secondary_instance"), 0); class_addmethod(glob_pdobject, (t_method)glob_setfilename, gensym("filename"), A_SYMBOL, A_SYMBOL, 0); class_addmethod(glob_pdobject, (t_method)glob_evalfile, gensym("open"), diff --git a/pd/src/s_inter.c b/pd/src/s_inter.c index 37d6ac0b69dd805037205f5e0c19dcfa27596adc..1d6d23c1f4d2eee44024e916ac2a2e98c3101694 100644 --- a/pd/src/s_inter.c +++ b/pd/src/s_inter.c @@ -1134,12 +1134,22 @@ static int defaultfontshit[MAXFONTS] = { int sys_startgui(const char *guidir) { pid_t childpid; + char cwd[FILENAME_MAX]; char cmdbuf[4*MAXPDSTRING]; struct sockaddr_in server = {0}; int len = sizeof(server); int ntry = 0, portno = FIRSTPORTNUM; int xsock = -1; stderr_isatty = isatty(2); +#ifdef MSW + if (GetCurrentDirectory(FILENAME_MAX, cwd) == 0) + strcpy(cwd, "."); +#endif +#ifdef HAVE_UNISTD_H + if (!getcwd(cwd, FILENAME_MAX)) + strcpy(cwd, "."); + +#endif #ifdef MSW short version = MAKEWORD(2, 0); WSADATA nobby; @@ -1178,15 +1188,7 @@ int sys_startgui(const char *guidir) skip starting the GUI up. */ t_atom zz[NDEFAULTFONT+2]; int i; -#ifdef MSW - if (GetCurrentDirectory(MAXPDSTRING, cmdbuf) == 0) - strcpy(cmdbuf, "."); -#endif -#ifdef HAVE_UNISTD_H - if (!getcwd(cmdbuf, MAXPDSTRING)) - strcpy(cmdbuf, "."); - -#endif + strcpy(cmdbuf, cwd); SETSYMBOL(zz, gensym(cmdbuf)); for (i = 0; i < (int)NDEFAULTFONT; i++) SETFLOAT(zz+i+1, defaultfontshit[i]); @@ -1358,13 +1360,14 @@ fprintf(stderr, "guidir is %s\n", guidir); we add it again as the last argument to make sure we can fetch it on the GUI side. */ sprintf(cmdbuf, - "%s/nw/nw %s %d localhost %s %s\n", + "%s/nw/nw %s %d localhost %s %s x%.6lx", guidir, guidir, // "/home/user/purr-data/pd/nw", portno, (sys_k12_mode ? "pd-l2ork-k12" : "pd-l2ork"), - guidir); + guidir, + (long unsigned int)pd_this); #endif sys_guicmd = cmdbuf; @@ -1596,6 +1599,7 @@ fprintf(stderr, "guidir is %s\n", guidir); gui_end_array(); gui_end_vmess(); + gui_vmess("gui_set_cwd", "xs", 0, cwd); binbuf_free(aapis); binbuf_free(mapis); } diff --git a/pd/src/s_main.c b/pd/src/s_main.c index c2ef2431f870eaadfcb8f4146954391d428b1be8..bacdd8d2f88033a78f977fcb3ffe30f9cd04afa4 100644 --- a/pd/src/s_main.c +++ b/pd/src/s_main.c @@ -259,6 +259,31 @@ static void pd_makeversion(void) pd_version = strdup(foo); } +/* send the openlist to the GUI before closing a secondary + instance of Pd. */ +void glob_forward_files_from_secondary_instance(void) +{ + /* check if we are unique, otherwise, just focus existing + instance, and if necessary open file inside it. This doesn't + yet work with the new GUI because we need to set it up to + allow multiple instances. */ + gui_start_vmess("gui_open_via_unique", "xi", pd_this, sys_unique); + gui_start_array(); + if (sys_openlist) + { + // send the files to be opened to the GUI. We send them as an + // array here so that we don't have to allocate anything here + // (as the previous API did) + t_namelist *nl; + for (nl = sys_openlist; nl; nl = nl->nl_next) + { + gui_s(nl->nl_string); + } + } + gui_end_array(); + gui_end_vmess(); +} + /* this is called from main() in s_entry.c */ int sys_main(int argc, char **argv) { @@ -296,22 +321,6 @@ int sys_main(int argc, char **argv) /* send the name of the gui preset */ gui_vmess("gui_set_gui_preset", "s", sys_gui_preset->s_name); - if (sys_openlist) - { - // send the files to be opened to the GUI. We send them one - // at a time and let the GUI accumulate them so that we don't - // have to allocate anything here (as the previous API did) - t_namelist *nl; - for (nl = sys_openlist; nl; nl = nl->nl_next) - { - gui_vmess("gui_build_filelist", "s", nl->nl_string); - } - } - /* check if we are unique, otherwise, just focus existing - instance, and if necessary open file inside it. This doesn't - yet work with the new GUI because we need to set it up to - allow multiple instances. */ - gui_vmess("gui_check_unique", "i", sys_unique); if (sys_externalschedlib) return (sys_run_scheduler(sys_externalschedlibname, sys_extraflagsstring));