Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Select which executable inside an AppImage to run based on argv[0]? #419

Open
lvml opened this issue Jun 30, 2017 · 26 comments
Open

Select which executable inside an AppImage to run based on argv[0]? #419

lvml opened this issue Jun 30, 2017 · 26 comments

Comments

@lvml
Copy link

lvml commented Jun 30, 2017

Many executables use the name under which they are invoked (argv[0]) to select one of multiple different roles they can fulfill - like "xz" which, if invoked as "xzcat" or "unxz" performs different operations.

It would make a lot of sense for AppImage files to allow for this, such that multiple executables inside the AppImage can all be executed by just creating differently named soft-links to the AppImage file.

I use this method in my https://github.com/lvml/makeaoi tool to allow packing multiple executables into the same directory - so their dependencies can be shared. (Think of examples like packaging "ffmpeg" and "ffprobe" into the same directory. Or "kdenlive" and its "kdenlive_render" batch processing tool.)

It would be great if AppImage would support this by also using the name it was invoked with to use names other than "AppRun" to run (if a file or link of that name exists).

@probonopd
Copy link
Member

Duplicate of #25, pull requests welcome.

@TheAssassin
Copy link
Member

I think that this is something that AppDir creation tools like linuxdeployqt should implement. It would just bloat the runtime, as it is a totally optional feature.

@lvml
Copy link
Author

lvml commented Jul 1, 2017

@TheAssassin: If the AppImage runtime does not evaluate the argv[0] it was invoked with, there is no way for the content of the directory (like AppRun) to know about it.

@TheAssassin
Copy link
Member

That's not quite true. The AppImage runtime sets the environment variable APPIMAGE environment, which is essentially the argv[0] you'd be interested in in AppRun. See https://github.com/AppImage/AppImageKit/blob/appimagetool/master/runtime.c#L462-L476.

@lvml
Copy link
Author

lvml commented Jul 1, 2017

@TheAssassin: The code in runtime.c you linked to reads the content of the softlink /proc/self/exe to fill the APPIMAGE environment variable - and that is a path to the actual AppImage binary file, this would not reflect the name of some soft-link to that binary file (as would argv[0]).
APPDIR seems to contain (as one part) the first 6 bytes of the actual argv[0] value, but using that could cause ambiguities.

@probonopd
Copy link
Member

$APPDIR points to the mountpoint of the AppImage. If you need runtime.c to export another environment variable containing argv[0], let's add that.

@TheAssassin
Copy link
Member

Okay, so this is not a duplicate of #25.

@bubnikv
Copy link

bubnikv commented Sep 2, 2020

I wonder how the AppImage handles multiple processes started

  1. From the same AppImage. We see that the AppImage squashfs is mounted twice. Do the two processes then share their program space in RAM or not? I suppose they do not, thus it is a waste of RAM.
  2. From the same AppImage, but one symlinked as proposed in this issue, so it has a different process name. I suppose it is the same as 1).
  3. An AppImage starts and executes another instance of the executable from an already mounted squashfs. Then if the main application closes, the squashfs is likely unmounted, but the 2nd process is still running. I have no idea how that technically works, but anyway, the 2nd process would likely be no more able to open data files from the already unmounted squashfs.

Why we care? For our PrusaSlicer application, we are adding a G-code viewer as a standalone application. For technical reasons (plenty of ugly global variables) we have to launch another process for the G-code viewer. Executing a 2nd process is also better from the stability point of view. If one dies, the other survives.

@probonopd
Copy link
Member

probonopd commented Sep 2, 2020

Hi @bubnikv if I understand it correctly, then you want to launch your main process A which than can launch a secondary process B from within the same AppImage mountpoint. I guess you need to have some kind of a wrapper script that launches A and keeps running and does not exit as long as either process A or process B are still running. As soon as that script exits, the AppImage gets unmounted.

Another solution would be that whenever A gets closed, it always kills B. (Like if A and B were the same application - you quit the application, it results in all its windows being closed.)

Note that process A needs to construct the path to the executable B relative to itself. It must not launch another instance of the AppImage, because this would result in another mountpoint being used and hence no memory being shared.

@TheAssassin
Copy link
Member

From the same AppImage. We see that the AppImage squashfs is mounted twice. Do the two processes then share their program space in RAM or not? I suppose they do not, thus it is a waste of RAM.

Well, likely they won't. But program space itself shouldn't be the biggest issue with your application, it's just a few MiB. Compared to the (up to) hundreds of MiB a slicer uses for its own purposes, it's negligible.

An AppImage starts and executes another instance of the executable from an already mounted squashfs. Then if the main application closes, the squashfs is likely unmounted, but the 2nd process is still running. I have no idea how that technically works, but anyway, the 2nd process would likely be no more able to open data files from the already unmounted squashfs.

The AppImage runtime should kill all running processes once the main process dies to avoid such situations.

Picking up the idea from @probonopd, you could probably also just write a small wrapper that starts PrusaSlicer and keeps track of all subprocesses launched later on. It can keep the mount point alive until all subprocesses have closed properly.

@bubnikv
Copy link

bubnikv commented Sep 4, 2020

Thanks @probonopd @TheAssassin for your comments.

From the same AppImage. We see that the AppImage squashfs is mounted twice. Do the two processes then share their program space in RAM or not? I suppose they do not, thus it is a waste of RAM.

Well, likely they won't. But program space itself shouldn't be the biggest issue with your application, it's just a few MiB. Compared to the (up to) hundreds of MiB a slicer uses for its own purposes, it's negligible.

Our monolitic static compiled binary is 120MB big. That is not quite negligible. It is true that the application consumes couple of GB RAM easily.

An AppImage starts and executes another instance of the executable from an already mounted squashfs. Then if the main application closes, the squashfs is likely unmounted, but the 2nd process is still running. I have no idea how that technically works, but anyway, the 2nd process would likely be no more able to open data files from the already unmounted squashfs.

The AppImage runtime should kill all running processes once the main process dies to avoid such situations.

We don't want to kill the other process. We have discussed the topic internally, we decided to go the KISS way and launch another AppImage instance instead of another instance of the application from the mounted file system. I feel quite ashamed doing that though, the perfectionist in mine hurts badly.

Picking up the idea from @probonopd, you could probably also just write a small wrapper that starts PrusaSlicer and keeps track of all subprocesses launched later on. It can keep the mount point alive until all subprocesses have closed properly.

We can do that, but first we don't have the resources to do that for the upcoming release of our product, second we believe that such functionality belongs to the AppImage for obvious reasons. We think that this is an universal solution applicable to any large application of which the user may want to execute multiple instances.

I propose the following generic solution, which I believe is a worthy addition to the AppImage concept, even if it was just to improve the case 1) (mounting the filesystem twice for two instances of the same AppImage). I understand your proposal to write a user wrapper, but I think the AppImage runtime shall rather implement such a functionality for performance reasons and for generality. I am proposing an extension to the AppImage runtime / libappimage / squashfuse / libsquash. The proposal is based on the assumption that the fuse_session_loop() exits when its process receives SIGCHLD when its subprocess dies. The proposal is based on the idea of having a single instance of the AppImage runtime running (the primary instance), mounting the squashfs, forking subprocesses and closing first when all the subprocesses die.

  1. When the AppImage is started, the runtime calculates a canonical path to itself (replacing all symlinks with the real thing). Then this path is used to calculate a MD5 hash and this hash is used as a file name to store and retrieve process ID of the primary AppImage runtime instance already running. The canonic path is needed to support functionality of Select which executable inside an AppImage to run based on argv[0]? #419(symlinking the AppImage).

  2. If such runtime is not running yet, then the currently running runtime becomes the primary. It mounts a filesystem, starts the 1st instance of the application and it writes its process ID to the MD5 hash named temp file. Otherwise a message is sent to the primary instance to start another instance of an application.

The fuse_session_loop() concept would have to be modified to keep a database of subprocesses, so that it dies only when the last SIGCHLD is received.

I am not sure about the stdin/stdout/stderr of the processes started remotely. I suppose the calling AppImage runtime may pipe the stdin/stdout/stderr streams to the primary AppImage runtime using Unix sockets or similar means. I suppose it is not possible to pass open file descriptors or unnamed pipes to the server to pass them to a forked process, so the stdin/stdout/stderr would have to be piped to the launched application by the secondary AppImage runtime instance. Thus it would be beneficial to have a "daemon" switch to not do the stdin/stdout/stderr redirection at all.

Likely asking the AppImage primary to fork another instance is the safest way. One may in theory call prctl(PR_SET_CHILD_SUBREAPER) on the primary AppImage process, so an application my fork a subprocess (a grandchild), and if the grandchild dies, the AppImage primary will be informed with SIGCHLD. But then the Appimage primary would first have to be informed about the existence of each newly launched grandchild, which would complicate the synchronization badly.

It makes sense to add the functionality to the AppImage runtime and not to some wrapper inside the squashfs. If the wrapper was inside the squashfs, then it would have to be mounted to perform the communication with the primary.

It may make sense to allow delayed release of the primary if the AppImage is used to pack some often executed binary. Imagine git or a compilator suite.

BTW we are producing OSX builds as well. While the architecture is similar to AppImage, I suppose the issue with unmounting the .dmg archive is not that bad because the applications are usually installed by unpacking the content of .dmg into user file system and unmounting of the .dmg is controlled by the user manually.

@TheAssassin
Copy link
Member

We don't want to kill the other process. We have discussed the topic internally, we decided to go the KISS way and launch another AppImage instance instead of another instance of the application from the mounted file system. I feel quite ashamed doing that though, the perfectionist in mine hurts badly.

Well, these kinds of scenarios are not necessarily covered by the AppImage design goals. AppImages are "one app = one file" conceptually. Doesn't mean you can't use them otherwise, but that's the main use case.

@probonopd
Copy link
Member

probonopd commented Sep 4, 2020

Thanks @bubnikv. I think we all agree that launching a second AppImage instance is less than ideal.

If I understand it correctly, then we need a way to keep a script or binary running until all of its child (and sub-children) processes have exited.
Do we have the same understanding?

(I think there was once an AppImage of KMail that had a script for this... if I am not totally wrong)

@probonopd
Copy link
Member

probonopd commented Sep 4, 2020

The trivial way would be to have an AppRun script that launches A, and after that periodically (every 10 seconds or so) checks with ps auxwwwe whether any processes in $APPDIR are still running - if not, the script exits.

Or am I thinking too simplistic here?

@bubnikv if you can send me what you have in the works so far, I can try writing (and testing) the script.

@TheAssassin
Copy link
Member

This is too simplistic indeed. You ignore the fact that you might have multiple AppImages running in parallel, e.g., different versions or simply different instances of the slicer.

@bubnikv
Copy link

bubnikv commented Sep 5, 2020

Or am I thinking too simplistic here?

I am worried about race conditions when the "primary" is closing up while the "secondary" is starting a binary from the already mounted file system. It is a non-trivial task to implement the "primary" - "secondary" synchronization in a bullet proof fashion.

I proposed a client / server approach, where the "primary" would have a full control over the life time of the binaries. That is certainly a bullet proof solution, as it solely relies on an operating system support of parent / child process synchronization, which itself is very well tested. The main drawback I see is the input / output redirection.

It is likely possible to start the "secondary" directly without having to be a child of the "primary". However, there is no way to reparent a process to a new parent apart from the double fork trick

https://stackoverflow.com/questions/881388/what-is-the-reason-for-performing-a-double-fork-when-creating-a-daemon#:~:text=The%20double%2Dfork%20technique%20ensures,process%20reacquiring%20a%20controlling%20terminal.

where the abandoned child is reparented to the init process or to a process with PR_SET_CHILD_SUBREAPER set.

Therefore if the process is not a child of the "primary", then the "primary" / "secondary" synchronization would have to be implemented in a custom way using the inter-process synchronization primitives, which is error prone thus it needs to be well designed and it should cover all the cases where either party blocks or dies unexpectedly. First the "secondary" would have to tell the "primary" not to close the FUSE file system before starting the "secondary" binary and the "primary" would have to confirm the lock. Then the "secondary" would be started and ideally its process ID would be passed to the "primary" server. Ideally when the secondary is dying, as a last thing it would send a message to the "primary".

if you can send me what you have in the works so far, I can try writing (and testing) the script.

The master PrusaSlicer has a menu item "Window->Open new instance", which just executes another instance of the prusa-slicer binary. We don't have anything more advanced.

@probonopd
Copy link
Member

probonopd commented Sep 5, 2020

This is too simplistic indeed. You ignore the fact that you might have multiple AppImages running in parallel, e.g., different versions or simply different instances of the slicer.

By checking ps auxwwwe for the absolute path of the AppImage mountpoint my proposed solution would take care of that.

I am worried about race conditions when the "primary" is closing up while the "secondary" is starting a binary from the already mounted file system.

Wouldn't it be sufficient to add a couple of sleeps in the checking code?

@bubnikv
Copy link

bubnikv commented Sep 6, 2020

Wouldn't it be sufficient to add a couple of sleeps in the checking code?

I would not sleep well doing that. In my experience, if something could get wrong, it eventually will go wrong if enough of users use it enough of times. This is especially true with computational geometry algorithms as in slicer.

I will consult with our resident Linux experts at PrusaSlicer about what would be the most elegant synchronization approach.

@probonopd
Copy link
Member

Another idea, maybe we can re-use (parts of) https://linux.die.net/man/1/pstree? I'd like to avoid writing complicated code from scratch and rather use something that has been proven already, since as you say, if something could get wrong, it eventually will go wrong if enough of users use it enough of times.

@TheAssassin
Copy link
Member

TheAssassin commented Sep 6, 2020

Again: it will not work (reliably). If those subprocesses are detached from their parent process (using a double fork, for instance), they might not show up in the process tree of the main process any more. Then you're lost, basically.

The best possible approach, however, is something tailored for this specific use case, less work and less trouble. It's not a classic AppImage use case and therefore there's no real reason to change our software and do the effort for them. It just increases complexity on our side for no real reason. They'll have to figure it out themselves in case they need this kind of optimization. A simple process manager that keeps track of all instances (maybe even using some IPC so new, forked child procs can register themselves) should do the trick.

@probonopd
Copy link
Member

If those subprocesses are detached from their parent process (using a double fork, for instance)

Is Prusa Slicer using double forks?

@bubnikv
Copy link

bubnikv commented Sep 11, 2020 via email

@bubnikv
Copy link

bubnikv commented Sep 11, 2020

Now it is getting interesting.

The process tree in my previous post is real. The AppImage starter gets daemonized while the prusa-slicer application is execve'd on the initial process, therefore the prusa-slicer gets both the stdin/stdout/stderr and all the subprocesses retain the parent/child relationship.

The AppImage squashfs server gets daemonized through a double fork, thus it is reparented to an init process. It cannot receive any SIGCHLD signals from the prusa-slicer when it dies, therefore the squashfs server has to do some form of polling. This polling is done in

squashfuse/ll.c

see setup_idle_timeout(), alarm_tick() and the comment

/* Idle unmount timeout management is based on signal handling from
   fuse (see set_one_signal_handler and exit_handler in libfuse's
   lib/fuse_signals.c.

   When an idle timeout is set, we use a one second alarm to check if
   no activity has taken place within the idle window, as tracked by
   last_access.  We also maintain an open/opendir refcount so that
   directories and files can be held open and unaccessed without
   triggering the idle timeout.
 */

Maybe we are completely safe already and we don't know it?

@bubnikv
Copy link

bubnikv commented Sep 11, 2020

The timeout seems to be off in the AppImage runtime. It should be sufficient to add the timeout parameter to fusefs_main() to shut down by polling the number of open files. I still don't know how the AppImage daemon gets shut down currently, but that would likely have to be suppressed if using the polling.

@bubnikv
Copy link

bubnikv commented Sep 11, 2020

I am not sure how that works, but it seems to me that the AppImage daemon dies when the keepalive_pipe
in AppImageKit.c/src/runtime.c is closed as the application is dying. The AppImage daemon sends itself a SIGKILL signal on that event.

So it should be sufficient to disable the suicide of the AppImage daemon by the keepalive_pipe watching and to enable the idle timeout. Doing that, we are basically done with allowing our application to launch a child process and everything should work by counting the open file references by the squashfs part of the AppImage daemon.

@bubnikv
Copy link

bubnikv commented Sep 11, 2020

To support just a single instance of the AppImage daemon mounting just a single copy of the AppImage squashfs, I would encode a MD5 hash of a full canonical path of the AppImage file into the mount point name, thus there will be a single mount point generated for a single AppImage. Then the AppImage starter would first try to open the mount point. If that succeeds and the mount point is mounted, then the server must be running and once the application is started from the mount point, the AppImage server will know it from the number of open files it registers through the squashfs library.

If the mount point does not exist yet, then the runner would fork the AppImage squashfs daemon.

The only race condition I see is when the mount point directory exists, but the AppImage daemon is starting or dying. One may read /proc/self/mountinfo and search for the mount point. It says

/tmp/.mount_PrusaSU65DAP ro,nosuid,nodev,relatime - fuse.PrusaSlicer-2.3.0-alpha0+867-linux-x64-g50a6680-202009102318.AppImage PrusaSlicer-2.3.0-alpha0+867-linux-x64-g50a6680-202009102318.AppImage ro,user_id=1000,group_id=1000

on my WSL2 system with Linux version 4.19.104-microsoft-standard kernel.

When the runner opens the mount point and the mount point is mounted (confirmed by reading the /proc/self/mountinfo afterwards), then the other runner should not die because the file system access reference counter inside the squashfs daemon has already been incremented, so this is working correctly IMHO.

When the runner opens the mount point and it is not mounted yet, then there is a chance that another runner is starting and it will mount the mount point in the meanwhile. If both try to mount, one of them will fail. This is burried deep in the squashfs library, thus it is more difficult to handle correctly.

Not the least, there will be situations where the AppImage squashfs daemon dies unexpectedly for whatever reason (out of RAM, someone sends it a SIGKILL etc). Then the filesystem will unmount, but the mount point directory will not be deleted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants