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

Linux binary #113

Closed
Pr0mises opened this issue Jan 5, 2023 · 35 comments · Fixed by #216
Closed

Linux binary #113

Pr0mises opened this issue Jan 5, 2023 · 35 comments · Fixed by #216
Labels
Dev For interminent issues created during development between releases Enhancement New feature or request

Comments

@Pr0mises
Copy link

Pr0mises commented Jan 5, 2023

In #35 linux support got added and released with v15 and it's working like a charm.
As of the last comment by Devil I tried my best to make an executable with some instructions but failed.

What I found out:

  • You can also use pyinstaller for building binaries in Linux.
  • python3.10 is not working with pyinstaller at all (it's an unsupported bug party) so I decided to use 3.11.

I had some big trouble settings up tkinter, and it looks like I still have (maybe).
Important
You have to install tkinter besides of pip. (sudo apt-get install tcl-dev && tk-dev), check for weird paths mine wasn't stored in /usr/lib/tcltk/ but instead in /usr/share/tcltk

Many fails later I finally managed to run pyinstaller and build an executable (yey) but now I'm stuck again and have no clue what's wrong and wth I'm doing wrong...

After using this build.spec file I was able to build an executable with this output:
pyinstaller.txt

After investigating, I found those WARNINGS:

1408 WARNING: Cannot find libwebp-112793e9.so.7.1.5 (needed by /path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/PIL/../Pillow.libs/libwebpmux-87a21b3d.so.3.0.10)
1435 WARNING: Cannot find libwebp-112793e9.so.7.1.5 (needed by /path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/PIL/../Pillow.libs/libwebpdemux-855d485d.so.2.0.11)
1464 WARNING: Cannot find libXau-154567c4.so.6.0.0 (needed by /path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/PIL/../Pillow.libs/libxcb-3e83370d.so.1.1.0)
1479 WARNING: Cannot find liblzma-160b9c62.so.5.4.0 (needed by /path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/PIL/../Pillow.libs/libtiff-b9364ff1.so.6.0.0)
1479 WARNING: Cannot find libjpeg-16b2c4cf.so.62.3.0 (needed by /path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/PIL/../Pillow.libs/libtiff-b9364ff1.so.6.0.0)

So I searched for those files and all of them are present...

/path/to/TwitchDropsMiner/dist/main/libwebp-112793e9.so.7.1.5
/home/kali/.cache/pyinstaller/bincache00_py311_64bit/libwebp-112793e9.so.7.1.5

/path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/Pillow.libs/libXau-154567c4.so.6.0.0
/path/to/TwitchDropsMiner/dist/main/libXau-154567c4.so.6.0.0
/home/kali/.cache/pyinstaller/bincache00_py311_64bit/libXau-154567c4.so.6.0.0

/path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/Pillow.libs/liblzma-160b9c62.so.5.4.0
/path/to/TwitchDropsMiner/dist/main/liblzma-160b9c62.so.5.4.0
/home/kali/.cache/pyinstaller/bincache00_py311_64bit/liblzma-160b9c62.so.5.4.0

/path/to/TwitchDropsMiner/env/lib/python3.11/site-packages/Pillow.libs/libjpeg-16b2c4cf.so.62.3.0
/path/to/TwitchDropsMiner/dist/main/libjpeg-16b2c4cf.so.62.3.0
/home/kali/.cache/pyinstaller/bincache00_py311_64bit/libjpeg-16b2c4cf.so.62.3.0

Now to the actual error when starting the compiled binary:

Traceback (most recent call last):
  File "PIL/ImageTk.py", line 65, in _pyimagingtkcall
_tkinter.TclError: invalid command name "PyImagingPhoto"

During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "main.py", line 99, in <module>
  File "PIL/ImageTk.py", line 130, in __init__
  File "PIL/ImageTk.py", line 190, in paste
  File "PIL/ImageTk.py", line 69, in _pyimagingtkcall
ModuleNotFoundError: No module named 'PIL._tkinter_finder'
[1337244] Failed to execute script 'main' due to unhandled exception!
sys:1: ResourceWarning: unclosed file <_io.BufferedReader name='/tmp/_MEIuAc0OX/pickaxe.ico'>
ResourceWarning: Enable tracemalloc to get the object allocation traceback

I'm unsure if everything is clear I made a TON of "fixes" without even knowing anymore, so some information might be missing. If this helps, great if not well...

I hope someone catches up on this. Cheers

@DevilXD DevilXD added the Enhancement New feature or request label Jan 5, 2023
@DevilXD
Copy link
Owner

DevilXD commented Jan 5, 2023

Hello o/ Thank you for taking interest in this topic 🙂

You can also use pyinstaller for building binaries in Linux.

Interesting, didn't know that. I'm assuming it requires a Linux environment to build though? I'm not using Linux myself so I think that'd be quite hard to handle, but I could try figuring something out.

python3.10 is not working with pyinstaller at all (it's an unsupported bug party) so I decided to use 3.11.

First time I'm hearing about this, I have Python 3.10 installed at my work laptop from which I was able to build a local non-release executable for testing, and it worked just fine there. Maybe it doesn't work only for Linux?

You have to install tkinter besides of pip. (sudo apt-get install tcl-dev && tk-dev), check for weird paths mine wasn't stored in /usr/lib/tcltk/ but instead in /usr/share/tcltk

That's weird. Tkinter is a library that's normally installed together with Python, per this picture from the Python installation guide I made for Windows: https://github.com/DevilXD/TwitchDropsMiner/wiki/Python-Installation

If you don't check it, you won't get tkinter and will need to install it separately as you said. A quick google search suggests that in Linux case, all you need to do after installing python itself, is to just grab python-tk to it: https://stackoverflow.com/a/4784123/8576445 (also see the original question for the error the person got). With Tkinter installed this way, it should get included during the building process as normal.

After using this build.spec file

I can see that you've reenabled console, probably to see any loading errors that might occur. Smart move.

After investigating, I found those WARNINGS:
I searched for those files and all of them are present...

Pyinstaller warnings about missing packages should be taken with a huge grain of salt. During the analisys process, all imports are collected to determine which libraries have to be included in the final binary for it to run without errors. This includes conditional imports, that try to import a library, but are just fine to fail the import if said library is not present. This is the main reason I've started using venvs since around v6, because otherwise PyInstaller would pick up many additional crap from my global installation, due the the PIL import, that was never needed for runtime in the first place. Limiting the amount of libraries installed in an environment via venv usage has reduced the final build size 5-fold.

In general, unless you're getting runtime errors about missing libraries or files, there's no need to bother with even looking at PyInstaller warnings at all.

Now to the actual error when starting the compiled binary:
ModuleNotFoundError: No module named 'PIL._tkinter_finder'

A quick search suggests that all you need is an additional hidden import to be added to the Analisys object in your build.txt. However, I have a feeling this error is related to PIL not being able to find a local Python Tkinter installation (due to you installing some kind of an external library for it), and it tried to invoke this special finder to resolve the path to it. Once you install python-tk per what I said above, none of this should be needed (PIL should pick up on the locally installed Tk) and this particular error should go away, without having to add any hidden imports at all.

@Pr0mises
Copy link
Author

Pr0mises commented Jan 7, 2023

Interesting, didn't know that. I'm assuming it requires a Linux environment to build though? I'm not using Linux myself so I think that'd be quite hard to handle, but I could try figuring something out.

yes you do have to build in on linux

First time I'm hearing about this, I have Python 3.10 installed at my work laptop from which I was able to build a local non-release executable for testing, and it worked just fine there. Maybe it doesn't work only for Linux?

Referring to Note that Python 3.10.0 contains a bug making it unsupportable by Pyinstaller (but looks like my mistake could be only 3.10.0 not 3.10.x.

That's weird. Tkinter is a library that's normally installed together with Python, per this picture from the Python installation guide I made for Windows: https://github.com/DevilXD/TwitchDropsMiner/wiki/Python-Installation

I followed your installation guide

If you don't check it, you won't get tkinter and will need to install it separately as you said. A quick google search suggests that in Linux case, all you need to do after installing python itself, is to just grab python-tk to it: https://stackoverflow.com/a/4784123/8576445 (also see the original question for the error the person got). With Tkinter installed this way, it should get included during the building process as normal.

That's correct, I do have python3-tk installed, but it doesn't work...

In general, unless you're getting runtime errors about missing libraries or files, there's no need to bother with even looking at PyInstaller warnings at all.

Alright, makes sense because I wasn't able to fix it :D

A quick search suggests that all you need is an additional hidden import to be added to the Analisys object in your build.txt. However, I have a feeling this error is related to PIL not being able to find a local Python Tkinter installation (due to you installing some kind of an external library for it), and it tried to invoke this special finder to resolve the path to it. Once you install python-tk per what I said above, none of this should be needed (PIL should pick up on the locally installed Tk) and this particular error should go away, without having to add any hidden imports at all.

Yes I've seen many threats about that as well, it didn't work for me though.
After some more uninstall reinstall rebooting, it works with the hiddenimports=["PIL._tkinter_finder",], but I'm not able to run it without it...

Edit: It is indeed only 3.10.0 I just build a working binary with 3.10.9

@DevilXD
Copy link
Owner

DevilXD commented Jan 7, 2023

After some more uninstall reinstall rebooting, it works with the hiddenimports=["PIL._tkinter_finder",], but I'm not able to run it without it...

That's fine then, if it really needs to be there, so be it. What's the resulting binary size?

That's correct, I do have python3-tk installed, but it doesn't work...

That could be because of the 3 there. My suggestion asked you to install python-tk, not python3-tk. I'm not that familiar with linux packages system, but this sounds to me like two different things, which could be the reason it doesn't work. One of the responses on SO I found suggests that python-tk is a Python 2 package, while python3-tk is a Python 3 package. I know Ubuntu comes with Python 2 preinstalled, and one has to take special care not to accidentally use it for anything that needs Python 3.

I followed your installation guide

You've installed it from the normal binary installer instead of the packages system? In that case, SO suggests to rerun the installer, use "Modify" and ensure Tk/Tcl is ticked as an installation component. I have no idea how Linux handles normal GUI installers vs packages from their system in this regard, it could be the same or completely independent installations. If they're independent, installing python-tk wouldn't do anything as you describe. I still don't believe one has to install any 3rd party packages for Tkinter to work with Python. It's possible to do so (that's why PIL has a searching module for it_, one you had to add to hidden imports), but otherwise it should just install together with Python, or as an additional package (python-tk) from the packages system.

If I had to guess, from the two paragraphs above, python3-tk needs a python3 main package for it to do anything, and then you have to know how to use it to create a venv and build an executable. Or, if you really installed it from a normal installer, then you need to ensure that Tk/Tcl option is/was ticked, and you're using this particular installation, and not the one from the package.

I have a VPS with ubuntu installed on it, and I can try setting up the build there, to see what trouble I could encounter myself. It's a no-GUI, console-only, so called "headless" instance, so I won't be able to actually go very far with running it, but it should at least build properly. Will do and let you know how it went.

@Pr0mises
Copy link
Author

Pr0mises commented Jan 7, 2023

That's fine then, if it really needs to be there, so be it. What's the resulting binary size?
The binary is 29M.

That could be because of the 3 there. My suggestion asked you to install python-tk, not python3-tk. I'm not that familiar with linux packages system, but this sounds to me like two different things, which could be the reason it doesn't work. One of the responses on SO I found suggests that python-tk is a Python 2 package, while python3-tk is a Python 3 package. I know Ubuntu comes with Python 2 preinstalled, and one has to take special care not to accidentally use it for anything that needs Python 3.

This is correct, python-tk is for python2.x. python3-tk is for python3.x.

You've installed it from the normal binary installer instead of the packages system? In that case, SO suggests to rerun the installer, use "Modify" and ensure Tk/Tcl is ticked as an installation component. I have no idea how Linux handles normal GUI installers vs packages from their system in this regard, it could be the same or completely independent installations. If they're independent, installing python-tk wouldn't do anything as you describe. I still don't believe one has to install any 3rd party packages for Tkinter to work with Python. It's possible to do so (that's why PIL has a searching module for it_, one you had to add to hidden imports), but otherwise it should just install together with Python, or as an additional package (python-tk) from the packages system.
If I had to guess, from the two paragraphs above, python3-tk needs a python3 main package for it to do anything, and then you have to know how to use it to create a venv and build an executable. Or, if you really installed it from a normal installer, then you need to ensure that Tk/Tcl option is/was ticked, and you're using this particular installation, and not the one from the package.

Well I removed all tk tk-dev tcl tcl-dev dependencies from my system and only used python3-tk for the last install. Same problem without hidden import.
For the installation I used python3 -m venv env to create the environment with python 3.10.9 and followed your installation guide but used pip install -I --no-cache-dir to prevent some cache problems. Building the binary was also done in the environment all the time.

@Pr0mises
Copy link
Author

Pr0mises commented Jan 8, 2023

Update:
new
build_linux.spec
added version check for seleniumwire certs. as it depends on it how the folder structure looks like.

and a
buid_linux.sh

works for me so far.

@DevilXD
Copy link
Owner

DevilXD commented Jan 8, 2023

Same problem without hidden import.

And with hidden import, it does work? The import is fine if necessary, it's more about Tk/Tcl being included in the build properly, using the one that installs with Python, not some 3rd party package.

Also, your build spec is missing said hidden import, not sure if that's intended or you've managed to get it working with Python Tk without it.

Actually, it's there. You've put two tabs instead of 8 spaces like in the original file, and the line was thus yeeted into the right so far I haven't noticed it.

@DevilXD
Copy link
Owner

DevilXD commented Jan 8, 2023

Installing python3-tk, with Python 3.8.10 already installed on this VPS:
obraz

Your build scripts, revised by me. Added wheel pip install that was causing chromedriver to fail the install, bash if statements use ne instead of eq for error level checks, redesigned python version path construct for something more appropriate:

build.zip

Build itself (had to use Dropbox cos of the file size): https://www.dropbox.com/s/ubjldhesss7s50p/Twitch%20Drops%20Miner%20%28by%20DevilXD%29?dl=1

As expected, this is the error I've got:

Traceback (most recent call last):
  File "main.py", line 28, in <module>
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "PyInstaller/loader/pyimod02_importers.py", line 499, in exec_module
  File "twitch.py", line 33, in <module>
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "PyInstaller/loader/pyimod02_importers.py", line 499, in exec_module
  File "gui.py", line 19, in <module>
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "PyInstaller/loader/pyimod02_importers.py", line 499, in exec_module
  File "pystray/__init__.py", line 64, in <module>
  File "pystray/__init__.py", line 56, in backend
  File "pystray/__init__.py", line 36, in xorg
  File "<frozen importlib._bootstrap>", line 991, in _find_and_load
  File "<frozen importlib._bootstrap>", line 975, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 671, in _load_unlocked
  File "PyInstaller/loader/pyimod02_importers.py", line 499, in exec_module
  File "pystray/_xorg.py", line 36, in <module>
  File "Xlib/display.py", line 89, in __init__
  File "Xlib/display.py", line 71, in __init__
  File "Xlib/protocol/display.py", line 84, in __init__
  File "Xlib/support/connect.py", line 73, in get_display
  File "Xlib/support/unix_connect.py", line 59, in get_display
Xlib.error.DisplayNameError: Bad display name ""
[232407] Failed to execute script 'main' due to unhandled exception!

No errors related to Tk/Tcl, but it also didn't really launch, so... I can't test it myself, but you can.

@Pr0mises
Copy link
Author

Pr0mises commented Jan 8, 2023

Your build scripts, revised by me. Added wheel pip install that was causing chromedriver to fail the install, bash if statements use ne instead of eq for error level checks, redesigned python version path construct for something more appropriate:

Ah that's what caused the warning:
DEPRECATION: undetected-chromedriver is being installed using the legacy 'setup.py install' method, because it does not have a 'pyproject.toml' and the 'wheel' package is not installed. pip 23.1 will enforce this behaviour change. A possible replacement is to enable the '--use-pep517' option. Discussion can be found at https://github.com/pypa/pip/issues/8559
with chromedriver being deprecated, it worked without it though.

You're correct about the error level check did it in hurry and wanted to fix it soon^tm :D

Running your binary I get:

ResourceWarning: Enable tracemalloc to get the object allocation traceback
gui.py:1768: ResourceWarning: unclosed file <_io.BufferedReader name='/tmp/_MEInjYDwg/pickaxe.ico'>
ResourceWarning: Enable tracemalloc to get the object allocation traceback
X Error of failed request:  BadLength (poly request too large or internal Xlib length error)
  Major opcode of failed request:  140 (RENDER)
  Minor opcode of failed request:  20 (RenderAddGlyphs)
  Serial number of failed request:  276
  Current serial number in output stream:  298

using the build script it does run though

@guihkx
Copy link
Contributor

guihkx commented Apr 30, 2023

I have been experimenting with a Linux build on my personal fork here:

master...guihkx:TwitchDropsMiner:linux-wip

And builds are available here (look for build jobs on the linux-wip branch):

https://github.com/guihkx/TwitchDropsMiner/actions

Keep in mind that this branch is not stable; I rebase it constantly to try stuff out. Additionally, I have added some changes to make debugging the build on Linux a bit easier. For instance, the app is not just a single executable yet.

Now, about the quality of the build itself: In theory, it should work just fine (with a lot of caveats 😄), on any Debian-based distro that has glibc 2.31 (or newer).

However, I'm currently facing an issue that prevents the app from working on Arch Linux (which is what I daily-drive), and also on Fedora 38 (which I tested on a virtual machine). This error pops up just a few seconds after the app launches:

Screenshot from 2023-04-30 13-32-42

The error looks looks like the one reported in #80, which doesn't seem to have a proper solution yet, I think.

But after running the binary with strace trying to find some clues, I noticed these lines in the output of strace:

openat(AT_FDCWD, "/usr/lib/ssl/openssl.cnf", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/ssl/cert.pem", O_RDONLY) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/home/gui/Downloads/Twitch.Drops.Miner.Linux-16.dev~d33c7ce/Twitch Drops Miner (by DevilXD)/TwitchDropsMiner", O_RDONLY|O_CLOEXEC) = 10
openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 10

And shortly after that, the runtime error pops up.

Now, this might be a red herring or I might be talking out of my ass, but I think this error happens because the libssl.so library that the apps ships (and that we got from Ubuntu 20.04), seems to be looking for root certificates in a different path than Arch Linux and Fedora.

On Ubuntu, root certificates seem to be stored in /usr/lib/ssl:

$ openssl version -a | grep OPENSSLDIR
OPENSSLDIR: "/usr/lib/ssl"

However, on Arch and Fedora they're stored in /etc/ssl:

$ openssl version -a | grep OPENSSLDIR
OPENSSLDIR: "/etc/ssl"

And what also makes me think this is the cause of the issue, it's because if you package the app on Arch Linux and then try to run it also on Arch Linux, things works just fine (and this is probably the same for Fedora, though I haven't tried it).

So, maybe something goes really bad behind the scenes because the app fails to find the root certificates? I'm not really sure, but I was able to get rid of that error just by configuring the SSL_CERT_DIR environment variable to the appropriate directory of root certificates, i.e.:

SSL_CERT_DIR=/etc/ssl/certs ./TwitchDropsMiner

image

🤷

@Pr0mises
Copy link
Author

@guihkx I just tried your binary but starting it resulted in those errors on a kali linux (vm) and glibc 2.36 (ui didn't even show up):

./TwitchDropsMiner
[3422351] PyInstaller Bootloader 5.x
[3422351] LOADER: executable is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/TwitchDropsMiner
[3422351] LOADER: homepath is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: _MEIPASS2 is NULL
[3422351] LOADER: archivename is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/TwitchDropsMiner
[3422351] LOADER: Cookie found at offset 0x255A6F
[3422351] LOADER: No need to extract files to run; setting up environment and restarting bootloader...
[3422351] LOADER: LD_LIBRARY_PATH=/home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] PyInstaller Bootloader 5.x
[3422351] LOADER: executable is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/TwitchDropsMiner
[3422351] LOADER: homepath is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: _MEIPASS2 is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: archivename is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/TwitchDropsMiner
[3422351] LOADER: Cookie found at offset 0x255A6F
[3422351] LOADER: Already in the child - running user's code.
[3422351] LOADER: Python library: /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/libpython3.8.so.1.0
[3422351] LOADER: Loaded functions from Python library.
[3422351] LOADER: Manipulating environment (sys.path, sys.prefix)
[3422351] LOADER: sys.prefix is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: Pre-init sys.path is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/base_library.zip:/home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/lib-dynload:/home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: Setting runtime options
[3422351] LOADER: Initializing python
[3422351] LOADER: Overriding Python's sys.path
[3422351] LOADER: Post-init sys.path is /home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/base_library.zip:/home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)/lib-dynload:/home/kali/Desktop/twitchdrop/Twitch Drops Miner (by DevilXD)
[3422351] LOADER: Setting sys.argv
[3422351] LOADER: setting sys._MEIPASS
[3422351] LOADER: importing modules from CArchive
[3422351] LOADER: extracted struct
[3422351] LOADER: running unmarshalled code object for struct...
[3422351] LOADER: extracted pyimod01_archive
[3422351] LOADER: running unmarshalled code object for pyimod01_archive...
[3422351] LOADER: extracted pyimod02_importers
[3422351] LOADER: running unmarshalled code object for pyimod02_importers...
[3422351] LOADER: extracted pyimod03_ctypes
[3422351] LOADER: running unmarshalled code object for pyimod03_ctypes...
[3422351] LOADER: Installing PYZ archive with Python modules.
[3422351] LOADER: PYZ archive: PYZ-00.pyz
[3422351] LOADER: Running pyiboot01_bootstrap.py
[3422351] LOADER: Running pyi_rth_inspect.py
[3422351] LOADER: Running pyi_rth_pkgutil.py
[3422351] LOADER: Running pyi_rth__tkinter.py
[3422351] LOADER: Running pyi_rth_multiprocessing.py
[3422351] LOADER: Running main.py
X Error of failed request:  BadLength (poly request too large or internal Xlib length error)
  Major opcode of failed request:  140 (RENDER)
  Minor opcode of failed request:  20 (RenderAddGlyphs)
  Serial number of failed request:  275
  Current serial number in output stream:  297

@guihkx
Copy link
Contributor

guihkx commented May 17, 2023

Based on what I found on Google, you could try one of these two things:

@Pr0mises
Copy link
Author

Pr0mises commented May 17, 2023

@guihkx as you can see a post above your first one I had the same problem while using the binary of Devil

X Error of failed request:  BadLength (poly request too large or internal Xlib length error)
  Major opcode of failed request:  140 (RENDER)
  Minor opcode of failed request:  20 (RenderAddGlyphs)
  Serial number of failed request:  276
  Current serial number in output stream:  298

After compiling the latest build (I didn't check your build) the executable is working even with the current build.spec file, which is an improvement from the last time I compiled it

@guihkx
Copy link
Contributor

guihkx commented May 17, 2023

Yeah, unfortunately it's also been my experience that packaging on the machine in which you intend to run the app works much better...

@guihkx
Copy link
Contributor

guihkx commented May 21, 2023

So it turns out that the X Error message was an libXft issue, which I think is caused by having an emoji font like Noto Emoji installed, and then libXft did not know how to use it to render emoji glyphs, or something like that.

Thankfully though, version 2.3.5 of libXft has fixed that ([1], [2]), but my build remained broken because Ubuntu 20.04 (which I'm using in the CI workflow) still has version 2.3.3 in its official repositories.

I worked around this simply by building a recent version of libXft and then telling PyInstaller to bundle the new version instead of the old system version.

Another fix I recently added is related to that weird await wasn't used with Future error (apparently caused by libssl failing to find certificates in distros like Arch and Fedora). So there's no need for that SSL_CERT_DIR workaround on non-Debian distros anymore!

Linux users, feel free to give the latest build a shot here: https://github.com/guihkx/TwitchDropsMiner/actions/runs/5037436242

The most important features of the app should now work as expected on virtually any Linux distro! 🤞


There are some other important issues that also need investigation, however (they are not related to packaging):

  • (FIXED) Attempting to close the app by using the [X] button doesn't really close it: The main window itself get closed, but the app keeps running in the background indefinitely, requiring you to terminate the process manually. :/

  • (FIXED) The "Minimize to Tray" feature is somewhat broken in weird ways:

    1. After minimized, the tray icon itself looks either invisible or distorted:
      KDE Plasma 5.27 (invisible):

      invisible-systray-icon-kde

      GNOME Shell 44 (needs a system tray extension) (distorted):

      distorted-systray-icon-gnome

      I tried to convert the pickaxe.ico to a PNG24 file instead, and while this workaround did fix the appearance of the tray icon on GNOME Shell:

      systray-icon-fixed-gnome

      It did absolutely nothing for KDE Plasma... Additionally, this change introduced the following warning upon hiding the app to tray:

      PIL/Image.py:992: UserWarning: Palette images with Transparency expressed in bytes should be converted to RGBA images
      

      I could fix the warning by making a PNG32 image instead, but then the tray icon gets invisible again on both GNOME and KDE Plasma, and we're back to square one. 😈 🤷‍♂️

    2. On KDE Plasma, interacting with the system tray icon does nothing, so you can't restore the app window once it's minimized to tray, which also requires you to manually terminate the process... :(

    3. On GNOME Shell, the issue above doesn't happen, but in compensation it takes about 5 seconds to restore the app from system tray:

    gnome-shell-restore-from-tray-huge-delay.mp4

Some other minor Linux-only issues I discovered:

  • (FIXED) The Autostart feature in the Settings tab doesn't work
  • (FIXED) System tray notifications don't work (YES THEY DO!)
  • (FIXED) Clickable URLs in the Help tab don't open (this one might be related to packaging, actually)

@DevilXD
Copy link
Owner

DevilXD commented May 21, 2023

The non-packaging related issues might be related to them having platform checks in place.

Attempting to close the app by using the [X] button doesn't really close it: The main window itself get closed, but the app keeps running in the background indefinitely, requiring you to terminate the process manually. :/

This one does two different things on Windows and Linux. On Windows, it hooks up to the app window's event queue and catches the event that is emitted when the system does it's shutdown sequence. On Linux, it uses the built-in protocol listener to catch the event of the user closing the window, then proceeds with the shutdown.

TwitchDropsMiner/gui.py

Lines 1873 to 1895 in 5b8b6c7

# gracefully handle Windows shutdown closing the application
if sys.platform == "win32":
# NOTE: this root.update() is required for the below to work - don't remove
root.update()
self._message_map = {
# window close request
win32con.WM_CLOSE: self.close,
# shutdown request
win32con.WM_QUERYENDSESSION: self.close,
}
# This hooks up the wnd_proc function as the message processor for the root window.
self.old_wnd_proc = win32gui.SetWindowLong(
self._handle, win32con.GWL_WNDPROC, self.wnd_proc
)
# This ensures all of this works when the application is withdrawn or iconified
ctypes.windll.user32.ShutdownBlockReasonCreate(
self._handle, ctypes.c_wchar_p(_("gui", "status", "exiting"))
)
# DEV NOTE: use this to remove the reason in the future
# ctypes.windll.user32.ShutdownBlockReasonDestroy(self._handle)
else:
# use old-style window closing protocol for non-windows platforms
root.protocol("WM_DESTROY_WINDOW", self.close)

Ref SO post: https://stackoverflow.com/questions/73497704/how-to-handle-a-wm-endsession-in-tkinter

The "Minimize to Tray" feature is somewhat broken in weird ways:

GNOME Shell 44 (needs a system tray extension)

Yes, that's listed in one of the Pystray issues: moses-palmer/pystray#29 (comment)
Not much I can do about it, other than using something other than Pystray for this functionality, or abolishing Tkinter and going with smth like PyQT. This would require an entire GUI rewrite though. I want to note that I've never done an application GUI before, and thus wanted to start with something basic and simple, like Tkinter Python comes with. By the time it's age and limitations became apparent, it was kinda too late to go back and redo it, especially since I was still learning and other libraries simply scared me. They don't scare me now, but it's also too late to change.

On KDE Plasma, interacting with the system tray icon does nothing, so you can't restore the app window once it's minimized to tray, which also requires you to manually terminate the process... :(

There is absolutely nothing in Pystray issues about that system. I could create a new issue and ask about it, but the last activity in the repo was a year ago, and the project seems kinda abandoned a bit. I can still try, but I think I'd need to install KDE Plasma on a VM first. You could also create an issue yourself, since you already have a way to test in some way, but still, I'm not sure where would that take us, really.

Does more mainstream Linux systems work fine? Ubuntu, Fedora, Debian? I'm not really a Linux person, but I always thought those three are the most common. If it works there, I'd say it's good enough already.

The Autostart feature in the Settings tab doesn't work

The code that handles autostart has an intentional check that makes it do nothing on non-Windows systems:

TwitchDropsMiner/gui.py

Lines 1566 to 1581 in 5b8b6c7

def update_autostart(self) -> None:
enabled = bool(self._vars["autostart"].get())
tray = bool(self._vars["tray"].get())
self._settings.autostart = enabled
self._settings.autostart_tray = tray
if sys.platform == "win32":
if enabled:
# NOTE: we need double quotes in case the path contains spaces
self_path = f'"{SELF_PATH.resolve()!s}"'
if tray:
self_path += " --tray"
with RegistryKey(self.AUTOSTART_KEY) as key:
key.set(self.AUTOSTART_NAME, ValueType.REG_SZ, self_path)
else:
with RegistryKey(self.AUTOSTART_KEY) as key:
key.delete(self.AUTOSTART_NAME, silent=True)

I just never learned how exactly one can do autostart on Linux. I know a way with systemd, but as far as I'm aware, that creates a service and thus needs a whole service definition file etc. Sounds way too complicated for a simple "run this when system starts". I just never sat down and figured out how to do it, really, so making it at least not crash the entire application (trying to import winreg on Linux results in a ModuleNotFoundError) sounded good enough "for now".

System tray notifications don't work

More Pystray problems. No idea, I couldn't find any issues on the Pystray repo about this either. They don't work at all, even on the mainstream systems?

Clickable URLs in the Help tab don't open (this one might be related to packaging, actually)

Link opening functionality is done via webbrowser standard lib Python module. It kinda has to work, unless the GUI fails to somehow call the respective function. There's no 3rd party lib being involved here. This is one of those things that'd need to be debugged on a working system, although the only thing that'd need debugging is figuring out if the function defined at L288 is called - if so, it's the Python that fails to open the page, not the miner.

TwitchDropsMiner/gui.py

Lines 271 to 289 in 5b8b6c7

class LinkLabel(ttk.Label):
def __init__(self, *args, link: str, **kwargs) -> None:
self._link: str = link
# style provides font and foreground color
if "style" not in kwargs:
kwargs["style"] = "Link.TLabel"
elif not kwargs["style"]:
super().__init__(*args, **kwargs)
return
if "cursor" not in kwargs:
kwargs["cursor"] = "hand2"
if "padding" not in kwargs:
# W, N, E, S
kwargs["padding"] = (0, 2, 0, 2)
super().__init__(*args, **kwargs)
self.bind("<ButtonRelease-1>", self.webopen(self._link))
def webopen(self, url: str):
return lambda e: webbrowser.open_new_tab(url)

@guihkx
Copy link
Contributor

guihkx commented May 21, 2023

The non-packaging related issues might be related to them having platform checks in place.

Indeed. Plus, I haven't really investigated them properly yet, so for now these are here only for documentation purposes. :P

On Linux, it uses the built-in protocol listener to catch the event of the user closing the window, then proceeds with the shutdown.

Thanks for the pointer! After a quick Google search, this one was actually very easy to fix: I just had to replace the WM_DESTROY_WINDOW protocol by WM_DELETE_WINDOW instead. Now the app terminates properly after closing the window. :D

Yes, that's listed in one of the Pystray issues: moses-palmer/pystray#29 (comment)
Not much I can do about it, other than using something other than Pystray for this functionality, or abolishing Tkinter and going with smth like PyQT.

I think there was a misunderstanding here, hahah. The comment I made about GNOME needing a system tray extension was just an observation, really. xD

Unfortunately, it is known that the GNOME desktop hasn't had the concept of a system tray for a very long time now... In fact, popular distros like Ubuntu include that system tray extension by default, because for many it's still a crucial feature, but GNOME developers apparently don't think so. 🤷‍♂️

So no worries, a GUI rewrite is not really needed because that's definitely not on you. :P

I could create a new issue and ask about it, but the last activity in the repo was a year ago, and the project seems kinda abandoned a bit

Don't sweat about it. Like I said, I didn't really investigate any of these problems thoroughly just yet. But I'll let you know if I find anything.

I just never learned how exactly one can do autostart on Linux. I know a way with systemd, but as far as I'm aware, that creates a service and thus needs a whole service definition file etc. Sounds way too complicated for a simple "run this when system starts".

Yeah, that's not what we usually do for desktop (GUI) applications on Linux. Instead, most apps ship with a desktop file, which you can then simply copy to the ~/.config/autostart directory and then your desktop environment should take care of the rest for you.

I'm not a Python dev by any means, but I'm pretty sure I could implement this very easily. I just can't guarantee that the quality of the code will be stellar... 😅

More Pystray problems. No idea, I couldn't find any issues on the Pystray repo about this either. They don't work at all, even on the mainstream systems?

I haven't really tested this particular feature on different distros because it takes some time for these desktop notifications to be triggered. I'm sure I could create a simple test case for this, but to be honest it's not really at the top of my priorities list at the moment.

Ironically though, if you run the Windows build of the app on Linux using Wine (which is something I was actually doing before working on problems from the Linux build), tray notifications work just fine:

wine-lmao

Not only that, but running the Windows app on Linux using Wine has none of the issues I mentioned in my previous post (except for the autostart one, but that's by design)... 😆

Link opening functionality is done via webbrowser standard lib Python module. It kinda has to work, unless the GUI fails to somehow call the respective function. There's no 3rd party lib being involved here

Hmm, that's good to know and it will definitely be useful when I debug this.

My suspicion that this is a packaging-related issue is because opening these links actually works when I run the app without being packaged by PyInstaller. I could be wrong, though.

Whenever I click on a link, this message pops up (not sure if it's just a red herring):

subprocess.py:1072: ResourceWarning: subprocess 75220 is still running
ResourceWarning: Enable tracemalloc to get the object allocation traceback

Anyway, I'll try to look into those thoroughly eventually.

@DevilXD
Copy link
Owner

DevilXD commented May 21, 2023

Thanks for the pointer! After a quick Google search, this one was actually very easy to fix: I just had to replace the WM_DESTROY_WINDOW protocol by WM_DELETE_WINDOW instead. Now the app terminates properly after closing the window. :D

Cool! Just a caution note from me though, even though WM_DESTROY_WINDOW doesn't appear to work, there's no harm in registering callbacks for both events here. So instead of replacing the existing listener, just add a second listener that listens for the other event, but still calls the same function, and it should be good.


Yeah, that's not what we usually do for desktop (GUI) applications on Linux. Instead, most apps ship with a desktop file, which you can then simply copy to the ~/.config/autostart directory and then your desktop environment should take care of the rest for you.

I'm not a Python dev by any means, but I'm pretty sure I could implement this very easily. I just can't guarantee that the quality of the code will be stellar... 😅

Oh, it's fine! You can try writing some code for it if you'd want to. If it's just a matter of dropping a text file into that path though, it'll be something like:

# this import needs to be added at the top
from textwrap import dedent

# rest of the code

# this adds double quotes around the path to the executable
# if that wouldn't be needed, just use: self_path = str(SELF_PATH.resolve())
self_path = f'"{SELF_PATH.resolve()!s}"'
if tray:
    self_path += " --tray" 

# contents of the desktop file - modify this up to what you think is needed
# 'dedent' is used to remove the 4 spaces from in front of each of the lines,
# so the code still looks nice while the file has them omitted
contents = dedent(f"""
    [Twitch Drops Miner]
    Exec={self_path}
    Name=Twitch Drops Miner
"""

# this is what actually opens the file (in create-or-overwrite mode), writes the contents, then closes it
with open("~/.config/autostart/TDM.desktop", "w", encoding="utf8") as file:
    file.write(contents)

# use this to delete the file when the option is unticked
try:
    os.unlink("~/.config/autostart/TDM.desktop")
except FileNotFoundError:
    pass

Ironically though, if you run the Windows build of the app on Linux using Wine (which is something I was actually doing before working on problems from the Linux build), tray notifications work just fine:

Not only that, but running the Windows app on Linux using Wine has none of the issues I mentioned in my previous post (except for the autostart one, but that's by design)... 😆

Funny. I've heard about Wine a long time ago, when someone tried to run Windows-only games on Linux. Sounds cool. "Try using Wine" sounds like a useful note to add for anyone who'd like to try using this on Linux in the future for sure.


Hmm, that's good to know and it will definitely be useful when I debug this.

Whenever I click on a link, this message pops up (not sure if it's just a red herring)

After some searching around, it seems to be an open Python bug. Refs:
https://bugs.python.org/issue27069
Current Github issue: python/cpython#50243

Not sure what to do here. There's no going around this one, unfortunately. The only way to open URLs without the standard lib would be using a 3rd party lib, which I'd like to avoid. What do you think about the discussion under that issue? I'm not sure how scary those "zombie processes" are, is that something to worry about, or not really?

@guihkx
Copy link
Contributor

guihkx commented May 28, 2023

Cool! Just a caution note from me though, even though WM_DESTROY_WINDOW doesn't appear to work, there's no harm in registering callbacks for both events here.

Good to know. Thanks!

Oh, it's fine! You can try writing some code for it if you'd want to. If it's just a matter of dropping a text file into that path though, it'll be something like:

Thanks for that. I haven't tried implementing it yet, but your snippet will definitely help!

"Try using Wine" sounds like a useful note to add for anyone who'd like to try using this on Linux in the future for sure.

Most definitely! I haven't had a single issue by running TDM on Linux using Wine. The RAM usage is a bit higher than running the native Linux version, but other than that, I'd say it's just as good as running on a real Windows machine.

Not sure what to do here. There's no going around this one, unfortunately. The only way to open URLs without the standard lib would be using a 3rd party lib, which I'd like to avoid.

Hmm, I don't know, but I believe the issues you linked don't seem related to the actual problem. Without being packaged by PyInstaller, webbrowser works just fine.

I found this PyInstaller issue:

pyinstaller/pyinstaller#4835

And even though the original reporter is using Windows, if you scroll at the end of the thread, you'll see this comment, which based on my Linux experience, seems very plausible to be the actual root cause of the problem, which is caused by PyInstaller modifying LD_LIBRARY_PATH on Linux and then causing the unintended side effect of 'breaking' installed applications when they're attempted to be launched by webbrowser.

The person even posted a workaround that looks very promising, but I haven't actually tried it yet.


Regarding the tray icon issue, it was never about the format of the icon (ICO or PNG), but instead simply because pystray.run_detached() does not work properly on Linux:

moses-palmer/pystray#123

If I replace that by just run(), then the icon shows up correctly, but that will block the rest the app, of course.

My attempt at implementing the tray icon in a separate thread as suggested by the creator of pystray was unsuccessful, mainly because I have no idea of what I'm doing. :D

@DevilXD
Copy link
Owner

DevilXD commented May 28, 2023

which is caused by PyInstaller modifying LD_LIBRARY_PATH on Linux and then causing the unintended side effect of 'breaking' installed applications when they're attempted to be launched by webbrowser.
pyinstaller/pyinstaller#4835

That looks easy to implement. The suggested code seems good enough to remedy the issue as well. The usage could be simplified into a simpler context manager though:

import os
from contextlib import contextmanager

@contextmanager
def restore_lib_path():
    lib_key = "LD_LIBRARY_PATH"
    if (curr_env := os.environ.get(lib_key)) is not None:
        if (orig_env := os.environ.get(f"{lib_key}_ORIG")) is None:
            os.environ.pop(lib_key)
        else:
            os.environ[lib_key] = orig_env
    yield
    if curr_env is not None:
        os.environ[lib_key] = curr_env

def open_link(url: str):
    with restore_lib_path():
        webbrowser.open_new_tab(url)

All of the above assumes this LD_LIBRARY_PATH env variable can't be just set back to the original once, after the program starts, instead of removing and readding it from the evn variables dict every time the user wants to open a link. If that'd be so, everything before the yield in that restoring function can be placed somewhere in main.py and it'd work too.


pystray.run_detached() does not work properly on Linux

Ah, well, I used run_detached as a lazy solution to the problem, really, as otherwise I'd need to setup the separate thread to run the icon in myself, while this was a one-liner solution that did the same for me already, so it was silly not to just use it.

But, since it doesn't work on Linux, it seems I'll need to do it "the hard way" anyway 😅 To run something in a separate thread, the default loop executor could be used: https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.run_in_executor There's a chance it wouldn't work either, in which case threading and a new thread would do the trick too.


I can prepare a branch that will have those implemented, together with the autostart bit implemented too, if you'd like. I'm currently struggling with being slightly overworked and can't really find the time for this project for now, but I'll try making this branch for you, if it'd allow you to continue testing the Linux build.

@guihkx
Copy link
Contributor

guihkx commented May 28, 2023

I can prepare a branch that will have those implemented

That would be incredible, thank you!

I'm currently struggling with being slightly overworked and can't really find the time for this project for now, but I'll try making this branch for you, if it'd allow you to continue testing the Linux build.

Don't even worry about it, I completely understand! And take your time too, the last thing I want is to take the fun out of working on personal a project. If ultimately you don't find the time to do it, I'll keep working on it myself. ^^

As a matter of fact, I am (slowly) trying to implement those changes, but without proper Python expertise they will probably end up looking like crap. XD

Here are the changes I added so far (feel free to change any of them):

master...guihkx:TwitchDropsMiner:linux

(btw, I tested that LD_LIBRARY_PATH workaround I just added and it worked!)

@DevilXD
Copy link
Owner

DevilXD commented May 29, 2023

without proper Python expertise they will probably end up looking like crap.

No worries, when I was starting out with Python, my code was less than ideal too x) All I can say, is that you're more than welcome to try and make things work for now, and we can handle "make code look nice" later.

(btw, I tested that LD_LIBRARY_PATH workaround I just added and it worked!)

Great! That's one less issue then :)

@DevilXD
Copy link
Owner

DevilXD commented May 29, 2023

I've implemented all 3 changes we've talked about here. The one replacing the environment variable has received a little rewrite, because MyPy was arguing that PyInstaller may not set LD_LIBRARY_PATH after all. The current solution ensures those two cases (current not set, original is or isn't there) are also handled as expected.

I did exactly zero testing of these, beyond doing a static typing check via MyPy. If you can, please check those.

Diff of changes: guihkx/TwitchDropsMiner@linux...DevilXD:TwitchDropsMiner:linux

@guihkx
Copy link
Contributor

guihkx commented May 29, 2023

Awesome, thanks! I have some feedback.

The autostart option is still borked, likely because the ~/.config/autostart/TDM.desktop path does not get expanded correctly. This shows up in the terminal when I enable the option:

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.11/tkinter/__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "/home/gui/dev/TwitchDropsMiner/gui.py", line 1596, in update_autostart
    with open(self.AUTOSTART_PATH, "w", encoding="utf8") as file:
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [Errno 2] No such file or directory: '~/.config/autostart/TDM.desktop'

On Linux, the ~ character is used to conventionally expand paths to the current user's home folder, e.g. /home/gui/.

However, the actual, standardized way to do create this .desktop file in the autostart directory would be:

  1. Check if the XDG_CONFIG_HOME env var is set and it's a valid path to a folder.
  2. If condition 1 is true, then we write the .desktop file to the $XDG_CONFIG_HOME/autostart/ folder (create the autostart directory if necessary)
  3. If condition 1 is false, we use $HOME/.config/autostart/ instead (create the autostart folder if necessary)

Still regarding the .desktop file, it looks almost correct: You just have to replace [Twitch Drops Miner] by [Desktop Entry].

I'd also choose a more descriptive name for the file itself, perhaps TwitchDropsMiner.desktop?


Unfortunately, commit d886b7d does not work correctly. :/

While the tray icon does get added after I click on 'Minimize to Tray', immediately the main window gets completely unresponsive and you can't quit the app normally:

frozen.mp4

@DevilXD
Copy link
Owner

DevilXD commented May 30, 2023

Just to be clear, I didn't expect things to work on the first try x)

While the tray icon does get added after I click on 'Minimize to Tray', immediately the main window gets completely unresponsive and you can't quit the app normally:

That's a classic mistake of me. run_in_executor accepts a function that will be called in the new thread. Instead, I called the function in the same thread by mistake, while trying to pass it in. Ouchies.

Please try this: db7dc23


Regarding the autostart feature, I've switched the logic to using the Path objects instead of simple strings. Path objects have an expanduser method, which can resolve the ~ character automatically. Also, I've added a path overwrite if XDG_CONFIG_HOME is defined and points to an existing folder, used a more descriptive file name and [Desktop Entry] in the file contents. I think I got everything, please give those changes a try as well.

Changes: a9605d2

@DevilXD DevilXD added the Dev For interminent issues created during development between releases label May 31, 2023
@guihkx
Copy link
Contributor

guihkx commented Jun 3, 2023

Sorry for the delay... :S

Just to be clear, I didn't expect things to work on the first try x)

Hahah yeah of course. No worries. :)

Please try this: db7dc23

That's so much better now! I found a minor issue, but as far as I'm concerned, it could be left as-is because it don't seem to break the app in any way (I think):

The issue is that, once created, the tray icon is permanent through the lifetime of the app:

permanent-tray-icon.mp4

This causes a rather funny behavior as well: If the app is not currently minimized to tray, the tray icon is not interactive (i.e. left/right clicks yield nothing).

But as soon as you click on 'Minimize to Tray' from the app, the tray icon becomes interactive again, and immediately dispatches any previous left/right clicks. And after that, at least on KDE Plasma, the tray icon gets invisible 😭:

lol.mp4

But like I said, I don't think these are major issues. And I think that if you're able to destroy the icon after the app is restored from tray, these two issues will likely go away.

Still regarding the tray icon, I just learned that pystray supports many system tray implementations on Linux. The current best one seems to be AppIndicator.

However, this implementation requires quite a lot of native libraries, which ends up increasing the final size of the app quite considerably: From ~25 MB to ~80 MB (after being UPX-compressed).

If the AppIndicator library is not found on the system, pystray tries to use the gtk implementation instead, but according to the docs, "it may result in an invisible icon", which explains that issue I was having a few posts earlier, since I did not have the required AppIndicator library installed!

Regarding the autostart feature...
Changes: a9605d2

That didn't work for me, unfortunately. The standard mentions that the Type property is mandatory (i.e. add Type=Application there).

Additionally, the Exec key looks a bit off as well. Right now, its value can end up looking like this:

Exec="/home/user/Downloads/Twitch Drops Miner/Twitch Drops Miner (by DevilXD)" --tray

I think that wrapping the path of the binary in quotes isn't really supported...? At least it doesn't work on KDE Plasma. So you can remove those. However, now you'd also have to escape a bunch of special characters, which is really annoying. A much easier way to do this would be:

Exec=sh -c '"/home/user/Downloads/Twitch Drops Miner/Twitch Drops Miner (by DevilXD)" --tray'

However, this approach could be problematic if the path has quotes or apostrophes, but I don't think those are common in paths anyway, so whatever. In any case, if you want to try to develop a "perfect solution", here's the specification for the Exec key so you can have nightmares.

FYI: I've cleanup some commits from my linux branch (I did include your changes too), so you might want to hard reset your branch against mine, just so you can get the most recent changes I added.

Thanks!

@DevilXD
Copy link
Owner

DevilXD commented Jun 3, 2023

And I think that if you're able to destroy the icon after the app is restored from tray, these two issues will likely go away.

The thing is, that's kinda already what happens. When the program's main window is restored, the icon's thread is stopped, and the icon itself gets its last reference removed, letting Python dealocate it at the earliest applicable time:

TwitchDropsMiner/gui.py

Lines 1081 to 1084 in 4664a9d

def stop(self):
if self.icon is not None:
self.icon.stop()
self.icon = None

There's not much more I can do/call here, besides the stop() call that already is done. There's no "destroy" or "delete" method on the icon. Really not sure what to do with this now.


Still regarding the tray icon, I just learned that pystray supports many system tray implementations on Linux. The current best one seems to be AppIndicator.

However, this implementation requires quite a lot of native libraries, which ends up increasing the final size of the app quite considerably: From ~20 MB to ~80 MB (after being UPX-compressed).

If the AppIndicator library is not found on the system, pystray tries to use the gtk implementation instead, but according to the docs, "it may result in an invisible icon", which explains that issue I was having a few posts earlier, since I did not have the required AppIndicator library installed!

This is selected automatically by Pystray, which handles the tray icon. Not entirely sure if I can influence the choice it makes. It will pick the best possible implementation it can use, and that's it. Maybe the final doc should recommend installing "AppIndicator" as it seems to be more reliable?


Regarding autostart issues, adding in Type is trivial. Adding quotes can be disabled too, it's just that they're needed on Windows around paths, that may end up containing spaces. Without them, any space can break the path. If Linux doesn't mind spaces, I can remove them.

@guihkx
Copy link
Contributor

guihkx commented Jun 3, 2023

There's not much more I can do/call here, besides the stop() call that already is done. There's no "destroy" or "delete" method on the icon. Really not sure what to do with this now.

Oh :(

Oh well, then you can leave it as is. No biggie.

This is selected automatically by Pystray, which handles the tray icon. Not entirely sure if I can influence the choice it makes. It will pick the best possible implementation it can use, and that's it. Maybe the final doc should recommend installing "AppIndicator" as it seems to be more reliable?

After a lot of trouble learning how PyInstaller works, I was able to include all the required dependencies for the AppIndicator backend (the changes are in this commit), so the user won't have to install these libraries manually. The app is a bit larger because of that, but IMO we're making a good trade-off here.

Without them, any space can break the path. If Linux doesn't mind spaces, I can remove them.

It's a similar situation on Linux, actually. But from what I could understand from the specification, we'd have to escape white space characters ( -> \ ) instead of wrapping everything in quotes.

Now, personally I tried this solution and it didn't work, so I still think the sh -c approach I mentioned is better.

@DevilXD
Copy link
Owner

DevilXD commented Jun 4, 2023

Alright :)

I've updated my linux branch with your latest changes. Cleaned up some of the code. Please give it a go and let me know how it performs now.

@guihkx
Copy link
Contributor

guihkx commented Jun 4, 2023

I've updated my linux branch with your latest changes. Cleaned up some of the code.

Thanks! And sorry for the mess of duplicates commit caused; I tend to rebase my branch quite often to keep it as clean as possible, and if you do a git merge guihkx/linux it instead of a git rebase -i guihkx/linux (or git reset --hard guihkx/linux), you will end up with duplicate commits.

Speaking of which, I've included your recent cleanups and rebased again, so keep that in mind... 😅

Please give it a go and let me know how it performs now.

Nice, it works great now!


I've been trying to find a better approach for the tray icon problem, and I think I came up with a decent solution.

Instead of stop()ping pystray every time we minimize to tray, on Linux we can just hide the icon instead (by changing the icon.visible property). I tested this approach on the two most popular desktop environments on Linux - KDE Plasma 5.27 and GNOME 44 -, and it worked great!

Note that this particular change requires we switch to the master version of pystray (we need the changes from this PR).

My implementation itself you can find here, so feel free to make it better if you want, and I can incorporate it later, as usual. :P

Remaining issue I could find:

  • Because the concept of tray notifications doesn't exist on Linux (we only have "regular" desktop notifications), pystray doesn't support it. Perhaps disabling any options related to notifications on Linux is good enough. Or maybe a new dependency could be introduced to properly support desktop notifications on Linux... Your call.

Overall, I'd say that's some awesome progress! Thanks a lot for the help!

@DevilXD
Copy link
Owner

DevilXD commented Jun 5, 2023

I tend to rebase my branch quite often to keep it as clean as possible, and if you do a git merge guihkx/linux it instead of a git rebase -i guihkx/linux (or git reset --hard guihkx/linux), you will end up with duplicate commits.

My git skills aren't the greatest, I usually google most of the commands. The linux branch will most likely end up getting squashed before merging anyway, so I don't really mind having merge commits. They keep a chronological order of things being added as time goes. Rebasing makes commits disappear from one place and reappear in another, and I always found this confusing, seeing the same commit names over and over, always trying to remember if I already reviewed them or not. I'm one of those people who just like the usual merging of things, even though I know that going back the "tree" merge commits create can be tricky sometimes.

That being said, if I won't forget, I'll try rebasing next time. I don't ever recall doing so, so I imagine I'll mess something up anyway. My VSCode does most git things for me already, so hopefully it won't be too bad. Hopefully >.>


icon.visible property

Sounds okay. To avoid complicating the logic, the icon could work like this on Windows as well. I never explored using visibility to hide the icon, simply because the current implementation already worked, so there was no incentive for me to do so.

EDIT: Just force-replaced my local linux branch with yours, and changed some things around to use icon.visible to hide and show the icon on all systems. Did a quick test and it works smooth on Windows.

Does this remove the AppIndicator requirement, or is it still needed, just more reliable?


Because the concept of tray notifications doesn't exist on Linux (we only have "regular" desktop notifications), pystray doesn't support it. Perhaps disabling any options related to notifications on Linux is good enough. Or maybe a new dependency could be introduced to properly support desktop notifications on Linux... Your call.

I did a quick research on this, and this library looks like it could do: https://pypi.org/project/desktop-notifier/

Windows notifications are supported, but we'd only really be interested in using it for Linux. Async interface will integrate well with the existing application. If whatever the library's page reads sounds enough for you, I can try adding to the linux branch some test code utilizing it. Or you can try implementing something yourself, if you'd like to try.

@guihkx
Copy link
Contributor

guihkx commented Jun 5, 2023

The linux branch will most likely end up getting squashed before merging anyway, so I don't really mind having merge commits.

Indeed. And in the end it's just a matter of preference, I guess. :p

Does this remove the AppIndicator requirement, or is it still needed, just more reliable?

Interestingly enough, I remember trying the previous backend (GtkStatusIcon) yesterday and I was having some issues. But I just briefly tested now, and the issue seem to be gone. But I'd like to test a little bit more. If GtkStatusIcon proves to be solid, I'll remove the AppIndicator changes. :)

One minor issue that I immediately noticed with GtkStatusIcon, is that the icon looks blurry (we probably can't do anything about it either):

image

And with AppIndicator the icon looks fine:

image

EDIT: Just force-replaced my local linux branch with yours, and changed some things around to use icon.visible to hide and show the icon on all systems. Did a quick test and it works smooth on Windows.

That's great. However, I tested your changes on Linux, and for some odd reason, when I minimize to tray for the first time, the icon ends up hiding as well. I narrowed the issue down to this line:

self.icon.visible = True

And I fixed it with this diff:

diff --git a/gui.py b/gui.py
index 2092a93..5781e67 100644
--- a/gui.py
+++ b/gui.py
@@ -1087,7 +1087,8 @@ class TrayIcon:
         if self.icon is None:
             self._start()
             assert self.icon is not None
-        self.icon.visible = True
+        else:
+            self.icon.visible = True
         self._manager._root.withdraw()
 
     def restore(self):

It's probably a bug in pystray. But can you check if the change above doesn't break things on Windows? If it doesn't, then I think it's safe to use it.

I did a quick research on this, and this library looks like it could do: https://pypi.org/project/desktop-notifier/

That looks like a solid project to me.

Or you can try implementing something yourself, if you'd like to try.

I could give it a shot implementing it, yes. But honestly it might take days, especially since I'm not too familiar with the code base (or with Python, for that matter lol). So, if you have the time and will to do it, be my guest. 😅

@DevilXD
Copy link
Owner

DevilXD commented Jun 6, 2023

RE: blurry icon - it's really up to decide. Someone needs to decide if having a slightly blurry icon is worth cutting down the app size in half. Since I'm not a Linux user, you'll have to decide on it :p

RE: icon.visible - Back when I started using Pystray, some studying of the code showed me that icon.visible is set to True automatically by the icon.run call, shortly after the run method finishes actually setting up the icon (requesting system resources, loading the icon image files, etc.). By doing an icon.run in a separate thread, and immediately setting icon.visible in the current thread, it was possible that this whole setup process broke somehow. Adding an else: like you've suggested fixes this. The behavior on Windows doesn't change, so if it works for Linux, I'd call it good enough :)

RE: desktop-notifier mockup for testing - I'll try to find some time for a test program. If everything works, I'll try implementing something in the actual application. I've been busy lately, so it may take a while, but I'll do it eventually.

@guihkx
Copy link
Contributor

guihkx commented Jun 8, 2023

Someone needs to decide if having a slightly blurry icon is worth cutting down the app size in half. Since I'm not a Linux user, you'll have to decide on it :p

The size increase is caused by libraries related to Gtk (which are needed by PyGObject). However, even though GtkStatusIcon doesn't need PyGObject, I think it still needs most of the same Gtk libraries to work, so in the end I don't think it will make much difference... I say let's keep the AppIndicator one.

Adding an else: like you've suggested fixes this. The behavior on Windows doesn't change, so if it works for Linux, I'd call it good enough :)

Awesome! :D

RE: desktop-notifier mockup for testing - I'll try to find some time for a test program. If everything works, I'll try implementing something in the actual application. I've been busy lately, so it may take a while, but I'll do it eventually.

Thank you! No need to rush.

@guihkx
Copy link
Contributor

guihkx commented Jun 10, 2023

@DevilXD So... It turns out desktop notifications are already supported on Linux by pystray. My bad! 🤡

When the documentation stated "notifications aren't supported on macOS and Xorg", I mistakenly assumed they meant Linux as a whole.

But that literally means only the Xorg backend of pystray doesn't support notifications. The others do... 🤦‍♂️

In fact, I confirmed that just now:

image

So I'd say the Linux build is pretty much ready for the masses!

Assuming you want these changes merged, how do you want to proceed now? Should I open pull requests for each of my commits? Or all changes in a single PR? Some other way you prefer...?

By the way, in my latest change I was able to reduce the size of the Linux build by 23.8 MiB!

It went from 78.7 MiB down to 54.9 MiB. Still not as small as the Windows build, but much better than before. :P

@DevilXD
Copy link
Owner

DevilXD commented Jun 10, 2023

Bruh x) Alright then, that's good news. Happy about the size reduction :)

I'd prefer a single PR. As far as commits go, it's either merge as-is, or squash first. I'll decide between the two, when it's time to do so, but probably just merge it in, unless there's many commits, then squash instead.

There's one more thing remaining. The Linux build has to have added a short description/explanation in the README.md file. A short paragraph explaining which systems are supported, what needs to be done to make it work on other systems, the tip about Wine usage we've talked about earlier, this Xorg note about notifications, AppIndicator being bundled by default to increase reliability, etc. These are from the top of my head.

This addition should be a part of the PR. You're much more familiar with the subject. Would you be able to write something up? A simple bullet point list would do.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Dev For interminent issues created during development between releases Enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants