diff --git a/Gruntfile.js b/Gruntfile.js index 467535b50..6650a69f3 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -131,8 +131,10 @@ module.exports = function (grunt) { "src" : [ "lib/**", "locales/**", + "node-core/**", "appshell*.png", "Brackets", + "Brackets-node", "cef.pak", "devtools_resources.pak" ], diff --git a/appshell.gyp b/appshell.gyp index c981e0ed4..e73456c2f 100755 --- a/appshell.gyp +++ b/appshell.gyp @@ -257,9 +257,22 @@ { 'destination': '<(PRODUCT_DIR)', 'files': [ - '<@(appshell_bundle_resources_linux)', + '<@(appshell_bundle_resources_linux)' ], }, + { + # Copy node executable to the output directory + 'destination': '<(PRODUCT_DIR)', + 'files': ['deps/node/bin/Brackets-node'], + }, + { + # Copy node server files to the output directory + # The '/' at the end of the 'files' directory is very important and magically + # causes 'xcopy' to get used instead of 'copy' for recursive copies. + # This seems to be an undocumented feature of gyp. + 'destination': '<(PRODUCT_DIR)', + 'files': ['appshell/node-core/'], + }, ], 'sources': [ '<@(includes_linux)', diff --git a/appshell/appshell_extensions_gtk.cpp b/appshell/appshell_extensions_gtk.cpp index 5df76723e..db3e15420 100644 --- a/appshell/appshell_extensions_gtk.cpp +++ b/appshell/appshell_extensions_gtk.cpp @@ -403,6 +403,10 @@ int ConvertLinuxErrorCode(int errorCode, bool isReading) } } +int32 CopyFile(ExtensionString src, ExtensionString dest) +{ +} + int32 GetPendingFilesToOpen(ExtensionString& files) { } diff --git a/appshell/appshell_node_process_linux.cpp b/appshell/appshell_node_process_linux.cpp index 34721a0fd..d4b4ce410 100644 --- a/appshell/appshell_node_process_linux.cpp +++ b/appshell/appshell_node_process_linux.cpp @@ -21,55 +21,213 @@ * */ + #include "appshell_node_process.h" #include "appshell_node_process_internal.h" +#include +#include +#include +#include +#include +#include +#include + + #ifndef OS_LINUX #define OS_LINUX 1 #endif #include "config.h" #define BRACKETS_NODE_BUFFER_SIZE 4096 +#define MAX_PATH 128 + +// init mutex +pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; + +// write & read references to subprocess +FILE* streamTo; +FILE* streamFrom; + +// Threads should hold mutex before using these +static int nodeState = BRACKETS_NODE_NOT_YET_STARTED; +static int nodeStartTime = 0; // Forward declarations -//DWORD WINAPI NodeThread(LPVOID); +void* nodeThread(void*); +void* nodeReadThread(void*); void restartNode(bool); // Creates the thread that starts Node and then monitors the state // of the node process. void startNodeProcess() { + + pthread_t thread_id; + if (pthread_create(&thread_id, NULL, &nodeThread, NULL) != 0) + nodeState = BRACKETS_NODE_FAILED; +} + + +// Thread function for the thread that starts the node process +// and monitors that process's state +void* nodeThread(void* unused) { + + // get mutex + if (pthread_mutex_lock(&mutex)) { + fprintf(stderr, + "failed to acquire mutex for Node subprocess start: %s\n", + strerror(errno)); + return NULL; + } + + // TODO nodeStartTime = get time(); + + char executablePath[MAX_PATH]; + char bracketsDirPath[MAX_PATH]; + char nodeExecutablePath[MAX_PATH]; + char nodecorePath[MAX_PATH]; + + // get path to Brackets + if (readlink("/proc/self/exe", executablePath, MAX_PATH) == -1) { + fprintf(stderr, "cannot find Brackets path: %s\n", strerror(errno)); + pthread_mutex_unlock(&mutex); + return NULL; + } + + // strip off trailing executable name + char* lastIndexOf = strrchr(executablePath, '/'); + memcpy(bracketsDirPath, executablePath, lastIndexOf - executablePath + 1); + + // create node exec and node-core paths + strcpy(nodeExecutablePath, bracketsDirPath); + strcat(nodeExecutablePath, NODE_EXECUTABLE_PATH); + strcpy(nodecorePath, bracketsDirPath); + strcat(nodecorePath, NODE_CORE_PATH); + + // create pipes for node process stdin/stdout + int toNode[2]; + int fromNode[2]; + + pipe(toNode); + pipe(fromNode); + + // create the Node process + pid_t child_pid = fork(); + if (child_pid == 0) { // child (node) process + // close our copy of write end and + // connect read end to stdin + close(toNode[1]); + dup2(toNode[0], STDIN_FILENO); + + // close our copy of read end and + // connect write end to stdout + close(fromNode[0]); + dup2(fromNode[1], STDOUT_FILENO); + + // run node executable + char* arg_list[] = { nodeExecutablePath, nodecorePath, NULL}; + execvp(arg_list[0], arg_list); + + fprintf(stderr, "the Node process failed to start: %s\n", strerror(errno)); + abort(); + } + else { // parent + + // close our reference of toNode's read end + // and fromNode's write end + close(toNode[0]); + close(fromNode[1]); + + // convert subprocess write & read to FILE objects + streamTo = fdopen(toNode[1], "w"); + streamFrom = fdopen(fromNode[0], "r"); + + nodeState = BRACKETS_NODE_PORT_NOT_YET_SET; + + // done launching process so release mutex + if (pthread_mutex_unlock(&mutex)) { + fprintf(stderr, + "failed to release mutex for Node subprocess startup: %s\n", + strerror(errno)); + } + + // start pipe read thread + pthread_t readthread_id; + if (pthread_create(&readthread_id, NULL, &nodeReadThread, NULL) != 0) + nodeState = BRACKETS_NODE_FAILED; + // ugly - need to think more about what to do if read thread fails + + } + + return NULL; } + // Thread function for the thread that reads from the Node pipe // Reads on anonymous pipes are always blocking (OVERLAPPED reads // are not possible) So, we need to do this in a separate thread -// + // TODO: This code first reads to a character buffer, and then // copies it to a std::string. The code could be optimized to avoid // this double-copy -//DWORD WINAPI NodeReadThread(LPVOID lpParam) { -//} +void* nodeReadThread(void* unused) { + + char charBuf[BRACKETS_NODE_BUFFER_SIZE]; + std::string strBuf(""); + while (fgets(charBuf, BRACKETS_NODE_BUFFER_SIZE, streamFrom) != NULL) { + strBuf.assign(charBuf); + processIncomingData(strBuf); + } +} -// Thread function for the thread that starts the node process -// and monitors that process's state -//DWORD WINAPI NodeThread(LPVOID lpParam) { -//} // Determines whether the current node process has run long // enough that a restart is warranted, and initiates the startup // if so. Can also optionally terminate the running process. void restartNode(bool terminateCurrentProcess) { + } // Sends data to the node process. If the write fails completely, // calls restartNode. void sendData(const std::string &data) { + + if (pthread_mutex_lock(&mutex)) { + fprintf(stderr, + "failed to acquire mutex for write to Node subprocess: %s\n", + strerror(errno)); + return; + } + + // write to pipe + fprintf(streamTo, "%s", data.c_str()); + + if (pthread_mutex_unlock(&mutex)) { + fprintf(stderr, + "failed to release mutex for write to Node subprocess: %s\n", + strerror(errno)); + } } -// Thread-safe way to access nodeState variable +// Returns nodeState variable int getNodeState() { + + return nodeState; } // Thread-safe way to set nodeState variable void setNodeState(int newState) { + + if (pthread_mutex_lock(&mutex)) { + fprintf(stderr, + "failed to acquire mutex for setting Node state: %s\n", + strerror(errno)); + return; + } + nodeState = newState; + if (pthread_mutex_unlock(&mutex)) { + fprintf(stderr, + "failed to release mutex for Node set state: %s\n", + strerror(errno)); + } } diff --git a/appshell/cefclient_gtk.cpp b/appshell/cefclient_gtk.cpp index 668e8d9ba..7c7349395 100644 --- a/appshell/cefclient_gtk.cpp +++ b/appshell/cefclient_gtk.cpp @@ -1,4 +1,4 @@ - // Copyright (c) 2011 The Chromium Embedded Framework Authors. All rights +// Copyright (c) 2011 The Chromium Embedded Framework Authors. All rights // reserved. Use of this source code is governed by a BSD-style license that // can be found in the LICENSE file. @@ -13,6 +13,7 @@ #include "include/cef_frame.h" #include "include/cef_runnable.h" #include "client_handler.h" +#include "appshell_node_process.h" static std::string APPICONS[] = {"appshell32.png","appshell48.png","appshell128.png","appshell256.png"}; char szWorkingDir[512]; // The current working directory @@ -24,7 +25,7 @@ bool isReallyClosing = false; // The global ClientHandler reference. extern CefRefPtr g_handler; -//Application startup time +// Application startup time time_t g_appStartupTime; void destroy(void) { @@ -75,7 +76,7 @@ int GetInitialUrl() { if (gtk_dialog_run (GTK_DIALOG (dialog)) == GTK_RESPONSE_ACCEPT) { - szInitialUrl.append(gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog))); + szInitialUrl = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog)); gtk_widget_destroy (dialog); return 0; } @@ -247,6 +248,9 @@ int main(int argc, char* argv[]) { // Install an signal handler so we clean up after ourselves. signal(SIGINT, TerminationSignalHandler); signal(SIGTERM, TerminationSignalHandler); + + // Start the node server process + startNodeProcess(); CefRunMessageLoop(); @@ -258,4 +262,4 @@ int main(int argc, char* argv[]) { CefString AppGetProductVersionString() { // TODO return CefString(""); -} \ No newline at end of file +} diff --git a/appshell/client_app_gtk.cpp b/appshell/client_app_gtk.cpp index 17937611f..29f3167db 100644 --- a/appshell/client_app_gtk.cpp +++ b/appshell/client_app_gtk.cpp @@ -94,3 +94,8 @@ CefString ClientApp::AppGetSupportDirectory() return home_dir.append("/.Brackets"); } +CefString ClientApp::AppGetDocumentsDirectory() +{ + std::string home_dir(getenv("HOME")); + return home_dir; +} diff --git a/appshell/config.h b/appshell/config.h index e43c5f6f2..094af766c 100644 --- a/appshell/config.h +++ b/appshell/config.h @@ -48,6 +48,17 @@ #define NODE_EXECUTABLE_PATH @"/Contents/MacOS/Brackets-node" #define NODE_CORE_PATH @"/Contents/node-core" +#endif +#ifdef OS_LINUX +// TODO linux preferences +//#define GROUP_NAME @"" +//#define APP_NAME @"Brackets" +//#define WINDOW_TITLE APP_NAME + +// Path for node resources is in dependencies dir and relative to the location of the appshell executable +#define NODE_EXECUTABLE_PATH "Brackets-node" +#define NODE_CORE_PATH "node-core" + #endif #define REMOTE_DEBUGGING_PORT 9234 diff --git a/installer/linux/build_installer.sh b/installer/linux/build_installer.sh index df704a823..7901b543a 100755 --- a/installer/linux/build_installer.sh +++ b/installer/linux/build_installer.sh @@ -2,8 +2,9 @@ # grunt-contrib-copy doesn't preserve permissions # https://github.com/gruntjs/grunt/issues/615 -chmod 755 debian/package-root/opt/brackets/Brackets chmod 755 debian/package-root/opt/brackets/brackets +chmod 755 debian/package-root/opt/brackets/Brackets +chmod 755 debian/package-root/opt/brackets/Brackets-node chmod 755 debian/package-root/DEBIAN/prerm chmod 755 debian/package-root/DEBIAN/postrm chmod 755 debian/package-root/DEBIAN/postinst diff --git a/tasks/setup.js b/tasks/setup.js index 99e94620a..a16ecd60a 100644 --- a/tasks/setup.js +++ b/tasks/setup.js @@ -42,7 +42,8 @@ module.exports = function (grunt) { /* use promises instead of callbacks */ link, rename = common.rename, - exec = common.exec; + exec = common.exec, + platform = common.platform(); // cross-platform symbolic link link = (function () { @@ -67,7 +68,7 @@ module.exports = function (grunt) { // task: cef grunt.registerTask("cef", "Download and setup CEF", function () { - var config = "cef-" + common.platform() + common.arch(), + var config = "cef-" + platform + common.arch(), zipSrc = grunt.config("curl-dir." + config + ".src"), zipName = zipSrc.substr(zipSrc.lastIndexOf("/") + 1), zipDest = grunt.config("curl-dir." + config + ".dest") + zipName, @@ -145,7 +146,7 @@ module.exports = function (grunt) { // rename version stamped name to cef return rename("deps/" + zipName, "deps/cef"); }).then(function () { - if (common.platform() === "mac") { + if (platform === "mac") { // FIXME figure out how to use fs.chmod to only do additive mode u+x return exec("chmod u+x deps/cef/tools/*"); } @@ -190,8 +191,7 @@ module.exports = function (grunt) { // task: node-download grunt.registerTask("node", "Download Node.js binaries and setup dependencies", function () { - var platform = common.platform(), - config = "node-" + platform + common.arch(), + var config = "node-" + platform + common.arch(), nodeSrc = grunt.config("curl-dir." + config + ".src"), nodeDest = [], dest = grunt.config("curl-dir." + config + ".dest"), @@ -308,7 +308,7 @@ module.exports = function (grunt) { gypCommand; // TODO why doesn't gyp (in the repository) work for linux? - if (common.platform() === "linux") { + if (platform === "linux") { gypCommand = "gyp --depth ."; } else { gypCommand = "bash -c 'gyp/gyp appshell.gyp -I common.gypi --depth=.'"; @@ -319,7 +319,7 @@ module.exports = function (grunt) { // Run gyp promise = exec(gypCommand); - if (common.platform() === "mac") { + if (platform === "mac") { promise = promise.then(function () { // FIXME port to JavaScript? return exec("bash scripts/fix-xcode.sh");