How to guarantee that implementation of CefStringVisitor in JCEF should be called before next statement of cefBrowser.getSource

Answered

Below is a simplified implementation of CefStringVisitor :-
 

package com.example;

import org.cef.callback.CefStringVisitor;

import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

class SourceCefStringVisitor implements CefStringVisitor {

    private List<String> lines = new Vector<>() ;

    @Override
    public void visit(String string) {
        lines.add(Thread.currentThread().getName() + ":" + string) ;
    }

    public String browserPageSource(){
        return lines.stream().collect(Collectors.joining("\n"));
    }
}

And it is getting called from below action :-

package com.example;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import org.cef.browser.CefBrowser;
import org.jetbrains.annotations.NotNull;


public class SourceVisitorAction extends AnAction {
    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {

        Project project = e.getProject();
        BrowserService browserService = project.getService(BrowserService.class);

        CefBrowser cefBrowser = browserService.getBrowser().getCefBrowser();

        SourceCefStringVisitor sourceVisitor = new SourceCefStringVisitor();

        cefBrowser.getSource( sourceVisitor );

		//here expectation is to get the source but empty string is found
        Messages.showInfoMessage(sourceVisitor.browserPageSource(), "browser page source");
        Messages.showInfoMessage(Thread.currentThread().getName() + ":" + "end of action", "SourceVisitorAction");
    }
}

A very simplified running example can be found here : https://github.com/moglideveloper/cef-string-visitor-issue

Kindly refer ReadMe for full explanation.

0
4 comments
Official comment

The callback passed to the getSource won't be invoked immediately, but some time later. Generally, you can't be sure when (and on what thread) it will be called. 

Why don't you just continue your logic inside the callback? E.g.:
cefBrowser.getSource(string -> {
    ApplicationManager.getApplication().invokeLater(() -> {
        Messages.showInfoMessage(string, "browser page source");
    });
});

Or you can utilize some synchronization mechanism if you don't want to use callback, e.g. a latch. Note, that you shouldn't wait for the latch.await() on the EDT, because this will block it and will cause the UI freeze.

Another option would be to use Kotlin coroutines (runWithModalProgressBlocking will block the thread, however, EDT will be still pumped during the call and the UI won't freeze):
fun collectPageSource(project: Project, browser: CefBrowser) {
  val result = runWithModalProgressBlocking(project, title = "Collecting page source") {
    // TODO: replace with suspendCancellableCoroutine and handle cancellation
    suspendCoroutine { continuation -> 
      browser.getSource { string -> 
        continuation.resume(string)
      }
    }
  }
  println(result)
}

Thanks Ivan Posti for above suggestions.

IMHO, we will prefer to go with runWithModalProgressBlocking approach as it will allow us to do minimal changes in the plugin code.

above github example contains Action written in java for simplification.

Actual plugin is written in scala and sbt-idea-plugin

Kindly suggest, if runWithModalProgressBlocking can be used with scala or is there any alternate way to implement runWithModalProgressBlocking in scala.

0

I don't have enough experience with Scala, but I don't think it is possible to use the suspendable functions from Kotlin there.
In that case, you use the following approach:
public static String collectPageSource(@Nullable Project project, @NotNull CefBrowser browser) {
    return ProgressManager.getInstance().runProcessWithProgressSynchronously(() -> {
        var latch = new CountDownLatch(1);
        var result = new Ref<String>();
        browser.getSource(string -> {
            result.set(string);
            latch.countDown();
        });
        while (true) {
            // Check for cancellation
            ProgressManager.checkCanceled();
            try {
                latch.await(50, TimeUnit.MILLISECONDS);
                break;
            } catch (InterruptedException exception) {
                // do nothing
            }
        }
        return result.get();
    }, "Collecting Page Source", true, project);
}
 

1

Thanks
countdownlatch approach worked for me.

BR

 

0

Please sign in to leave a comment.