Search code examples
bashzenitygcr.io

Why is this if statement forcing zenity's --auto-close to close immediately?


Got a Debian package set up for a little application I was working on, this isn't really relevant for the question but for some context, the application is a simple bash script that deploys some docker containers on the local machine. But I wanted to add a dependency check to make sure the system had docker before it attempted to do anything. If it doesn't, download it, if it does, ignore it. Figured it be nice to have a little zenity dialog alongside it to show what was going on.

In that process, I check for internet before starting for obvious reasons and for some reason, the way I check if there is internet if zenity has the --auto-close flag, will instantly close the entire progress block.

Here is a little dummy example, that if statement is a straight copy-paste from my code, everything else is filler. :

#!/bin/bash
condition=0
if [[ $condition ]]; then
(
    echo "0"
    # Check for internet
    if ping -c 3 -W 3 gcr.io; then
        echo "# Internet detected, starting updates..."; sleep 1
        echo "10"
    else            
        err_msg="# No internet detected. You may be missing some dependencies. 
        Services may not function as expected until they are installed."
        echo $err_msg
        zenity --error --text="$err_msg"
        echo "100"
        exit 1
    fi
    
    echo "15"

    echo "# Downloading a thing" ; sleep 1
    echo "50" 
    if zenity --question --text="Do you want to download a special thing?"; then
        echo "# Downloading special thing" ; sleep 1
    else
        echo "# Not downloading special thing" ; sleep 1
    fi
    echo "75" 
    echo "# downloading big thing" ; sleep 3
    echo "90"
    echo "# Downloading last thing" ; sleep 1
    echo "100" 
) | 
zenity --progress --title="Dependency Management" --text="downloading dependencies, please wait..." \
--percentage=0 --auto-close
fi

So im really just wondering why this is making zenity freak-out. If you comment out that if statement, everything works as you expect and zenity progress screen closes once it hits 100. If you keep the if statement but remove the auto-close flag, it will execute as expected. It's like its initializing at 100 and then going to 0 to progress normally. But if that was the case, --auto-close would never work but in the little example they give you in the help section, it works just fine. https://help.gnome.org/users/zenity/stable/progress.html.en


Solution

  • Thank you for a fun puzzle! Spoiler is at the end, but I thought it might be helpful to look over my shoulder while I poked at the problem. 😀️ If you're more interested in the answer than the journey, feel free to scroll. I'll never know, anyway.

    Following my own advice (see 1st comment beneath the question), I set out to create a small, self-contained, complete example. But, as they say in tech support: Before you can debug the problem, you need to debug the customer. (No offense; I'm a terrible witness myself unless I know ahead of time that someone's going to need to reproduce a problem I've found.)

    I interpreted your comment about checking for Internet to mean "it worked before I added the ping and failed afterward," so the most sensible course of action seemed to be commenting out that part of the code... and then it worked! So what happens differently when the ping is added?

    Changes in timing wouldn't make sense, so the problem must be that ping generates output that gets piped to zenity. So I changed the command to redirect its output to the bit bucket:

    ping -c 3 -W 3 gcr.io &>/dev/null;

    ...and that worked, too! Interesting!

    I explored what turned out to be a few ratholes:

    • I ran ping from the command line and piped its output through od -xa to check for weird control characters, but nope.
    • Instead of enclosing the contents of the if block in parentheses (()), which executes the commands in a sub-shell, I tried braces ({}) to execute them in the same shell. Nope, again.
    • I tried a bunch of other embarrassingly useless and time-consuming ideas. Nope, nope, and nope.

    Then I realized I could just do

    ping -c 3 -W 3 gcr.io | zenity --progress --auto-close

    directly from the command line. That failed with the --auto-close flag but worked normally without it. Boy, did that simplify things! That's about as "smallest" as you can get. But it's not, actually: I used up all of my remaining intelligence points for the day by redirecting the output from ping into a file, so I could just

    (cat output; sleep 1) | zenity --progress --auto-close

    and not keep poking at poor gcr.io until I finally figured this thing out. (The sleep gave me enough time to see the pop-up when it worked, because zenity exits when the pipe closes at the end of the input. So, what's in that output file?

    PING gcr.io (172.253.122.82) 56(84) bytes of data.
    64 bytes from bh-in-f82.1e100.net (172.253.122.82): icmp_seq=1 ttl=59 time=18.5 ms
    64 bytes from bh-in-f82.1e100.net (172.253.122.82): icmp_seq=2 ttl=59 time=21.8 ms
    64 bytes from bh-in-f82.1e100.net (172.253.122.82): icmp_seq=3 ttl=59 time=21.4 ms
    
    --- gcr.io ping statistics ---
    3 packets transmitted, 3 received, 0% packet loss, time 2003ms
    rtt min/avg/max/mdev = 18.537/20.572/21.799/1.449 ms
    

    The magic zenity-killer must be in there somewhere! All that was left (ha, "all"!) was to make my "smallest" example even smaller by deleting pieces of the file until it stopped breaking. Then I'd put back whatever I'd deleted last, and I deleted something else, da capo, ad nauseam, or at least ad minimus. (Or whatever; I don't speak Latin.) Eventually the file dwindled to

    64 bytes from bh-in-f82.1e100.net (172.253.122.82): icmp_seq=1 ttl=59 time=18.5 ms
    

    and I started deleting stuff from the beginning. Eventually I found that it would break regardless of the length of the line, as long as it started with a number that wasn't 0 and had at least 3 digits somewhere within it. Huh. It'd also break if it did start with a 0 and had at least 4 digits within... unless the second digit was also 0! What's more, a period would make it even weirder: none of the digits anywhere after the period would make it break, no matter what they were.

    And then, then came the ah-ha! moment. The zenity documentation says:

    Zenity reads data from standard input line by line. If a line is prefixed with #, the text is updated with the text on that line. If a line contains only a number, the percentage is updated with that number.

    Wow, really? It can't be that ridiculous, can it?

    I found the source for zenity, downloaded it, extracted it (with tar -xf zenity-3.42.1.tar.xz), opened progress.c, and found the function that checks to see if "a line contains only a number." The function is called only if the first character in the line is a number.

    108 static float
    109 stof(const char* s) {
    110         float rez = 0, fact = 1;
    111         if (*s == '-') {
    112                 s++;
    113                 fact = -1;
    114         }
    115         for (int point_seen = 0; *s; s++) {
    116                 if (*s == '.' || *s == ',') {
    117                         point_seen = 1;
    118                         continue;
    119                 }
    120                 int d = *s - '0';
    121                 if (d >= 0 && d <= 9) {
    122                         if (point_seen) fact /= 10.0f;
    123                         rez = rez * 10.0f + (float)d;
    124                 }
    125         }
    126         return rez * fact;
    127 }
    

    Do you see it yet? Here, I'll give you a sscce, with comments:

          // Clear the "found a decimal point" flag and iterate
          // through the input in `s`.
    115   for (int point_seen = 0; *s; s++) {
            // If the next char is a decimal point (or a comma,
            // for Europeans), set the "found it" flag and check
            // the next character.
    116     if (*s == '.' || *s == ',') {
    117       point_seen = 1;
    118       continue;
    119     }
            // Sneaky C trick that converts a numeric character
            // to its integer value. Ex: char '1' becomes int 1.
    120     int d = *s - '0';
            // We only care if it's actually an integer; skip anything else.
    121     if (d >= 0 && d <= 9) {
              // If we saw a decimal point, we're looking at tenths,
              // hundredths, thousandths, etc., so we'll need to adjust
              // the final result. (Note from the peanut gallery: this is
              // just ridiculous. A progress bar doesn't need to be this
              // accurate. Just quit at the first decimal point instead
              // of trying to be "clever." 
    122       if (point_seen) fact /= 10.0f;
              // Tack the new digit onto the end of the "rez"ult.
              // Ex: if rez = 12.0 and d = 5, this is 12.0 * 10.0 + 5. = 125.
    123       rez = rez * 10.0f + (float)d;
    124     }
    125   }
          // We've scanned the entire line, so adjust the result to account
          // for the decimal point and return the number.
    126   return rez * fact;
    

    Now do you see it?

    The author decides "[i]f a line contains only a number" by checking (only!) that the first character is a number. If it is, then it plucks out all the digits (and the first decimal, if there is one), mashes them all together, and returns whatever it found, ignoring anything else it may have seen.

    So of course it failed if there were 3 digits and the first wasn't 0, or if there were 4 digits and the first 2 weren't 0... because a 3-digit number is always at least 100, and zenity will --auto-close as soon as the progress is 100 or higher.


    Spoiler:

    The ping statement generates output that confuses zenity into thinking the progress has reached 100%, so it closes the dialog.

    By the way, congratulations: you found one of the rookiest kinds of rookie mistakes a programmer can make... and it's not your bug! For whatever reason, the author of zenity decided to roll their own function to convert a line of text to a floating-point number, and it doesn't do at all what the doc says, or what any normal person would expect it to do. (Protip: libraries will do this for you, and they'll actually work most of the time.)

    You can score a bunch of karma points if you can figure out how to report the bug, and you'll get a bonus if you submit your report in the form of a fix. 😀️