Perl Programmer/Consultant
Remote System Administrator
Free Software
... contact me
 
  while ($making_other_plans) { life(); }
  location('ipsstb', 'perl_programming', 'curses_ui_notebook_bug');

 For Web Designers 2018-09-23 14:39:49 UTC Mail Delivery Problems? 

Saturday, February 02 2013

Curses::UI::Notebook Delete Page Bug

The Problem: Curses::UI::Notebook delete_page() crashes the application.


  #!/usr/bin/perl

  use strict;
  use Curses::UI;

  my $ui = Curses::UI->new();

  my $win = $ui->add('win1', 'Window',
    -border => 0,
    -y      => 0,
    -bfg    => 'blue',
  );

  my $notebook = $win->add(undef, 'Notebook');

  my $p1 = $notebook->add_page('Page 1');
  $p1 -> add(undef, 'Label',
    -x    => 1,
    -y    => 1,
    -text => "This is Page One",
  );

  my $p2 = $notebook->add_page('Page 2');
  $p2 -> add(undef, 'Label',
    -x    => 1,
    -y    => 1,
    -text => "This is Page Two",
  );

  # Now the fun part:
  $ui->set_timer(
    'delete_active_page',
    sub {
      $notebook->delete_page($notebook->active_page());
      $_[0]->delete_timer('delete_active_page');
    },
    5
  );
  # What we expect to happen is that five seconds into runtime, the
  # currently active page will disappear.
  #
  # What ACTUALLY happens is that the application crashes.

  $ui->set_binding( sub { $ui->mainloopExit() }, "\cQ" );

  $notebook->focus();
  $ui->mainloop();

Never mind the use of the undocumented features set_timer() and delete_timer() for now. I'll 'splain 'em momentarily. They're very handy. I'm a source code reading fool but if you haven't seen it yet I'll save you the bother. First, though:

I need in the application I'm writing to create and destroy pages in the Curses::UI::Notebook widget in response to user action. It seems easy enough; the documentation makes it look that way. Delete one, though, and Poof! on the next call to Notebook's draw method. Off I went to debug, fix, and diff so I could deliver a nice bug report. But then I found a few points at which the bug manifests itself, proved it by bashing those spots with a hammer just so I'd know I was on the right track...

... and then decided that I'd rather just finish my own thing first. So if you're not inclined to find the root of the bug and submit your own bug report with a patch, here's how:


  # Now the fun part:
  $ui->set_timer(
    'delete_active_page',
    sub {
      $notebook->delete_page($notebook->active_page());
      $_[0]->reset_curses();                              # BIG freakin' hammer
      $_[0]->delete_timer('delete_active_page');
    },
    5
  );

But if you're not into that whole brevity thing and like to assign a $self or a $this or whatever, it'd look like this:


  # Now the fun part:
  $ui->set_timer(
    'delete_active_page',
    sub {
      my $self = shift;                                   # $self is the $ui object
      $notebook->delete_page($notebook->active_page());
      $self->reset_curses();                              # BIG freakin' hammer
      $self->delete_timer('delete_active_page');
    },
    5
  );
  # No more crashy-crashy.

So that's one way to keep Curses::UI::Notebook from ruining your day. There are other big ugly bugs in the thing that aren't going to get fixed until someone takes over for the original author who's retired.

Now, about that timer: Curses::UI::Notebook gives you four methods to play with that do just what their names imply:

  • set_timer($id, $callback, [$time_in_seconds])
  • delete_timer($id)
  • disable_timer($id)
  • enable_timer($id)

When you call set_timer($id, $callback, [$time_in_seconds]) you inject a timer into the main loop. $id is an arbitrary string that's used to refer to the thing and that's used internally as a hash key by Curses::UI. $callback is a code reference or anonymous method, and $time_in_seconds is how long to wait between invocations of your $callback. So you can do something like this:


sub autostatus {
    my ( $message, $time ) = @_;
    if ( !defined $message || !length $message ) {
        return;
    }
    if ( !defined $time || !$time || $time !~ /^\d+$/ ) {
        $time = 2;
    }
    $ui->set_timer(
        'status_on',
        sub {
            $ui->delete_timer('status_on');
            $ui->status($message);
        },
        -0.01
    );
    $ui->set_timer(
        'status_off',
        sub {
            $ui->nostatus();
            $ui->delete_timer('status_off');
        },
        $time
    );
}

And with that you don't have to fool around with finding a good time to call nostatus() or resort to being that kind of bonehead who puts a blocking sleep() into an event loop. Just hurl your message at it and get on with more important things.

If you want to use that sample above, go for it. But don't be tempted to change the status_on timer to a call to schedule_event even though it seems smarter and more in keeping with the intent expressed in the documentation. Or go ahead and be tempted... it's all the same to me. But if the user employs the rodent to do his talking for him, you'll find that the status widget quite often doesn't show.

If you don't provide a $time_in_seconds to set_timer it will default to 1. Also, keep in mind that your timers will repeat forever if you don't disable or delete them.

Caveats: That timer is very coarse and can be expected to jitter quite a lot. The only gaurantee is that your callback will be called more or less kinda sorta when you schedule it sometimes. The glitch:


Time:     Operation:                         Result:
-----     ----------                         -------
0.999999  set_timer('tick', $callback, 1)       -
1.000000  ($last + $t_i_s) <= time()         callback invoked in
                                             just one microsecond!

0.000000  set_timer('tock' $callback, 1)        -
          main loop is very busy until:
3.141593  ($last + $t_i_s) <= time()         callback had to wait
                                             for pi!

If your Curses:UI application sets a timer late in the current clock second your callback might be invoked very quickly. On the other hand, if your application is doing a blocking operation at the tick when the timer should fire, the timer is going to be stalled until the main loop gets control back after the blocking operation ends.

So there you have it. If I've saved you some headache, I'm really very happy to have helped out. And if I've not saved you some headache you've probably not read this far.

→ committed: 2/2/2013 19:03:50

[ / perl_programming] permanent link

Comments: 0    Trackbacks: 0

 

Comments are closed for this story.

 

Trackbacks are closed for this story.

Save the Net

Creative Commons License

Project Honeypot Member

 
February 2013
Mon Tue Wed Thu Fri Sat Sun
        2
     

By Month:

By category:

Feeds:

Served to 54.198.55.167:39230 at 14:39:49 GMT on Tuesday, October 23, 2018.

return(0.5420);