Nyxt freezes when prompt is called in a class

Hi, so I’m still working on a prompt feature for a download destination directory.

I’ve narrowed down to data-storage.lisp and that’s where I’ve been looking at. I’ve figured out that the download path is specified in the download-data-path class in its dirname slot value and this slot value calls the function xdg-download-dir to get download directory.

So I tried to call a prompt in the xdg-download-dir function. I tried this line (setf dir ((promp :input (namestring *default-pathname-defaults* :prompt "Open download directory" :sources (list (make-instance 'file-source)))))) and I added it to xdg-download-dir thinking that it would assign the variable dir with whatever value the prompt receives from the user and assigns this value to the download-data-path slot value.

But when I load the data-storage.lisp file my Nyxt browser just freezes. I don’t get any error ouput or anything it just freezes and I’m not sure why. I’ve been scratching my head with this problem for a few days now but I still don’t know what’s wrong. I would appreciate any thoughts or input please because I have no idea what to do. Thank you.

I have a theory! Could you please post a full code sample so I can see if it is correct? Alternatively if you’ve forked Nyxt just post the url to your fork and branch :slight_smile:

Here’s my fork

Looking at your source, I believe my theory to be correct. By overriding the xdg-download-dir function to be interactive you are making it lock.

Why? Creating a new buffer invokes this function. In order for the new buffer creation to finish, it will have to invoke your modified xdg-download-dir function. If Nyxt is in a state where it cannot yet receive input, then the program will not start.

I would instead look at gtk.lisp in the ffi-buffer-download.

We see the important source in question:

      (alex:when-let* ((download-dir (download-path buffer))

So, what we could do here is make download-path of the buffer class somehow configurable. We could make it prompt the user perhaps? We could turn it into a function! From there we would just have:

(defmethod download-path ((buffer buffer))

Within the download-path method, you could check the slot-value of download-path of the buffer and see if it is not set, or set to some special value. For example if the user does:

(define-configuration buffer
  ((download-path :interactive)))

then the download-path method could query the user for /where/ to download things, otherwise download to the default path as specified by the slot download-path.

That is just one idea of course, there are many ways to do this :slight_smile:
If you need more help, please do not hesitate to ask!

Thanks for the suggestions. I’m still confused about why my solution didn’t work though.

From my understanding, it’s because Nyxt is not in a state where it can receive input so there’s sort of a deadlock happening where my modified xdg-download-dir calls for input but Nyxt is in a state where it cannot receive input so it has to wait until it does. Hence why it’s freezing because it’s waiting to enter that state where it can receive input. Is that right?

That’s correct! :-).

But why is turning download-path into a function a working solution? Don’t we also need to invoke this function and we’ll run into the same problem where Nyxt is not in a state where it can receive input so it has to wait?

Here is why:

the define-class macro we are using produces an initform. The following form:

(define-class tomato () ((seeds (make-instance 'seed)))

translates to:

(defclass tomato () ((seeds :initform (make-instance 'seed)))

when we make an instance of tomato (make-instance 'tomato) we’ll also make an instance of seed. If (make-instance 'seed) is problematic for whatever reason, then we would never be able to instantiate a tomato.

Now consider this example:

(defclass tomato () ((seeds)))

Now, (make-instance 'tomato) will not make any seeds. It doesn’t matter if (make-instance 'seed) is problematic. We can always make tomatoes. Sure, we won’t have any seeds automatically made, but that is OK!

Taking it a step further:

(defclass tomato () ((seeds)))

(defmethod seeds ((tomato tomato))
  (if (slot-boundp tomato 'seeds)
    (slot-value tomato 'seeds)
    (setf (slot-value tomato 'seeds) (make-instance 'seed)))

As you can now see, we can check the value of seeds and set it or do any arbitrary action when (seeds tomato) is invoked. This is DIFFERENT than:

(defclass tomato () ((seeds :accessor seeds)))

We have full control over what happens when (seeds tomato) is invoked.

Bringing it back to what we are talking about:

(define-class buffer ()
   ((download-path (xdg-download-path) :accessor nil)))

(defmethod download-path ((buffer buffer))
  (if (eq :query-user-for-path (slot-value buffer 'download-path))
       (slot-value buffer 'download-path))))

Now, if the user set’s the value like this:

(define-configuration buffer ((download-path :query-user-for-path)))

they will be queried for a path whenever (download-path buffer) is invoked, OTHERWISE, it would use the path calculated by (xdg-download-path). Important distinction: MAKING an instance of the buffer will NOT query the user.

Disclaimer: all of the above is pseudocode, parenthesis may not line up etc, despite this, I hope it is clear.

Please let me know if that helps :slight_smile:

Right, so by defining a method download-path we allow Nyxt to make an instance of buffer without causing any problems (using the tomato and seed example it doesn’t matter if seed instance is problematic we can still make tomatoes), thus allowing it to enter into a state where it can receive input. And then we can invoke this method to get a download path from the user. Is that right?

That is correct! :slight_smile:

1 Like

Hi, it seems that I’m facing the same issue of Nyxt freezing as well when I try to implement the solution you proposed.

I’ve tried to implement the following code according to your suggestion:

(defmethod download-path ((buffer buffer))
  (if (equal (uiop:getenv "HOME") (slot-value buffer 'download-path))
       :input "Hello world!"
       :prompt "Confirm download path"
       :sources (list (make-instance 'file-source)))
      (slot-value buffer 'download-path)))

(define-configuration buffer ((download-path (uiop:getenv "HOME"))))

So in this case I’ve set the value of download-path as the home directory so that the buffer should prompt the user. But once again when I run the code it freezes again. I’m not sure why.

Could it be that the issue is with prompt itself? Perhaps for some reason prompt cannot be called within a function or a class?

I see. Thank you for the update. I’ll have to give it a try when I have a little more time! Sorry for the delay!

I will be looking at this next on my list, sorry for the delay!

I’ve made progress, unfortunately the signal handler for downloads is acting on a GTK thread. It is the same exact problem as described in this thread: gtk.lisp: create outside-renderer-thread by jmercouris · Pull Request #1763 · atlas-engineer/nyxt · GitHub

when we get to the download-path method of the buffer class we need to call the prompt buffer to get input from the user. Some of these actions must run on the GTK thread- meaning a deadlock.

the problem is that this code in gtk.lisp:

(connect-signal download "decide-destination" (webkit-download suggested-file-name)
      (alex:when-let* ((download-dir (download-path buffer))
                       (download-directory (expand-path download-dir))
                       (path (str:concat download-directory suggested-file-name))
                       (unique-path (download-manager::ensure-unique-file path))
                       (file-path (format nil "file://~a" unique-path)))
        (if (string= path unique-path)
            (echo "Destination ~s exists, saving as ~s." path unique-path)
            (log:debug "Downloading file to ~s." unique-path))
        (webkit:webkit-download-set-destination webkit-download file-path)))

is running on the GTK main thread. Meaning, that when we try to do:

(defmethod download-path ((buffer buffer))
  (print "Access download path.")
  (if (eq :query (slot-value buffer 'download-path))
      (let ((path (file (first (prompt
                                :prompt "Select Download Directory"
                                :input (namestring (uiop:getcwd))
                                (list (make-instance 'user-file-source
                                                     :name "Directory"
                                                     :actions '(identity))
                                      (make-instance 'prompter:raw-source
                                                     :name "New file")))))))
        (make-instance 'download-data-path :dirname path))
      (slot-value buffer 'download-path)))

we are unable to do so because calling prompt calls the main GTK thread.

Well, the main GTK thread cannot continue until the signal handler is satisfied. The signal handler cannot be satisfied until the Prompt comes up, which requires the GTK thread. Et voila, resource starvation.

The proposed solution by Pierre may or may not fix this problem. I of course tried the obvious:

(connect-signal download "decide-destination" (webkit-download suggested-file-name)
       (lambda ()
         (alex:when-let* ((download-dir (download-path buffer))
                          (download-directory (expand-path download-dir))
                          (path (str:concat download-directory suggested-file-name))
                          (unique-path (download-manager::ensure-unique-file path))
                          (file-path (format nil "file://~a" unique-path)))
           (if (string= path unique-path)
               (echo "Destination ~s exists, saving as ~s." path unique-path)
               (log:debug "Downloading file to ~s." unique-path))
           (webkit:webkit-download-set-destination webkit-download file-path)))))

this will not work for WebKit downloads which require us to return some operation inline!

Apologies for the late reply, I’ve been busy and I haven’t really concentrated on this lately.

This sounds a bit complex but I’ll try and summarize and digest this. So from my current understanding, to rephrase what you said, the GTK thread is waiting on the signal handler which is waiting on the prompt which is waiting on the GTK thread. A dead lock.

Thus, a solution that you proposed, in your reply above and in here: Connect signal by jmercouris · Pull Request #1838 · atlas-engineer/nyxt · GitHub, is to make the signal handler run in a new thread by adding a new argument run-in-new-thread-p. But we run into a problem mentioned earlier that some of the actions in the download-path method must run on the GTK thread so creating a new thread wouldn’t work. Is that right?

That is correct. So far the only solution I can think of is running another instance of Nyxt to query the user and return the path. It would be a sort of “pop up dialog”.