diff --git a/CMakeLists.txt b/CMakeLists.txt index 5e8726d08..48ad5033e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -160,6 +160,9 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Linux") # Boost on Raspberry-Pi does not link to pthreads. set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) + + find_package(DBus REQUIRED) + include_directories(${DBUS_INCLUDE_DIRS}) endif() if (CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUXX) diff --git a/cmake/modules/FindDBus.cmake b/cmake/modules/FindDBus.cmake new file mode 100644 index 000000000..1d0f29dd7 --- /dev/null +++ b/cmake/modules/FindDBus.cmake @@ -0,0 +1,59 @@ +# - Try to find DBus +# Once done, this will define +# +# DBUS_FOUND - system has DBus +# DBUS_INCLUDE_DIRS - the DBus include directories +# DBUS_LIBRARIES - link these to use DBus +# +# Copyright (C) 2012 Raphael Kubo da Costa +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS +# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +FIND_PACKAGE(PkgConfig) +PKG_CHECK_MODULES(PC_DBUS QUIET dbus-1) + +FIND_LIBRARY(DBUS_LIBRARIES + NAMES dbus-1 + HINTS ${PC_DBUS_LIBDIR} + ${PC_DBUS_LIBRARY_DIRS} +) + +FIND_PATH(DBUS_INCLUDE_DIR + NAMES dbus/dbus.h + HINTS ${PC_DBUS_INCLUDEDIR} + ${PC_DBUS_INCLUDE_DIRS} +) + +GET_FILENAME_COMPONENT(_DBUS_LIBRARY_DIR ${DBUS_LIBRARIES} PATH) +FIND_PATH(DBUS_ARCH_INCLUDE_DIR + NAMES dbus/dbus-arch-deps.h + HINTS ${PC_DBUS_INCLUDEDIR} + ${PC_DBUS_INCLUDE_DIRS} + ${_DBUS_LIBRARY_DIR} + ${DBUS_INCLUDE_DIR} + PATH_SUFFIXES include +) + +SET(DBUS_INCLUDE_DIRS ${DBUS_INCLUDE_DIR} ${DBUS_ARCH_INCLUDE_DIR}) + +INCLUDE(FindPackageHandleStandardArgs) +FIND_PACKAGE_HANDLE_STANDARD_ARGS(DBUS REQUIRED_VARS DBUS_INCLUDE_DIRS DBUS_LIBRARIES) \ No newline at end of file diff --git a/src/PrusaSlicer.cpp b/src/PrusaSlicer.cpp index 9efecb50c..dd4ee3b98 100644 --- a/src/PrusaSlicer.cpp +++ b/src/PrusaSlicer.cpp @@ -51,13 +51,14 @@ #include "slic3r/GUI/GUI.hpp" #include "slic3r/GUI/GUI_App.hpp" #include "slic3r/GUI/3DScene.hpp" + #include "slic3r/GUI/InstanceCheck.hpp" + #include "slic3r/GUI/AppConfig.hpp" #endif /* SLIC3R_GUI */ using namespace Slic3r; int CLI::run(int argc, char **argv) { - #ifdef __WXGTK__ // On Linux, wxGTK has no support for Wayland, and the app crashes on // startup if gtk3 is used. This env var has to be set explicitly to @@ -524,6 +525,16 @@ int CLI::run(int argc, char **argv) #ifdef SLIC3R_GUI // #ifdef USE_WX GUI::GUI_App *gui = new GUI::GUI_App(); + + bool gui_single_instance_setting = gui->app_config->get("single_instance") == "1"; + if (Slic3r::instance_check(argc, argv, gui_single_instance_setting)) { + //TODO: do we have delete gui and other stuff? + return -1; + } + + //gui->app_config = app_config; + //app_config = nullptr; + // gui->autosave = m_config.opt_string("autosave"); GUI::GUI_App::SetInstance(gui); gui->CallAfter([gui, this, &load_configs] { diff --git a/src/PrusaSlicer_app_msvc.cpp b/src/PrusaSlicer_app_msvc.cpp index b3d1e8bb4..712cff687 100644 --- a/src/PrusaSlicer_app_msvc.cpp +++ b/src/PrusaSlicer_app_msvc.cpp @@ -7,6 +7,8 @@ #include #include + + #ifdef SLIC3R_GUI extern "C" { @@ -216,7 +218,6 @@ int APIENTRY wWinMain(HINSTANCE /* hInstance */, HINSTANCE /* hPrevInstance */, int wmain(int argc, wchar_t **argv) { #endif - std::vector argv_extended; argv_extended.emplace_back(argv[0]); diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index a155dff01..43c83fe2b 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -39,6 +39,11 @@ void PrintConfigDef::init_common_params() { ConfigOptionDef* def; + def = this->add("single_instance", coBool); + def->label = L("Single Instance"); + def->mode = comAdvanced; + def->set_default_value(new ConfigOptionBool(false)); + def = this->add("printer_technology", coEnum); def->label = L("Printer technology"); def->tooltip = L("Printer technology"); diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index 2ac8a1ff0..f910c9602 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -161,6 +161,8 @@ set(SLIC3R_GUI_SOURCES GUI/DoubleSlider.hpp GUI/ObjectDataViewModel.cpp GUI/ObjectDataViewModel.hpp + GUI/InstanceCheck.cpp + GUI/InstanceCheck.hpp Utils/Http.cpp Utils/Http.hpp Utils/FixModelByWin10.cpp @@ -195,6 +197,8 @@ if (APPLE) GUI/RemovableDriveManagerMM.mm GUI/RemovableDriveManagerMM.h GUI/Mouse3DHandlerMac.mm + GUI/InstanceCheckMac.mm + GUI/InstanceCheckMac.h ) FIND_LIBRARY(DISKARBITRATION_LIBRARY DiskArbitration) @@ -206,6 +210,10 @@ encoding_check(libslic3r_gui) target_link_libraries(libslic3r_gui libslic3r avrdude cereal imgui GLEW::GLEW OpenGL::GL OpenGL::GLU hidapi libcurl ${wxWidgets_LIBRARIES}) +if (CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_libraries(libslic3r_gui ${DBUS_LIBRARIES}) +endif() + if(APPLE) target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY}) endif() diff --git a/src/slic3r/GUI/AppConfig.cpp b/src/slic3r/GUI/AppConfig.cpp index 616d95147..291949ce9 100644 --- a/src/slic3r/GUI/AppConfig.cpp +++ b/src/slic3r/GUI/AppConfig.cpp @@ -69,6 +69,9 @@ void AppConfig::set_defaults() set("use_retina_opengl", "1"); #endif + if (get("single_instance").empty()) + set("single_instance", "0"); + if (get("remember_output_path").empty()) set("remember_output_path", "1"); diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp index aee71f16e..c04d7ca16 100644 --- a/src/slic3r/GUI/GUI_App.cpp +++ b/src/slic3r/GUI/GUI_App.cpp @@ -50,6 +50,7 @@ #include "UpdateDialogs.hpp" #include "Mouse3DController.hpp" #include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" #ifdef __WXMSW__ #include @@ -209,6 +210,17 @@ static void register_win32_device_notification_event() } return false; }); + + wxWindow::MSWRegisterMessageHandler(WM_COPYDATA, [](wxWindow* win, WXUINT /* nMsg */, WXWPARAM wParam, WXLPARAM lParam) { + + COPYDATASTRUCT* copy_data_structure = { 0 }; + copy_data_structure = (COPYDATASTRUCT*)lParam; + if (copy_data_structure->dwData == 1) { + LPCWSTR arguments = (LPCWSTR)copy_data_structure->lpData; + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(boost::nowide::narrow(arguments)); + } + return true; + }); } #endif // WIN32 @@ -253,7 +265,11 @@ GUI_App::GUI_App() , m_imgui(new ImGuiWrapper()) , m_wizard(nullptr) , m_removable_drive_manager(std::make_unique()) -{} + , m_other_instance_message_handler(std::make_unique()) +{ + //app config initializes early becasuse it is used in instance checking in PrusaSlicer.cpp + this->init_app_config(); +} GUI_App::~GUI_App() { @@ -284,6 +300,30 @@ bool GUI_App::init_opengl() } #endif // ENABLE_NON_STATIC_CANVAS_MANAGER +void GUI_App::init_app_config() +{ + // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. + SetAppName(SLIC3R_APP_KEY); + //SetAppName(SLIC3R_APP_KEY "-beta"); + SetAppDisplayName(SLIC3R_APP_NAME); + + // Set the Slic3r data directory at the Slic3r XS module. + // Unix: ~/ .Slic3r + // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" + // Mac : "~/Library/Application Support/Slic3r" + + if (data_dir().empty()) + set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); + + if (!app_config) + app_config = new AppConfig(); + + // load settings + app_conf_exists = app_config->exists(); + if (app_conf_exists) { + app_config->load(); + } +} bool GUI_App::OnInit() { try { @@ -301,34 +341,14 @@ bool GUI_App::on_init_inner() wxCHECK_MSG(wxDirExists(resources_dir), false, wxString::Format("Resources path does not exist or is not a directory: %s", resources_dir)); - // Profiles for the alpha are stored into the PrusaSlicer-alpha directory to not mix with the current release. - SetAppName(SLIC3R_APP_KEY); -// SetAppName(SLIC3R_APP_KEY "-beta"); - SetAppDisplayName(SLIC3R_APP_NAME); - -// Enable this to get the default Win32 COMCTRL32 behavior of static boxes. + // Enable this to get the default Win32 COMCTRL32 behavior of static boxes. // wxSystemOptions::SetOption("msw.staticbox.optimized-paint", 0); -// Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible -// performance when working on high resolution multi-display setups. + // Enable this to disable Windows Vista themes for all wxNotebooks. The themes seem to lead to terrible + // performance when working on high resolution multi-display setups. // wxSystemOptions::SetOption("msw.notebook.themed-background", 0); // Slic3r::debugf "wxWidgets version %s, Wx version %s\n", wxVERSION_STRING, wxVERSION; - - // Set the Slic3r data directory at the Slic3r XS module. - // Unix: ~/ .Slic3r - // Windows : "C:\Users\username\AppData\Roaming\Slic3r" or "C:\Documents and Settings\username\Application Data\Slic3r" - // Mac : "~/Library/Application Support/Slic3r" - if (data_dir().empty()) - set_data_dir(wxStandardPaths::Get().GetUserDataDir().ToUTF8().data()); - - app_config = new AppConfig(); - - // load settings - app_conf_exists = app_config->exists(); - if (app_conf_exists) { - app_config->load(); - } - + std::string msg = Http::tls_global_init(); wxRichMessageDialog dlg(nullptr, @@ -407,6 +427,8 @@ bool GUI_App::on_init_inner() if (! plater_) return; + //m_other_instance_message_handler->report(); + if (app_config->dirty() && app_config->get("autosave") == "1") app_config->save(); diff --git a/src/slic3r/GUI/GUI_App.hpp b/src/slic3r/GUI/GUI_App.hpp index bf75eaaa6..b2fcef48b 100644 --- a/src/slic3r/GUI/GUI_App.hpp +++ b/src/slic3r/GUI/GUI_App.hpp @@ -35,6 +35,7 @@ class PrintHostJobQueue; namespace GUI{ class RemovableDriveManager; +class OtherInstanceMessageHandler; enum FileType { FT_STL, @@ -108,7 +109,7 @@ class GUI_App : public wxApp std::unique_ptr m_imgui; std::unique_ptr m_printhost_job_queue; ConfigWizard* m_wizard; // Managed by wxWindow tree - + std::unique_ptr m_other_instance_message_handler; public: bool OnInit() override; bool initialized() const { return m_initialized; } @@ -196,6 +197,7 @@ public: std::vector tabs_list; RemovableDriveManager* removable_drive_manager() { return m_removable_drive_manager.get(); } + OtherInstanceMessageHandler* other_instance_message_handler() { return m_other_instance_message_handler.get(); } ImGuiWrapper* imgui() { return m_imgui.get(); } @@ -211,6 +213,7 @@ public: private: bool on_init_inner(); + void init_app_config(); void window_pos_save(wxTopLevelWindow* window, const std::string &name); void window_pos_restore(wxTopLevelWindow* window, const std::string &name, bool default_maximized = false); void window_pos_sanitize(wxTopLevelWindow* window); diff --git a/src/slic3r/GUI/InstanceCheck.cpp b/src/slic3r/GUI/InstanceCheck.cpp new file mode 100644 index 000000000..e408ce118 --- /dev/null +++ b/src/slic3r/GUI/InstanceCheck.cpp @@ -0,0 +1,495 @@ +#include "GUI_App.hpp" +#include "InstanceCheck.hpp" + +#include "boost/nowide/convert.hpp" +#include +#include + +#include +#include + +#if __linux__ +#include /* Pull in all of D-Bus headers. */ +#endif //__linux__ + +namespace Slic3r { +namespace instance_check_internal +{ + struct CommandLineAnalysis + { + bool should_send; + std::string cl_string; + }; + static CommandLineAnalysis process_command_line(int argc, char** argv) //d:\3dmodels\Klapka\Klapka.3mf + { + CommandLineAnalysis ret { false }; + if (argc < 2) + return ret; + ret.cl_string = argv[0]; + for (size_t i = 1; i < argc; i++) { + std::string token = argv[i]; + if (token == "--single-instance") { + ret.should_send = true; + } else { + ret.cl_string += " "; + ret.cl_string += token; + } + } + BOOST_LOG_TRIVIAL(debug) << "single instance: "<< ret.should_send << ". other params: " << ret.cl_string; + return ret; + } +} //namespace instance_check_internal + +#if _WIN32 + +namespace instance_check_internal +{ + static HWND l_prusa_slicer_hwnd; + static BOOL CALLBACK EnumWindowsProc(_In_ HWND hwnd, _In_ LPARAM lParam) + { + //checks for other instances of prusaslicer, if found brings it to front and return false to stop enumeration and quit this instance + //search is done by classname(wxWindowNR is wxwidgets thing, so probably not unique) and name in window upper panel + //other option would be do a mutex and check for its existence + TCHAR wndText[1000]; + TCHAR className[1000]; + GetClassName(hwnd, className, 1000); + GetWindowText(hwnd, wndText, 1000); + std::wstring classNameString(className); + std::wstring wndTextString(wndText); + if (wndTextString.find(L"PrusaSlicer") != std::wstring::npos && classNameString == L"wxWindowNR") { + l_prusa_slicer_hwnd = hwnd; + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + SetForegroundWindow(hwnd); + return false; + } + return true; + } + static void send_message(const HWND hwnd) + { + LPWSTR command_line_args = GetCommandLine(); + //Create a COPYDATASTRUCT to send the information + //cbData represents the size of the information we want to send. + //lpData represents the information we want to send. + //dwData is an ID defined by us(this is a type of ID different than WM_COPYDATA). + COPYDATASTRUCT data_to_send = { 0 }; + data_to_send.dwData = 1; + data_to_send.cbData = sizeof(TCHAR) * (wcslen(command_line_args) + 1); + data_to_send.lpData = command_line_args; + + SendMessage(hwnd, WM_COPYDATA, 0, (LPARAM)&data_to_send); + } +} //namespace instance_check_internal + +bool instance_check(int argc, char** argv, bool app_config_single_instance) +{ + instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv); + if (cla.should_send || app_config_single_instance) { + // Call EnumWidnows with own callback. cons: Based on text in the name of the window and class name which is generic. + if (!EnumWindows(instance_check_internal::EnumWindowsProc, 0)) { + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate."; + instance_check_internal::send_message(instance_check_internal::l_prusa_slicer_hwnd); + return true; + } + } + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set."; + return false; +} + +#elif defined(__APPLE__) + +namespace instance_check_internal +{ + static int get_lock() + { + struct flock fl; + int fdlock; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 1; + + if ((fdlock = open("/tmp/prusaslicer.lock", O_WRONLY | O_CREAT, 0666)) == -1) + return 0; + + if (fcntl(fdlock, F_SETLK, &fl) == -1) + return 0; + + return 1; + } +} //namespace instance_check_internal + +bool instance_check(int argc, char** argv, bool app_config_single_instance) +{ + instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv); + if (!instance_check_internal::get_lock() && (cla.should_send || app_config_single_instance)) { + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate."; + send_message_mac(cla.cl_string); + return true; + } + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set."; + return false; +} + +#elif defined(__linux__) + +namespace instance_check_internal +{ + static int get_lock() + { + struct flock fl; + int fdlock; + fl.l_type = F_WRLCK; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 1; + + if ((fdlock = open("/tmp/prusaslicer.lock", O_WRONLY | O_CREAT, 0666)) == -1) + return 0; + + if (fcntl(fdlock, F_SETLK, &fl) == -1) + return 0; + + return 1; + } + + static void send_message(std::string message_text) + { + DBusMessage* msg; + DBusMessageIter args; + DBusConnection* conn; + DBusError err; + dbus_uint32_t serial = 0; + const char* sigval = message_text.c_str(); + std::string interface_name = "com.prusa3d.prusaslicer.InstanceCheck"; + std::string method_name = "AnotherInstace"; + std::string object_name = "/com/prusa3d/prusaslicer/InstanceCheck"; + + + // initialise the error value + dbus_error_init(&err); + + // connect to bus, and check for errors (use SESSION bus everywhere!) + conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection Error. Message to another instance wont be send."; + BOOST_LOG_TRIVIAL(error) << "DBus Connection Error: "<< err.message; + dbus_error_free(&err); + return; + } + if (NULL == conn) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection is NULL. Message to another instance wont be send."; + return; + } + + //some sources do request interface ownership before constructing msg but i think its wrong. + + //create new method call message + msg = dbus_message_new_method_call(interface_name.c_str(), object_name.c_str(), interface_name.c_str(), method_name.c_str()); + if (NULL == msg) { + BOOST_LOG_TRIVIAL(error) << "DBus Message is NULL. Message to another instance wont be send."; + dbus_connection_unref(conn); + return; + } + //the AnotherInstace method is not sending reply. + dbus_message_set_no_reply(msg, TRUE); + + //append arguments to message + if (!dbus_message_append_args(msg, DBUS_TYPE_STRING, &sigval, DBUS_TYPE_INVALID)) { + BOOST_LOG_TRIVIAL(error) << "Ran out of memory while constructing args for DBus message. Message to another instance wont be send."; + dbus_message_unref(msg); + dbus_connection_unref(conn); + return; + } + + // send the message and flush the connection + if (!dbus_connection_send(conn, msg, &serial)) { + BOOST_LOG_TRIVIAL(error) << "Ran out of memory while sending DBus message."; + dbus_message_unref(msg); + dbus_connection_unref(conn); + return; + } + dbus_connection_flush(conn); + + BOOST_LOG_TRIVIAL(trace) << "DBus message sent."; + + // free the message and close the connection + dbus_message_unref(msg); + dbus_connection_unref(conn); + } +} //namespace instance_check_internal + +bool instance_check(int argc, char** argv, bool app_config_single_instance) +{ + instance_check_internal::CommandLineAnalysis cla = instance_check_internal::process_command_line(argc, argv); + if (!instance_check_internal::get_lock() && (cla.should_send || app_config_single_instance)) { + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance found. This instance will terminate."; + instance_check_internal::send_message(cla.cl_string); + return true; + } + BOOST_LOG_TRIVIAL(info) << "instance check: Another instance not found or single-instance not set."; + return false; +} +#endif //_WIN32/__APPLE__/__linux__ + + + +namespace GUI { + +wxDEFINE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); +wxDEFINE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); + +void OtherInstanceMessageHandler::init(wxEvtHandler* callback_evt_handler) +{ + assert(!m_initialized); + assert(m_callback_evt_handler == nullptr); + if (m_initialized) + return; + + m_initialized = true; + m_callback_evt_handler = callback_evt_handler; + +#if _WIN32 + //create_listener_window(); +#endif //_WIN32 + +#if defined(__APPLE__) + this->register_for_messages(); +#endif //__APPLE__ + +#ifdef BACKGROUND_MESSAGE_LISTENER + m_thread = boost::thread((boost::bind(&OtherInstanceMessageHandler::listen, this))); +#endif //BACKGROUND_MESSAGE_LISTENER +} +void OtherInstanceMessageHandler::shutdown() +{ + BOOST_LOG_TRIVIAL(debug) << "message handler shutdown()."; + assert(m_initialized); + if (m_initialized) { +#if __APPLE__ + //delete macos implementation + this->unregister_for_messages(); +#endif //__APPLE__ +#ifdef BACKGROUND_MESSAGE_LISTENER + if (m_thread.joinable()) { + // Stop the worker thread, if running. + { + // Notify the worker thread to cancel wait on detection polling. + std::lock_guard lck(m_thread_stop_mutex); + m_stop = true; + } + m_thread_stop_condition.notify_all(); + // Wait for the worker thread to stop. + m_thread.join(); + m_stop = false; + } +#endif //BACKGROUND_MESSAGE_LISTENER + m_initialized = false; + } +} + +namespace MessageHandlerInternal +{ + // returns ::path to possible model or empty ::path if input string is not existing path + static boost::filesystem::path get_path(const std::string possible_path) + { + BOOST_LOG_TRIVIAL(debug) << "message part: " << possible_path; + + if (possible_path.empty() || possible_path.size() < 3) { + BOOST_LOG_TRIVIAL(debug) << "empty"; + return boost::filesystem::path(); + } + if (boost::filesystem::exists(possible_path)) { + BOOST_LOG_TRIVIAL(debug) << "is path"; + return boost::filesystem::path(possible_path); + } else if (possible_path[0] == '\"') { + if(boost::filesystem::exists(possible_path.substr(1, possible_path.size() - 2))) { + BOOST_LOG_TRIVIAL(debug) << "is path in quotes"; + return boost::filesystem::path(possible_path.substr(1, possible_path.size() - 2)); + } + } + BOOST_LOG_TRIVIAL(debug) << "is NOT path"; + return boost::filesystem::path(); + } +} //namespace MessageHandlerInternal + +void OtherInstanceMessageHandler::handle_message(const std::string message) { + std::vector paths; + auto next_space = message.find(' '); + size_t last_space = 0; + int counter = 0; + + BOOST_LOG_TRIVIAL(info) << "message from other instance: " << message; + + while (next_space != std::string::npos) + { + if (counter != 0) { + const std::string possible_path = message.substr(last_space, next_space - last_space); + boost::filesystem::path p = MessageHandlerInternal::get_path(possible_path); + if(!p.string().empty()) + paths.emplace_back(p); + } + last_space = next_space; + next_space = message.find(' ', last_space + 1); + counter++; + } + if (counter != 0 ) { + boost::filesystem::path p = MessageHandlerInternal::get_path(message.substr(last_space + 1)); + if (!p.string().empty()) + paths.emplace_back(p); + } + if (!paths.empty()) { + //wxEvtHandler* evt_handler = wxGetApp().plater(); //assert here? + //if (evt_handler) { + wxPostEvent(m_callback_evt_handler, LoadFromOtherInstanceEvent(GUI::EVT_LOAD_MODEL_OTHER_INSTANCE, std::vector(std::move(paths)))); + //} + } +} + +#ifdef BACKGROUND_MESSAGE_LISTENER + +namespace MessageHandlerDBusInternal +{ + //reply to introspect makes our DBus object visible for other programs like D-Feet + static void respond_to_introspect(DBusConnection *connection, DBusMessage *request) + { + DBusMessage *reply; + const char *introspection_data = + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " "; + + reply = dbus_message_new_method_return(request); + dbus_message_append_args(reply, DBUS_TYPE_STRING, &introspection_data, DBUS_TYPE_INVALID); + dbus_connection_send(connection, reply, NULL); + dbus_message_unref(reply); + } + //method AnotherInstance receives message from another PrusaSlicer instance + static void handle_method_another_instance(DBusConnection *connection, DBusMessage *request) + { + DBusError err; + char* text= ""; + wxEvtHandler* evt_handler; + + dbus_error_init(&err); + dbus_message_get_args(request, &err, DBUS_TYPE_STRING, &text, DBUS_TYPE_INVALID); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(trace) << "Dbus method AnotherInstance received with wrong arguments."; + dbus_error_free(&err); + return; + } + wxGetApp().other_instance_message_handler()->handle_message(text); + + evt_handler = wxGetApp().plater(); + if (evt_handler) { + wxPostEvent(evt_handler, InstanceGoToFrontEvent(EVT_INSTANCE_GO_TO_FRONT)); + } + } + //every dbus message received comes here + static DBusHandlerResult handle_dbus_object_message(DBusConnection *connection, DBusMessage *message, void *user_data) + { + const char* interface_name = dbus_message_get_interface(message); + const char* member_name = dbus_message_get_member(message); + + BOOST_LOG_TRIVIAL(trace) << "DBus message received: interface: " << interface_name << ", member: " << member_name; + + if (0 == strcmp("org.freedesktop.DBus.Introspectable", interface_name) && 0 == strcmp("Introspect", member_name)) { + respond_to_introspect(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } else if (0 == strcmp("com.prusa3d.prusaslicer.InstanceCheck", interface_name) && 0 == strcmp("AnotherInstace", member_name)) { + handle_method_another_instance(connection, message); + return DBUS_HANDLER_RESULT_HANDLED; + } + return DBUS_HANDLER_RESULT_NOT_YET_HANDLED; + } +} //namespace MessageHandlerDBusInternal + +void OtherInstanceMessageHandler::listen() +{ + DBusConnection* conn; + DBusError err; + int name_req_val; + DBusObjectPathVTable vtable; + std::string interface_name = "com.prusa3d.prusaslicer.InstanceCheck"; + std::string object_name = "/com/prusa3d/prusaslicer/InstanceCheck"; + + dbus_error_init(&err); + + // connect to the bus and check for errors (use SESSION bus everywhere!) + conn = dbus_bus_get(DBUS_BUS_SESSION, &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating."; + dbus_error_free(&err); + return; + } + if (NULL == conn) { + BOOST_LOG_TRIVIAL(error) << "DBus Connection is NULL. Dbus Messages listening terminating."; + return; + } + + // request our name on the bus and check for errors + name_req_val = dbus_bus_request_name(conn, interface_name.c_str(), DBUS_NAME_FLAG_REPLACE_EXISTING , &err); + if (dbus_error_is_set(&err)) { + BOOST_LOG_TRIVIAL(error) << "DBus Request name Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating."; + dbus_error_free(&err); + dbus_connection_unref(conn); + return; + } + if (DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER != name_req_val) { + BOOST_LOG_TRIVIAL(error) << "Not primary owner of DBus name - probably another PrusaSlicer instance is running."; + BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating."; + dbus_connection_unref(conn); + return; + } + + // Set callbacks. Unregister function should not be nessary. + vtable.message_function = MessageHandlerDBusInternal::handle_dbus_object_message; + vtable.unregister_function = NULL; + + // register new object - this is our access to DBus + dbus_connection_try_register_object_path(conn, object_name.c_str(), &vtable, NULL, &err); + if ( dbus_error_is_set(&err) ) { + BOOST_LOG_TRIVIAL(error) << "DBus Register object Error: "<< err.message; + BOOST_LOG_TRIVIAL(error) << "Dbus Messages listening terminating."; + dbus_connection_unref(conn); + dbus_error_free(&err); + return; + } + + BOOST_LOG_TRIVIAL(trace) << "Dbus object registered. Starting listening for messages."; + + for (;;) { + // Wait for 1 second + // Cancellable. + { + std::unique_lock lck(m_thread_stop_mutex); + m_thread_stop_condition.wait_for(lck, std::chrono::seconds(1), [this] { return m_stop; }); + } + if (m_stop) + // Stop the worker thread. + + break; + //dispatch should do all the work with incoming messages + //second parameter is blocking time that funciton waits for new messages + //that is handled here with our own event loop above + dbus_connection_read_write_dispatch(conn, 0); + } + + dbus_connection_unref(conn); +} +#endif //BACKGROUND_MESSAGE_LISTENER +} // namespace GUI +} // namespace Slic3r diff --git a/src/slic3r/GUI/InstanceCheck.hpp b/src/slic3r/GUI/InstanceCheck.hpp new file mode 100644 index 000000000..4207296a0 --- /dev/null +++ b/src/slic3r/GUI/InstanceCheck.hpp @@ -0,0 +1,91 @@ +#ifndef slic3r_InstanceCheck_hpp_ +#define slic3r_InstanceCheck_hpp_ + +#include "Event.hpp" + +#if _WIN32 +#include +#endif //_WIN32 + +#include + +#include + +#include +#include +#include + + + +namespace Slic3r { +// checks for other running instances and sends them argv, +// if there is --single-instance argument or AppConfig is set to single_instance=1 +// returns true if this instance should terminate +bool instance_check(int argc, char** argv, bool app_config_single_instance); + +#if __APPLE__ +// apple implementation of inner functions of instance_check +// in InstanceCheckMac.mm +void send_message_mac(const std::string msg); +#endif //__APPLE__ + +namespace GUI { + +#if __linux__ + #define BACKGROUND_MESSAGE_LISTENER +#endif // __linux__ + +using LoadFromOtherInstanceEvent = Event>; +wxDECLARE_EVENT(EVT_LOAD_MODEL_OTHER_INSTANCE, LoadFromOtherInstanceEvent); + +using InstanceGoToFrontEvent = SimpleEvent; +wxDECLARE_EVENT(EVT_INSTANCE_GO_TO_FRONT, InstanceGoToFrontEvent); + +class OtherInstanceMessageHandler +{ +public: + OtherInstanceMessageHandler() = default; + OtherInstanceMessageHandler(OtherInstanceMessageHandler const&) = delete; + void operator=(OtherInstanceMessageHandler const&) = delete; + ~OtherInstanceMessageHandler() { assert(!m_initialized); } + + // inits listening, on each platform different. On linux starts background thread + void init(wxEvtHandler* callback_evt_handler); + // stops listening, on linux stops the background thread + void shutdown(); + + //finds paths to models in message(= command line arguments, first should be prusaSlicer executable) + //and sends them to plater via LoadFromOtherInstanceEvent + //security of messages: from message all existing paths are proccesed to load model + // win32 - anybody who has hwnd can send message. + // mac - anybody who posts notification with name:@"OtherPrusaSlicerTerminating" + // linux - instrospectable on dbus + void handle_message(const std::string message); +private: + bool m_initialized { false }; + wxEvtHandler* m_callback_evt_handler { nullptr }; + +#ifdef BACKGROUND_MESSAGE_LISTENER + //worker thread to listen incoming dbus communication + boost::thread m_thread; + std::condition_variable m_thread_stop_condition; + mutable std::mutex m_thread_stop_mutex; + bool m_stop{ false }; + bool m_start{ true }; + + // background thread method + void listen(); +#endif //BACKGROUND_MESSAGE_LISTENER + +#if __APPLE__ + //implemented at InstanceCheckMac.mm + void register_for_messages(); + void unregister_for_messages(); + // Opaque pointer to RemovableDriveManagerMM + void* m_impl_osx; +#endif //__APPLE__ + +}; +} // namespace GUI +} // namespace Slic3r +#endif // slic3r_InstanceCheck_hpp_ diff --git a/src/slic3r/GUI/InstanceCheckMac.h b/src/slic3r/GUI/InstanceCheckMac.h new file mode 100644 index 000000000..9931bb679 --- /dev/null +++ b/src/slic3r/GUI/InstanceCheckMac.h @@ -0,0 +1,8 @@ +#import + +@interface OtherInstanceMessageHandlerMac : NSObject + +-(instancetype) init; +-(void) add_observer; +-(void) message_update:(NSNotification *)note; +@end diff --git a/src/slic3r/GUI/InstanceCheckMac.mm b/src/slic3r/GUI/InstanceCheckMac.mm new file mode 100644 index 000000000..cbc833c79 --- /dev/null +++ b/src/slic3r/GUI/InstanceCheckMac.mm @@ -0,0 +1,68 @@ +#import "InstanceCheck.hpp" +#import "InstanceCheckMac.h" +#import "GUI_App.hpp" + +@implementation OtherInstanceMessageHandlerMac + +-(instancetype) init +{ + self = [super init]; + return self; +} +-(void)add_observer +{ + NSLog(@"adding observer"); + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(message_update:) name:@"OtherPrusaSlicerInstanceMessage" object:nil suspensionBehavior:NSNotificationSuspensionBehaviorDeliverImmediately]; +} + +-(void)message_update:(NSNotification *)msg +{ + //NSLog(@"recieved msg %@", msg); + //demiaturize all windows + for(NSWindow* win in [NSApp windows]) + { + if([win isMiniaturized]) + { + [win deminiaturize:self]; + } + } + //bring window to front + [[NSApplication sharedApplication] activateIgnoringOtherApps : YES]; + //pass message + Slic3r::GUI::wxGetApp().other_instance_message_handler()->handle_message(std::string([msg.userInfo[@"data"] UTF8String])); +} + +@end + +namespace Slic3r { + +void send_message_mac(const std::string msg) +{ + NSString *nsmsg = [NSString stringWithCString:msg.c_str() encoding:[NSString defaultCStringEncoding]]; + //NSLog(@"sending msg %@", nsmsg); + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:@"OtherPrusaSlicerInstanceMessage" object:nil userInfo:[NSDictionary dictionaryWithObject:nsmsg forKey:@"data"] deliverImmediately:YES]; +} + +namespace GUI { +void OtherInstanceMessageHandler::register_for_messages() +{ + m_impl_osx = [[OtherInstanceMessageHandlerMac alloc] init]; + if(m_impl_osx) { + [m_impl_osx add_observer]; + } +} + +void OtherInstanceMessageHandler::unregister_for_messages() +{ + //NSLog(@"unreegistering other instance messages"); + if (m_impl_osx) { + [m_impl_osx release]; + m_impl_osx = nullptr; + } else { + NSLog(@"unreegister not required"); + } +} +}//namespace GUI +}//namespace Slicer + + diff --git a/src/slic3r/GUI/MainFrame.cpp b/src/slic3r/GUI/MainFrame.cpp index f7d7a6cac..21aad8987 100644 --- a/src/slic3r/GUI/MainFrame.cpp +++ b/src/slic3r/GUI/MainFrame.cpp @@ -26,6 +26,7 @@ #include "GUI_ObjectList.hpp" #include "Mouse3DController.hpp" #include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" #include "I18N.hpp" #include @@ -234,7 +235,8 @@ void MainFrame::shutdown() // Stop the background thread of the removable drive manager, so that no new updates will be sent to the Plater. wxGetApp().removable_drive_manager()->shutdown(); - + //stop listening for messages from other instances + wxGetApp().other_instance_message_handler()->shutdown(); // Save the slic3r.ini.Usually the ini file is saved from "on idle" callback, // but in rare cases it may not have been called yet. wxGetApp().app_config->save(); diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp index db9e7e59a..d909442b9 100644 --- a/src/slic3r/GUI/Plater.cpp +++ b/src/slic3r/GUI/Plater.cpp @@ -74,6 +74,8 @@ #include "../Utils/FixModelByWin10.hpp" #include "../Utils/UndoRedo.hpp" #include "RemovableDriveManager.hpp" +#include "InstanceCheck.hpp" + #if ENABLE_NON_STATIC_CANVAS_MANAGER #ifdef __APPLE__ #include "Gizmos/GLGizmosManager.hpp" @@ -1946,6 +1948,28 @@ Plater::priv::priv(Plater *q, MainFrame *main_frame) // Initialize the Undo / Redo stack with a first snapshot. this->take_snapshot(_L("New Project")); + + this->q->Bind(EVT_LOAD_MODEL_OTHER_INSTANCE, [this](LoadFromOtherInstanceEvent &evt) { + BOOST_LOG_TRIVIAL(debug) << "received load from other instance event "; + this->load_files(evt.data, true, true); + }); + this->q->Bind(EVT_INSTANCE_GO_TO_FRONT, [this](InstanceGoToFrontEvent &) { + BOOST_LOG_TRIVIAL(debug) << "prusaslicer window going forward"; + //this code maximize app window on Fedora + wxGetApp().mainframe->Iconize(false); + if (wxGetApp().mainframe->IsMaximized()) + wxGetApp().mainframe->Maximize(true); + else + wxGetApp().mainframe->Maximize(false); + //this code (without code above) maximize window on Ubuntu + wxGetApp().mainframe->Restore(); + wxGetApp().GetTopWindow()->SetFocus(); // focus on my window + wxGetApp().GetTopWindow()->Raise(); // bring window to front + wxGetApp().GetTopWindow()->Show(true); // show the window + + }); + wxGetApp().other_instance_message_handler()->init(this->q); + } Plater::priv::~priv() diff --git a/src/slic3r/GUI/Preferences.cpp b/src/slic3r/GUI/Preferences.cpp index 1299d9a48..e2bccb1c7 100644 --- a/src/slic3r/GUI/Preferences.cpp +++ b/src/slic3r/GUI/Preferences.cpp @@ -100,6 +100,13 @@ void PreferencesDialog::build() option = Option (def,"show_incompatible_presets"); m_optgroup_general->append_single_option_line(option); + def.label = L("Single Instance"); + def.type = coBool; + def.tooltip = L("If this is enabled, when staring PrusaSlicer and another instance is running, that instance will be reactivated instead."); + def.set_default_value(new ConfigOptionBool{ app_config->has("single_instance") ? app_config->get("single_instance") == "1" : false }); + option = Option(def, "single_instance"); + m_optgroup_general->append_single_option_line(option); + #if __APPLE__ def.label = L("Use Retina resolution for the 3D scene"); def.type = coBool; @@ -177,6 +184,8 @@ void PreferencesDialog::accept() app_config->set(it->first, it->second); } + app_config->save(); + EndModal(wxID_OK); // Nothify the UI to update itself from the ini file.