tryphotino / photino.NET

https://tryphotino.io
Apache License 2.0
913 stars 74 forks source link

Running a process after a Photino window has been opened never exits (Linux only) #74

Closed vvollers closed 2 years ago

vvollers commented 3 years ago

On Linux, when you are trying to start a process after a photino (Webkit) window has been opened, it executes but never returns (Process.WaitForExit never returns and the Process.Exited event is never called).

here is a minimal example program to demonstrate the problem (dotnet 6, but it is the same under 5)

MinimalProcessExample.zip

Expected behaviour: 1) run process once, return 2) run process again, return 3) open photino window, close photino window 4) run process for a third time, return 5) program finishes

Observed behaviour: 1) run process once, return 2) run process again, return 3) open photino window, close photino window 4) run process for a third time, now it does not return 5) program never finishes

The code (with a different test command for windows) functions as expected (correctly) under both MacOS and Windows, but under Linux the problem exists. In this minimal example we run a process after a photino window has been closed, but the same behavior occurs if you do it in a messagehandler. So I believe the Linux webkit code interferes somehow? I've looked at the Photino.Native code but I could not determine the cause of the issue.

using PhotinoNET;
using System.Diagnostics;

// First Run, works as expected
TestProcess();

// Second Run, just to be sure
TestProcess();

var window = new PhotinoWindow();
window
    .SetTitle("Process Test")
    .SetMaximized(true)
    .Center()
    .Load("https://www.google.com");
window.WaitForClose();

// After Photino, the process exit code is never called?
TestProcess();

// Never gets here...
Console.WriteLine("Program is done!");

// Start a child process
void TestProcess() { 
    var cmd = "ls";
    var cmdArgs = "";

    var process = new Process
    {
        EnableRaisingEvents = true,
        StartInfo = new ProcessStartInfo
        {
            CreateNoWindow = true,
            ErrorDialog = false,
            FileName = cmd,
            Arguments = cmdArgs,
            RedirectStandardError = true,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            UseShellExecute = false,
            WorkingDirectory = "."
        },
    };

    Console.WriteLine($"Starting process '{cmd} {cmdArgs}'");
    process.Start();

    process.Exited += (sender, args) =>
    {
        Console.WriteLine($"Proces exited event, exit code {process.ExitCode}");
    };

    string output = process.StandardOutput.ReadToEnd();

    Console.WriteLine("Awaiting process exit...");
    process.WaitForExit();  // After Photino window, never returns?
    Console.WriteLine("Process exited");
}
MikeYeager commented 3 years ago

We will look into this as soon as we can find some time.

vvollers commented 3 years ago

Allright, thank you!

vvollers commented 2 years ago

I've created a similar test using SpiderEye (https://github.com/JBildstein/SpiderEye)

This has the same behavior, so it might be a weird interaction between WebKit2GTK and dotnet itself?

here is the sourcecode for the SpiderEye version of the test: SpiderTest.zip

vvollers commented 2 years ago

Ok, I've just tested using GtkSharp (https://github.com/GtkSharp/GtkSharp)

This is interesting. If you run the GtkSharp Samples (which includes a WebKit widget), the test PASSES if you do anything except the webkit widget test.

(Run Samples, click on any gui sample in the program, then exit the program. The program will show you the process has started and exited as expected)

However, if you click on the webkit widget sample, the test FAILS again (same as Photino & Spidereye).

(Run Samples, click the webkit sample in the program, then exit the program. The program will start a process and keep waiting for it to exit)

Conclusion so far: its definitely something with the interaction between WebKit and dotnet. I'll keep on experimenting...

Here is the sourcecode for the GtkSharp test (note: please follow the build instructions of gtksharp, then you can run the examples with "dotnet run" in the "Source\Samples" directory. I've only modified "Program.cs" of the GtkSharp Samples to include the Process Test: Webkittest-GtkSharp-develop.zip

ptupitsyn commented 2 years ago

Same here, .NET 5, Ubuntu 20. Added Photino to an existing app, all Process usages now hang.

ptupitsyn commented 2 years ago

I've had this problem before in another project involving .NET interop with native libraries, where SIGCHLD handler was replaced by a library, causing .NET Process API to break and leave child processes in zombie state after exit.

I've googled Gtk webkit sigchld and found a couple seemingly relevant links:

vvollers commented 2 years ago

Mh, thank you @ptupitsyn, that is some good info!

the fix (or really, workaround) might be as simple as

photino.Native/Photino.Native/Photino.Linux.cpp

void Photino::NavigateToUrl(AutoString url)
{
+  struct sigaction old_action;
+  sigaction (SIGCHLD, NULL, &old_action);
  webkit_web_view_load_uri(WEBKIT_WEB_VIEW(_webview), url);
+  sigaction (SIGCHLD, &old_action, NULL);
}

I'll try and see if I can test that soon.

vvollers commented 2 years ago

It wasn't exactly the above, but applying the same workaround to another location worked! (verified with patching my own project and the Photino.Native/Photino.Test project).

I'll submit a PR and also update the WebkitGTK & GtkSharp issues so they can hopefully add a similar patch as well.

photino.Native/Photino.Native/Photino.Linux.cpp

void Photino::Show()
{
    if (!_webview)
    {
+       struct sigaction old_action;
+       sigaction (SIGCHLD, NULL, &old_action);
        WebKitUserContentManager* contentManager = webkit_user_content_manager_new();
        _webview = webkit_web_view_new_with_user_content_manager(contentManager);

        //https://webkit.org/reference/webkit2gtk/unstable/WebKitSettings.html#WebKitSettings--allow-file-access-from-file-urls
        WebKitSettings* settings = webkit_web_view_get_settings(WEBKIT_WEB_VIEW(_webview));

        webkit_settings_set_allow_file_access_from_file_urls(settings, TRUE);
        webkit_settings_set_allow_modal_dialogs(settings, TRUE);
        webkit_settings_set_allow_top_navigation_to_data_urls(settings, TRUE);
        webkit_settings_set_allow_universal_access_from_file_urls(settings, TRUE);

        webkit_settings_set_enable_back_forward_navigation_gestures(settings, TRUE);
        webkit_settings_set_enable_caret_browsing(settings, TRUE);
        webkit_settings_set_enable_developer_extras(settings, _devToolsEnabled);
        webkit_settings_set_enable_media_capabilities(settings, TRUE);
        webkit_settings_set_enable_media_stream(settings, TRUE);

        webkit_settings_set_javascript_can_access_clipboard(settings, TRUE);
        webkit_settings_set_javascript_can_open_windows_automatically(settings, TRUE);

        gtk_container_add(GTK_CONTAINER(_window), _webview);

        WebKitUserScript* script = webkit_user_script_new(
            "window.__receiveMessageCallbacks = [];"
            "window.__dispatchMessageCallback = function(message) {"
            "   window.__receiveMessageCallbacks.forEach(function(callback) { callback(message); });"
            "};"
            "window.external = {"
            "   sendMessage: function(message) {"
            "       window.webkit.messageHandlers.Photinointerop.postMessage(message);"
            "   },"
            "   receiveMessage: function(callback) {"
            "       window.__receiveMessageCallbacks.push(callback);"
            "   }"
            "};", WEBKIT_USER_CONTENT_INJECT_ALL_FRAMES, WEBKIT_USER_SCRIPT_INJECT_AT_DOCUMENT_START, NULL, NULL);
        webkit_user_content_manager_add_script(contentManager, script);
        webkit_user_script_unref(script);

        g_signal_connect(contentManager, "script-message-received::Photinointerop",
            G_CALLBACK(HandleWebMessage), (void*)_webMessageReceivedCallback);
        webkit_user_content_manager_register_script_message_handler(contentManager, "Photinointerop");

        if (_startUrl != NULL)
            Photino::NavigateToUrl(_startUrl);
        else if (_startString != NULL)
            Photino::NavigateToString(_startString);
        else
        {
            GtkWidget* dialog = gtk_message_dialog_new(
                nullptr
                , GTK_DIALOG_DESTROY_WITH_PARENT
                , GTK_MESSAGE_ERROR
                , GTK_BUTTONS_CLOSE
                , "Neither StartUrl not StartString was specified");
            gtk_dialog_run(GTK_DIALOG(dialog));
            gtk_widget_destroy(dialog);
            exit(0);
        }
+       sigaction (SIGCHLD, &old_action, NULL);
    }

    gtk_widget_show_all(_window);
}
MikeYeager commented 2 years ago

Thank you for the fix. We've merged your pull request and it will be in the next build.