File Coverage

lib/CallBackery/Config.pm
Criterion Covered Total %
statement 87 252 34.5
branch 8 58 13.7
condition 3 18 16.6
subroutine 18 31 58.0
pod 10 15 66.6
total 126 374 33.6


line stmt bran cond sub pod time code
1             # $Id: Config.pm 539 2013-12-09 22:28:11Z oetiker $
2             package CallBackery::Config;
3              
4             =head1 NAME
5              
6             CallBackery::Config - get parse configuration file for CallBackery
7              
8             =head1 SYNOPSIS
9              
10             use Nq::Config;
11             my $cfg = CallBackery::Config->new(file=>$file);
12             my $hash_ref = $cfg->cfgHash();
13             my $pod = $cfg->pod();
14              
15             =head1 DESCRIPTION
16              
17             CallBackery gets much of its configuration from this config file.
18              
19             =cut
20              
21 1     1   5 use Mojo::Base -base,-async_await, -signatures;
  1         2  
  1         7  
22 1     1   302 use CallBackery::Exception qw(mkerror);
  1         2  
  1         46  
23 1     1   3 use CallBackery::Translate qw(trm);
  1         1  
  1         33  
24 1     1   678 use Config::Grammar::Dynamic;
  1         10876  
  1         84  
25 1     1   6 use Carp;
  1         1  
  1         57  
26 1     1   958 use autodie;
  1         26184  
  1         5  
27 1     1   8232 use File::Spec;
  1         4  
  1         74  
28 1     1   1102 use Locale::PO;
  1         9537  
  1         146  
29 1     1   12 use Mojo::Promise;
  1         2  
  1         29  
30 1     1   56 use Mojo::Loader qw(load_class);
  1         3  
  1         118  
31 1     1   7 use Mojo::JSON qw(true false);
  1         2  
  1         65  
32 1     1   6 use Mojo::Exception;
  1         2  
  1         52  
33 1     1   5 use Scalar::Util qw(blessed);
  1         3  
  1         9103  
34             # use Devel::Cycle;
35              
36             =head2 file
37              
38             the name of the config file
39              
40             =cut
41              
42             has file => sub { croak "the file parameter is mandatory" };
43              
44             has secretFile => sub ($self) {
45             my $secretFile = $self->file.'.secret';
46             if (not -f $secretFile){
47             open my $rand, '>', $secretFile;
48             chmod 0600,$secretFile;
49             print $rand sprintf('%x%x',int(rand()*1e14),int(rand()*1e14));
50             close $rand;
51             chmod 0400,$secretFile;
52             }
53             return $secretFile;
54             };
55              
56             has app => sub { croak "the app parameter is mandatory" }, weak => 1;
57              
58             has log => sub {
59             shift->app->log;
60             };
61              
62             =head2 cfgHash
63              
64             a hash containing the data from the config file
65              
66             =cut
67              
68             has cfgHash => sub {
69             my $self = shift;
70             my $cfg_file = shift;
71             my $parser = $self->makeParser();
72             my $cfg = $parser->parse($self->file, {encoding => 'utf8'}) or croak($parser->{err});
73             # the grammar is self referential, so we need to clean it up
74             $self->grammar(undef);
75             return $cfg;
76             };
77              
78             =head2 pod
79              
80             returns a pod documenting the config file
81              
82             =cut
83              
84             has pod => sub {
85             my $self = shift;
86             my $parser = $self->makeParser();
87             my $E = '=';
88             my $footer = <<"FOOTER";
89              
90             ${E}head1 COPYRIGHT
91              
92             Copyright (c) 2014 by OETIKER+PARTNER AG. All rights reserved.
93              
94             ${E}head1 AUTHOR
95              
96             Stobi\@oetiker.chE>
97             Sfritz.zaucker\@oetiker.chE>
98              
99             ${E}head1 HISTORY
100              
101             2014-01-11 to 1.0 first version
102             2014-04-29 fz 1.1 implement plugin path
103              
104             FOOTER
105             my $header = <<"HEADER";
106             ${E}head1 NAME
107              
108             callbackery.cfg - The Appliance FRONTEND Builder config file
109              
110             ${E}head1 SYNOPSIS
111              
112             *** BACKEND ***
113             log_file = /tmp/nw-tobi.log
114              
115             *** FRONTEND ***
116             logo = logo.png
117             spinner = myspinner.gif
118             logo_small = logo-small.png
119             title = Appliance Configurator
120              
121             ${E}head1 DESCRIPTION
122              
123             The afb.cfg provides all the info for afb and its gui modules to interact with your appliance.
124              
125             ${E}head1 CONFIGURATION
126              
127             HEADER
128             return $header.$parser->makepod().$footer;
129             };
130              
131             =head2 pluginPath
132              
133             array of name spaces to look for gui plugins
134              
135             =cut
136              
137             has pluginPath => sub { ['CallBackery::GuiPlugin']; };
138              
139             =head2 B('PluginModule')
140              
141             Find the given module in the F, load it and create a first instance.
142              
143             =cut
144              
145              
146             sub loadAndNewPlugin {
147 4     4 1 36 my $self = shift;
148 4         8 my $plugin = shift;
149              
150 4         9 my $module;
151             my $ok;
152 4         8 for my $path (@{$self->pluginPath}) {
  4         14  
153             #$self->log->debug("looking for $plugin in $path");
154 4 50       38 if (my $e = load_class "${path}::$plugin") {
155 0 0       0 die mkerror(3894,"Loading ${path}::$plugin: $e") if ref $e;
156             } else {
157 4         146 my $proto = "${path}::${plugin}"->new(log=>$self->log);
158 4         409 $proto->{prototype} = 1;
159 4         26 return $proto;
160             }
161             }
162 0         0 die mkerror(123, "Plugin Module $plugin not found");
163             };
164              
165             has grammar => sub {
166             my $self = shift;
167              
168             my $pluginList = {};
169             my $pluginPath = $self->pluginPath;
170             for my $path (@INC){
171             for my $pPath (@$pluginPath) {
172             my @pDirs = split /::/, $pPath;
173             my $fPath = File::Spec->catdir($path, @pDirs, '*.pm');
174             for my $file (glob($fPath)) {
175             my ($volume, $modulePath, $moduleName) = File::Spec->splitpath($file);
176             $moduleName =~ s{\.pm$}{};
177             $pluginList->{$moduleName} = 'Plugin Module';
178             }
179             }
180             }
181             return {
182             _sections => [ qw(BACKEND FRONTEND FRONTEND-COLORS /PLUGIN:\s*\S+/)],
183             _mandatory => [qw(BACKEND FRONTEND)],
184             BACKEND => {
185             _doc => 'BACKEND Settings',
186             _vars => [ qw(log_file cfg_db sesame_user sesame_pass) ],
187             _mandatory => [ qw(cfg_db sesame_user sesame_user) ],
188             log_file => { _doc => 'write a log file to this location (unless in development mode)'},
189             cfg_db => { _doc => 'file to store the config database'},
190             sesame_user => { _doc => <<'DOC'},
191             In Open Sesame mode, one has to use this username to get access to the system.
192             The password you enter does not matter.
193             DOC
194             sesame_pass => { _doc => <<'DOC'},
195             Using sesame_user and sesame_pass, the system can always be accessed.
196             In default configuration sesame_pass is NOT set.
197             DOC
198             },
199             FRONTEND => {
200             _doc => 'Settings for the Web FRONTEND',
201             _vars => [ qw(logo logo_small logo_noscale spinner title initial_plugin company_name company_url company_support
202             hide_password hide_password_icon hide_release hide_company max_width
203             )
204             ],
205             logo => {
206             _doc => 'url for the logo brand the login sceen',
207             },
208             company_name => {
209             _doc => 'who created the app',
210             },
211             company_url => {
212             _doc => 'link to the company homepage'
213             },
214             max_width => {
215             _doc => 'maximum content width'
216             },
217             company_support => {
218             _doc => 'company support eMail'
219             },
220             logo_small => {
221             _doc => 'url for the small logo brand the UI',
222             },
223             logo_noscale => {
224             _doc => "don't scale logo on login window",
225             _re => '(yes|no|true|false)',
226             _re_error => 'pick yes or no OR true or false',
227             _sub => sub {
228             $_[0] = ($_[0] =~ /yes|true/) ? true : false;
229             return;
230             },
231             },
232             spinner => {
233             _doc => 'url for the busy animation spinner gif',
234             },
235             title => {
236             _doc => 'title string for the application'
237             },
238             initial_plugin => {
239             _doc => 'which tab should be active upon login ?'
240             },
241             hide_password => {
242             _doc => 'hide password field on login screen',
243             _re => '(yes|no|true|false)',
244             _re_error => 'pick yes or no OR true or false',
245             _sub => sub {
246             $_[0] = ($_[0] =~ /yes|true/) ? true : false;
247             return;
248             },
249             },
250             hide_password_icon => {
251             _doc => 'hide password icon on login screen',
252             _re => '(yes|no|true|false)',
253             _re_error => 'pick yes or no OR true or false',
254             _sub => sub {
255             $_[0] = ($_[0] =~ /yes|true/) ? true : false;
256             return;
257             },
258             },
259             hide_release => {
260             _doc => 'hide release string on login screen',
261             _re => '(yes|no|true|false)',
262             _re_error => 'pick yes or no OR true or false',
263             _sub => sub {
264             $_[0] = ($_[0] =~ /yes|true/) ? true : false;
265             return;
266             },
267             },
268             hide_company => {
269             _doc => 'hide company string on login screen',
270             _re => '(yes|no|true|false)',
271             _re_error => 'pick yes or no OR true or false',
272             _sub => sub {
273             $_[0] = ($_[0] =~ /yes|true/) ? true : false;
274             return;
275             },
276             },
277             },
278             'FRONTEND-COLORS' => {
279             _vars => [ '/[a-zA-Z]\S+/' ],
280             '/[a-zA-Z]\S+/' => {
281             _doc => <
282             Use this section to override any color key used in the qooxdoo simple theme as well as the following:
283             C,
284             C,
285             C,
286             C,
287             C,
288             C,
289             C,
290             C.
291             C.
292              
293             The keys can be set to standard web colors C or to other key names.
294             COLORKEYS_END
295             _example => <
296             ff0000
297             EXAMPLE_END
298             _sub => sub {
299             if ($_[0] =~ /^\s*([0-9a-f]{3,6})\s*$/i){
300             $_[0] = '#'.lc($1);
301             }
302             return undef;
303             }
304             }
305             },
306             '/PLUGIN:\s*\S+/' => {
307             _order => 1,
308             _doc => 'Plugins providing appliance specific funtionality',
309             _vars => [qw(module)],
310             _mandatory => [qw(module)],
311             module => {
312             _sub => sub {
313             eval {
314             $_[0] = $self->loadAndNewPlugin($_[0]);
315             };
316             if ($@){
317             return "Failed to load Plugin $_[0]: $@";
318             }
319             return undef;
320             },
321             _dyn => sub {
322             my $var = shift;
323             my $module = shift;
324             $module = $self->loadAndNewPlugin($module) if not ref $module;
325             my $tree = shift;
326             my $grammar = $module->grammar();
327             push @{$grammar->{_vars}}, 'module';
328             for my $key (keys %$grammar){
329             $tree->{$key} = $grammar->{$key};
330             }
331             },
332             _dyndoc => $pluginList,
333             },
334             }
335             };
336             };
337              
338             sub makeParser {
339 1     1 0 49 my $self = shift;
340 1         5 my $parser = Config::Grammar::Dynamic->new($self->grammar);
341 1         11 return $parser;
342             }
343              
344             =head2 getTranslations
345              
346             Load translations from po files
347              
348             =cut
349              
350             sub getTranslations {
351 1     1 1 3 my $self = shift;
352 1   50     105 my $cfg = shift || {};
353 1         3 my %lx;
354 1   33     12 my $path = $cfg->{path} // $self->app->home->rel_file("share");
355 1         150 my $po = new Locale::PO();
356 1         84 for my $file (glob(File::Spec->catdir($path, '*.po'))) {
357 0         0 my ($volume, $localePath, $localeName) = File::Spec->splitpath($file);
358 0         0 my $locale = $localeName;
359 0         0 $locale =~ s/\.po$//;
360 0         0 my $lang = $locale;
361 0         0 $lang =~ s/_.+//;
362 0         0 local $_; # since load_file_ashash modifies $_ and does not localize it
363 0         0 my $href = Locale::PO->load_file_ashash($file, 'utf8');
364 0         0 for my $key (keys %$href) {
365 0         0 my $o = $href->{$key};
366 0         0 my $id = $po->dequote($o->msgid);
367 0         0 my $str = $po->dequote($o->msgstr);
368 0 0       0 next unless $id;
369 0         0 $lx{$locale}{$id} = $str;
370             }
371             }
372 1         147 return \%lx;
373             }
374              
375             =head2 postProcessCfg
376              
377             Post process the configuration data into a format that is easily used
378             by the application.
379              
380             =cut
381              
382             sub postProcessCfg {
383 1     1 1 46 my $self = shift;
384 1         5 my $cfg = $self->cfgHash;
385             # only postprocess once
386 1 50       9 return $cfg if $cfg->{PLUGIN}{list};
387 1         5 my %plugin;
388             my @pluginOrder;
389 1         10 for my $section (sort keys %$cfg){
390 7         16 my $sec = $cfg->{$section};
391 7 100       21 next unless ref $sec eq 'HASH'; # skip non hash stuff
392 6         18 for my $key (keys %$sec){
393 17 50 33     52 next unless ref $sec->{$key} eq 'HASH' and $sec->{$key}{_text};
394 0         0 $sec->{$key} = $sec->{$key}{_text};
395             }
396 6 100       30 if ($section =~ /^PLUGIN:\s*(.+)/){
397 2         39 my $name = $1;
398 2         9 $pluginOrder[$sec->{_order}] = $name;
399 2         4 delete $sec->{_order};
400 2         10 my $obj = $cfg->{PLUGIN}{prototype}{$name} = $sec->{module};
401 2         4 delete $sec->{module};
402 2         26 $obj->config($sec);
403 2         29 $obj->name($name);
404 2         19 $obj->app($self->app);
405 2         58 $obj->massageConfig($cfg);
406             # cleanup the config
407 2         9 delete $cfg->{$section};
408             }
409 6         19 $cfg->{PLUGIN}{list} = \@pluginOrder;
410             }
411             # rename section
412             # delete returns the value of the deleted hash element
413 1 50       7 if (exists $cfg->{'FRONTEND-COLORS'}) {
414 1         5 $cfg->{FRONTEND}{COLORS} = $cfg->{'FRONTEND-COLORS'};
415 1         4 delete $cfg->{'FRONTEND-COLORS'};
416             }
417 1         7 $cfg->{FRONTEND}{TRANSLATIONS} = $self->getTranslations();
418 1         7 return $cfg;
419             }
420              
421             =head2 instantiatePlugin(pluginName,userObj,args)
422              
423             create a new instance of this plugin prototype
424              
425             =cut
426              
427             sub _getPluginObject {
428 0     0     my $self = shift;
429 0           my $name = shift;
430              
431 0           my $user = shift;
432 0           my $args = shift;
433              
434 0           my $prototype = $self->cfgHash->{PLUGIN}{prototype}{$name};
435              
436             # clean the name
437 0           $name =~ s/[^-_0-9a-z]/_/gi;
438 0 0         die mkerror(39943,"No prototype for $name")
439             if not defined $prototype;
440 0   0       my $obj = $prototype->new(
441             user => $user,
442             name => $prototype->name,
443             config => $prototype->config,
444             args => $args // {},
445             app => $self->app,
446             );
447 0           $obj->log; # make sure logging is initialized
448 0           return $obj;
449             }
450              
451             # do not (!!!) implement this with async/await as it causes the the generated
452             # object to be somehow get a problem with reference counting with prevents
453             # timely destruction of the object 2024-06-12 tobi
454             sub instantiatePlugin_p {
455 0     0 0   my $self = shift;
456 0           my $obj = $self->_getPluginObject(@_);
457             return $self->promisify($obj->checkAccess)->then(sub {
458 0     0     my $access = shift;
459 0 0         return $obj if $access;
460 0           my $name = $obj->name;
461 0           Mojo::Promise->reject(mkerror(39944,"No permission to access $name"));
462 0           });
463             }
464              
465             sub instantiatePlugin {
466 0     0 1   my $self = shift;
467 0           my @args = @_;
468 0           my $obj = $self->_getPluginObject(@args);
469 0           my $name = $obj->name;
470 0 0         die mkerror(39944,"No permission to access $name")
471             if not $self->promiseDeath($obj->checkAccess);
472 0           return $obj;
473             }
474              
475             =head2 $configBlob = $cfg->getConfigBlob()
476              
477             return the configuration state of the system as a blob
478              
479             =cut
480              
481             has configPlugins => sub {
482             my $self = shift;
483             my $user = $self->app->userObject->new(app=>$self->app,userId=>'__CONFIG', log=>$self->log);
484             my $cfg = $self->cfgHash;
485             my @plugins;
486             for my $name (@{$cfg->{PLUGIN}{list}}){
487             my $obj = eval {
488             $self->instantiatePlugin($name,$user);
489             } or next;
490             push @plugins, $obj;
491             }
492             return \@plugins;
493             };
494              
495             sub getCrypt {
496 0     0 0   require Crypt::Rijndael;
497 0           my $self = shift;
498 0   0       my $password = substr((shift || '').('x' x 32),0,32);
499 0           return Crypt::Rijndael->new( $password,Crypt::Rijndael::MODE_CBC() );
500             }
501              
502             sub pack16 {
503 0     0 0   my $self = shift;
504 0           my $string = shift;
505 0           my $len = length($string);
506 0           my $mod = 16 - ($len % 16);
507 0           return sprintf("%016x%s",$len,$string.('x' x $mod));
508             }
509              
510             sub unpack16 {
511 0     0 0   my $self = shift;
512 0           my $string = shift;
513 0           my $len = substr($string,0,16);
514 0 0 0       if ( $len !~ /^[0-9a-f]{16}$/ or hex($len) > length($string)-16 ){
515 0           die mkerror(3844,trm("Wrong password!"));
516             }
517 0           return substr($string,16,hex($len));
518             }
519              
520             sub getConfigBlob {
521 0     0 1   my $self = shift;
522 0           my $password = shift;
523 0           require Archive::Zip;
524              
525 0           my $zip = Archive::Zip->new();
526 0           my $cfg = $self->cfgHash;
527             # flush all the changes in the database to the db file
528 0           my $dumpfile = '/tmp/cbdump'.$$;
529 0 0         unlink $dumpfile if -f $dumpfile;
530 0           open my $dump, '|-','/usr/bin/sqlite3',$cfg->{BACKEND}{cfg_db};
531 0           print $dump ".output $dumpfile\n";
532 0           print $dump ".dump\n";
533 0           close $dump;
534 0           $zip->addFile({
535             filename => $dumpfile,
536             zipName => '{DATABASEDUMP}',
537             });
538 0           for my $obj (@{$self->configPlugins}){
  0            
539 0           my $name = $obj->name;
540 0           for my $file (@{$obj->stateFiles}) {
  0            
541 0 0         if (-r $file){
542 0           $zip->addFile({
543             filename => $file,
544             zipName => '{PLUGINSTATE.'.$name.'}'.$file
545             })
546             }
547             }
548             }
549 0           my $zipData;
550 0           open(my $fh, ">", \$zipData);
551 0           $zip->writeToFileHandle($fh,0);
552              
553 0           my $crypt = $self->getCrypt($password);
554 0           return $crypt->encrypt($self->pack16($zipData));
555             }
556              
557             =head2 $cfg->restoreConfigBlob(configBlob)
558              
559             retore the confguration state
560              
561             =cut
562              
563             sub restoreConfigBlob {
564 0     0 1   my $self = shift;
565 0           my $config = shift;
566 0           my $password = shift;
567 0           require Archive::Zip;
568 0           my $crypt = $self->getCrypt($password);
569 0           $config = $self->unpack16($crypt->decrypt($config));
570              
571 0           my $cfg = $self->cfgHash;
572 0           my $user = $self->app->userObject->new(app=>$self->app,userId=>'__CONFIG', log=>$self->log);
573 0           open my $fh ,'<', \$config;
574 0           my $zip = Archive::Zip->new();
575 0           $zip->readFromFileHandle($fh);
576 0           my %stateFileCache;
577 0           for my $member ($zip->members){
578 0           for ($member->fileName){
579 0 0         /^\{DATABASE\}$/ && do {
580 0           $self->log->warn("Restoring Database!");
581 0           $self->app->database->mojoSqlDb->disconnect;
582 0           unlink glob $cfg->{BACKEND}{cfg_db}.'*';
583 0           $member->extractToFileNamed($cfg->{BACKEND}{cfg_db});
584 0           last;
585             };
586 0 0         /^\{DATABASEDUMP\}$/ && do {
587 0           $self->log->warn("Restoring Database Dump!");
588 0           $self->app->database->mojoSqlDb->disconnect;
589 0           unlink glob $cfg->{BACKEND}{cfg_db}.'*';
590 0           open my $sqlite, '|-', '/usr/bin/sqlite3',$cfg->{BACKEND}{cfg_db};
591 0           my $sql = $member->contents();
592 0           $sql =~ s/0$//; # for some reason the dump ends in 0
593 0           print $sqlite $sql;
594 0           close $sqlite;
595 0           last;
596             };
597 0 0         m/^\{PLUGINSTATE\.([^.]+)\}(.+)/ && do {
598 0           my $plugin = $1;
599 0           my $file = $2;
600 0 0         if (not $stateFileCache{$plugin}){
601 0           my $obj = eval {
602 0           $self->instantiatePlugin($plugin,$user);
603             };
604 0 0         if (not $obj){
605 0           $self->log->warn("Ignoring $file from plugin $plugin since the plugin is not available here.");
606 0           next;
607             }
608 0           $stateFileCache{$plugin} = { map { $_ => 1 } @{$obj->stateFiles} };
  0            
  0            
609             };
610 0 0         if ($stateFileCache{$plugin}{$file}){
611 0           $member->extractToFileNamed($file);
612             }
613             else {
614 0           $self->log->warn("Ignoring $file from archive since it is not listed in $plugin stateFiles.");
615             }
616             }
617             }
618             }
619 0           $self->reConfigure;
620             }
621              
622             =head2 $cfg->reConfigure()
623              
624             Regenerate all the template based configuration files using input from the database.
625              
626             =cut
627              
628             sub reConfigure {
629 0     0 1   my $self = shift;
630 0           my $secretFile = $self->secretFile;
631 0 0         if (not -f $secretFile){
632 0           open my $rand, '>', $secretFile;
633 0           chmod 0600,$secretFile;
634 0           print $rand sprintf('%x%x',int(rand()*1e14),int(rand()*1e14));
635 0           close $rand;
636 0           chmod 0400,$secretFile;
637             }
638 0           for my $obj (@{$self->configPlugins}){
  0            
639 0           $obj->reConfigure;
640             }
641             }
642              
643             =head2 $cfg->unConfigure()
644              
645             Restore the system to unconfigured state. By removing the
646             configuration database, unlinking all user supplied configuration
647             files and regenerating all template based configuration files with
648             empty input.
649              
650             =cut
651              
652             sub unConfigure {
653 1     1   8 no autodie;
  1         1  
  1         8  
654 0     0 1   my $self = shift;
655 0           my $cfg = $self->cfgHash;
656 0           $self->log->debug("unlinking config database ".$cfg->{BACKEND}{cfg_db});
657 0 0         unlink $cfg->{BACKEND}{cfg_db} if -f $cfg->{BACKEND}{cfg_db};
658 0           open my $gen, '>', $cfg->{BACKEND}{cfg_db}.'.flush';
659 0           close $gen;
660             #get 'clean' config files
661 0           $self->reConfigure();
662             # and now remove all state
663 0           for my $obj (@{$self->configPlugins}){
  0            
664 0           for my $file (@{$obj->stateFiles},@{$obj->unConfigureFiles}) {
  0            
  0            
665 0 0         next if not -f $file;
666 0           $self->log->debug('['.$obj->name."] unlinking $file");
667 0           unlink $file;
668             }
669             }
670 0 0 0       unlink $cfg->{BACKEND}{log_file} if defined $cfg->{BACKEND}{log_file} and -f $cfg->{BACKEND}{log_file} ;
671 0 0         unlink $self->secretFile if -f $self->secretFile;
672 0           system "sync";
673             }
674              
675             =head2 $cfg->promisify(xxx)
676              
677             always return a promise resolving to the value
678              
679             =cut
680              
681             sub promisify {
682 0     0 1   my $self = shift;
683 0           my $value = shift;
684 0 0         if (eval { blessed $value && $value->isa('Mojo::Promise') }){
  0 0          
685 0           return $value;
686             }
687              
688 0           return Mojo::Promise->resolve($value);
689             }
690              
691             =head2 $cfg->promiseDeath(xxx)
692              
693             die when there is a promise response
694              
695             =cut
696              
697             sub promiseDeath {
698 0     0 1   my $self = shift;
699 0           my $value = shift;
700 0 0         if (eval { blessed $value && $value->isa('Mojo::Promise') }){
  0 0          
701 0           Mojo::Exception->throw("unexpected promise respone!");
702             }
703 0           return $value;
704             }
705              
706              
707             1;
708              
709             __END__