1package Mojo::Weixin::Controller;
2use strict;
3use Config;
4use File::Spec;
5use Mojo::Weixin::Base 'Mojo::EventEmitter';
6use Mojo::Weixin;
7use Mojo::Weixin::Server;
8use Mojo::Weixin::Log;
9use Mojo::UserAgent;
10use Mojo::IOLoop;
11use IO::Socket::IP;
12use Time::HiRes ();
13use Storable qw();
14use POSIX qw();
15use File::Spec ();
16use if $^O eq "MSWin32",'Win32::Process';
17use if $^O eq "MSWin32",'Win32';
18use base qw(Mojo::Weixin::Util Mojo::Weixin::Request);
19our $VERSION = $Mojo::Weixin::VERSION;
20
21has backend => sub{+{}};
22has ioloop  => sub {Mojo::IOLoop->singleton};
23has backend_start_port => 3000;
24has post_api => undef;
25has poll_api => undef;
26has poll_interval => 5;
27has auth     => undef;
28has server =>  sub { Mojo::Weixin::Server->new };
29has listen => sub { [{host=>"0.0.0.0",port=>2000},] };
30
31has http_debug          => sub{$ENV{MOJO_WEIXIN_CONTROLLER_HTTP_DEBUG} || 0 } ;
32has ua_debug            => sub{$_[0]->http_debug};
33has ua_debug_req_body   => sub{$_[0]->ua_debug};
34has ua_debug_res_body   => sub{$_[0]->ua_debug};
35has ua_debug_req_body   => sub{$_[0]->ua_debug};
36has ua_debug_res_body   => sub{$_[0]->ua_debug};
37has ua_retry_times          => 5;
38has ua_connect_timeout      => 10;
39has ua_request_timeout      => 120;
40has ua_inactivity_timeout   => 120;
41has ua  => sub {
42    Mojo::UserAgent->new(
43        connect_timeout=>$_[0]->ua_connect_timeout,
44        request_timeout=>$_[0]->ua_request_timeout,
45        inactivity_timeout=>$_[0]->ua_inactivity_timeout,
46    )
47};
48
49has tmpdir              => sub {File::Spec->tmpdir();};
50has keep_cookie         => 0;
51has cookie_path         => sub {File::Spec->catfile($_[0]->tmpdir,join('','mojo_weixin_controller_cookie','.dat'))};
52has pid_path            => sub {File::Spec->catfile($_[0]->tmpdir,join('','mojo_weixin_controller_process','.pid'))};
53has backend_path        => sub {File::Spec->catfile($_[0]->tmpdir,join('','mojo_weixin_controller_backend','.dat'))};
54has template_path        => sub {File::Spec->catfile($_[0]->tmpdir,join('','mojo_weixin_controller_template','.pl'))};
55has check_interval      => 5;
56
57has log_level           => 'info';     #debug|info|msg|warn|error|fatal
58has log_path            => undef;
59has log_encoding        => undef;      #utf8|gbk|...
60has log_head            => "[wxc][$$]";
61has log_console         => 1;
62has disable_color       => 0;
63has max_clients         => 100;
64
65has version             => sub{$Mojo::Weixin::Controller::VERSION};
66
67has log     => sub{
68    Mojo::Weixin::Log->new(
69        encoding    =>  $_[0]->log_encoding,
70        path        =>  $_[0]->log_path,
71        level       =>  $_[0]->log_level,
72        head        =>  $_[0]->log_head,
73        disable_color   => $_[0]->disable_color,
74        console_output  => $_[0]->log_console,
75    )
76};
77sub new {
78    my $class = shift;
79    my $self  = $class->SUPER::new(@_);
80    $self->info("当前正在使用 Mojo-Weixin-Controller v" . $self->version);
81    $self->ioloop->reactor->on(error=>sub{
82        my ($reactor, $err) = @_;
83        $self->error("reactor error: " . Carp::longmess($err));
84    });
85    $SIG{__WARN__} = sub{$self->warn(Carp::longmess @_);};
86    $self->on(error=>sub{
87        my ($self, $err) = @_;
88        $self->error(Carp::longmess($err));
89    });
90    if( $^O!~/^MSWin32/i and $Config{d_pseudofork}){
91        $self->fatal("非常抱歉, Mojo-Weixin-Controller不支持您当前使用的系统");
92        $self->stop();
93    }
94    $self->check_pid();
95    $self->load_backend();
96    $self->check_client();
97    $SIG{CHLD} =  'IGNORE';
98    $SIG{INT} = $SIG{KILL} = $SIG{TERM} = $SIG{HUP} = sub{
99        $self->info("捕获到停止信号[$_[0]],准备停止...");
100        $self->info("正在停止Controller...");
101        $self->save_backend();
102        $self->clean_pid();
103        $self->stop();
104    };
105    eval{$0 = 'wxcontroller';} if $^O ne 'MSWin32';
106    if(defined $self->poll_api){
107        $self->on('_mojo_weixin_controller_poll_over' => sub{
108            $self->http_get($self->poll_api,sub{
109                $self->ioloop->timer($self->poll_interval || 5,sub {$self->emit('_mojo_weixin_controller_poll_over');});
110            });
111        });
112    }
113    $Mojo::Weixin::Controller::_CONTROLLER = $self;
114    $self;
115}
116sub stop{
117    my $self = shift;
118    $self->info("Controller停止运行");
119    CORE::exit();
120}
121
122sub save_backend{
123    my $self = shift;
124    my $backend_path = $self->backend_path;
125    eval{Storable::nstore($self->backend,$backend_path);};
126    $self->warn("Controller保存backend失败: $@") if $@;
127
128}
129sub load_backend {
130    my $self = shift;
131    my $backend_path = $self->backend_path;
132    return if not -f $backend_path;
133    eval{$self->backend(Storable::retrieve($backend_path))};
134    if($@){
135        $self->warn("Controller加载backend失败: $@");
136        return;
137    }
138    else{
139        $self->info("Controller加载backend[ $backend_path ]");
140    }
141}
142sub check_pid {
143    my $self = shift;
144    return if not $self->pid_path;
145    eval{
146        if(not -f $self->pid_path){
147            $self->spurt($$,$self->pid_path);
148        }
149        else{
150            my $pid = $self->slurp($self->pid_path);
151            if( $pid=~/^\d+$/ and kill(0, $pid) ){
152                $self->warn("检测到有其他运行中的Controller(pid:$pid), 请先将其关闭");
153                $self->stop();
154            }
155            else{
156                $self->spurt($$,$self->pid_path);
157            }
158        }
159    };
160    $self->warn("进程检测遇到异常: $@") if $@;
161
162}
163
164
165sub clean_pid {
166    my $self = shift;
167    return if not defined $self->pid_path;
168    return if not -f $self->pid_path;
169    $self->info("清除残留的Controller pid文件");
170    unlink $self->pid_path or $self->warn("删除pid文件[ " . $self->pid_path . " ]失败: $!");
171}
172
173sub kill_process {
174    my $self = shift;
175    my $ret = 0;
176    if(!$_[0] or $_[0]!~/^\d+$/){
177        $self->error("pid无效,无法终止进程");
178        return;
179    }
180    if($^O  eq "MSWin32"){
181    #    my $exitcode = 0;
182    #    Win32::Process::KillProcess($_[0],$exitcode);
183    #    return $exitcode;
184        $ret = kill POSIX::SIGINT,$_[0] ;
185    }
186    else{
187        $ret = kill POSIX::SIGTERM,$_[0] ;
188    }
189
190    #client进程退出没有那么快,马上检查的话,仍然是存在的,干脆先不检查了
191    #return !$self->check_process($_[0]);
192
193    return $ret;
194}
195sub check_process {
196    my $self = shift;
197    if(!$_[0] or $_[0]!~/^\d+$/){
198        $self->error("pid无效,无法检测进程");
199        return;
200    }
201    #if($^O  eq "MSWin32"){
202    #    my $p;
203    #    return Win32::Process::Open($p,$_[0],0);
204    #}
205    kill 0,$_[0];
206}
207sub start_client {
208    my $self = shift;
209    my $param = shift;
210    if(!$param->{client}){
211        return {code => 1, status=>'client not found',};
212    }
213    elsif($self->max_clients < keys %{$self->backend}){
214        return {code => 5, status=>'max clients exceed'};
215    }
216    elsif(exists $self->backend->{$param->{client}}){
217        if( $self->check_process($self->backend->{$param->{client}}{pid}) ){
218            my %client = %{ $self->backend->{$param->{client}} };
219            for(keys %client){ delete $client{$_} if substr($_,0,1) eq "_"};
220            return {code=>0, status=>'client already exists',%client};
221        }
222    }
223    my $backend_port = empty_port({host=>'127.0.0.1',port=>$self->backend_start_port,proto=>'tcp'});
224    return {code => 2, status=>'no available port',client=>$param->{client}} if not defined $backend_port;
225    my $post_api = $param->{post_api} || $self->post_api;
226    my $poll_api = $param->{poll_api};
227    if(defined $post_api){
228        my $url = Mojo::URL->new($post_api);
229        $url->query->merge(client=>$param->{client});
230        $post_api =  $url->to_string;
231    }
232    if(defined $poll_api){
233        my $url = Mojo::URL->new($poll_api);
234        $url->query->merge(client=>$param->{client});
235        $poll_api =  $url->to_string;
236    }
237    $param->{account} = $param->{client};
238
239    for my $env(keys %ENV){
240        delete $ENV{$env} if $env=~/^MOJO_WEIXIN_([A-Z_]+)$/;
241    }
242    for my $p (keys %$param){
243        my $env_key = "MOJO_WEIXIN_" . uc($p);
244        $ENV{$env_key} = $param->{$p};
245    }
246    $ENV{MOJO_WEIXIN_PLUGIN_OPENWX_PORT} = $backend_port;
247    $ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POST_API} = $post_api;
248    $ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POLL_API} = $poll_api;
249
250    $ENV{MOJO_WEIXIN_LOG_PATH} = $self->log_path;
251    $ENV{MOJO_WEIXIN_LOG_ENCODING} = $self->log_encoding;
252    $ENV{MOJO_WEIXIN_LOG_CONSOLE} = $self->log_console;
253    $ENV{MOJO_WEIXIN_DISABLE_COLOR} = $self->disable_color;
254    $ENV{MOJO_WEIXIN_HTTP_DEBUG} = $self->http_debug;
255    $ENV{MOJO_WEIXIN_LOG_LEVEL} = $self->log_level;
256    $ENV{MOJO_WEIXIN_CONTROLLER_PID} = $$;
257
258    $ENV{MOJO_WEIXIN_TMPDIR} = $self->tmpdir if not defined $ENV{MOJO_WEIXIN_TMPDIR};
259    $ENV{MOJO_WEIXIN_STATE_PATH} = File::Spec->catfile($ENV{MOJO_WEIXIN_TMPDIR},join('','mojo_weixin_state_',$ENV{MOJO_WEIXIN_ACCOUNT},'.json')) if not defined $ENV{MOJO_WEIXIN_STATE_PATH};
260    $ENV{MOJO_WEIXIN_QRCODE_PATH} = File::Spec->catfile($ENV{MOJO_WEIXIN_TMPDIR},join('','mojo_weixin_qrcode_',$ENV{MOJO_WEIXIN_ACCOUNT},'.jpg')) if not defined $ENV{MOJO_WEIXIN_QRCODE_PATH};
261    $ENV{MOJO_WEIXIN_PID_PATH} = File::Spec->catfile($ENV{MOJO_WEIXIN_TMPDIR},join('','mojo_weixin_pid_',$ENV{MOJO_WEIXIN_ACCOUNT},'.pid')) if not defined $ENV{MOJO_WEIXIN_PID_PATH};
262    local $ENV{PERL5LIB} = join( ($^O eq "MSWin32"?";":":"),@INC);
263    if(!-f $self->template_path or -z $self->template_path){
264        my $template =<<'MOJO_WEIXIN_CLIENT_TEMPLATE';
265#!/usr/bin/env perl
266use Mojo::Weixin;
267$|=1;
268my $client = Mojo::Weixin->new(log_head=>"[$ENV{MOJO_WEIXIN_ACCOUNT}][$$]");
269$0 = "wxclient(" . $client->account . ")" if $^O ne "MSWin32";
270$client->load(["ShowMsg","UploadQRcode"]);
271$client->load("Openwx",data=>{listen=>[{host=>"127.0.0.1",port=>$ENV{MOJO_WEIXIN_PLUGIN_OPENWX_PORT} }], post_api=>$ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POST_API} || undef,post_event=>$ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POST_EVENT} // 1,post_media_data=> $ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POST_MEDIA_DATA} // 1, poll_api=>$ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POLL_API} || undef, poll_interval => $ENV{MOJO_WEIXIN_PLUGIN_OPENWX_POLL_INTERVAL} },call_on_load=>1);
272$client->run();
273MOJO_WEIXIN_CLIENT_TEMPLATE
274        $self->spurt($template,$self->template_path);
275    }
276    $self->info("使用模版[" . $self->template_path .  "]创建客户端");
277    if( $^O eq 'MSWin32'){#Windows
278        my $process;
279        no strict;
280        my $p = $self->decode("gbk",$Config{perlpath});
281        if($p=~/\p{Han}|\s+/){
282            $self->warn("perl路径包含空格或中文可能导致客户端创建失败: [" . $self->encode("utf8",$p) . "]");
283        }
284        if(Win32::Process::Create($process,$Config{perlpath},'perl ' . $self->template_path,0,CREATE_NEW_PROCESS_GROUP,".") ){
285
286            my $pid;eval{$pid = $process->GetProcessID()};
287            if($pid!~/^\d+$/){
288                return {code=>4,status=>'client pid not ok' };
289            }
290            $self->backend->{$param->{client}} = $param;
291            $self->backend->{$param->{client}}{pid} = $pid;
292            $self->backend->{$param->{client}}{port} = $backend_port;
293            $self->backend->{$param->{client}}{_tmpdir} = $ENV{MOJO_WEIXIN_TMPDIR};
294            $self->backend->{$param->{client}}{_state_path} = $ENV{MOJO_WEIXIN_STATE_PATH};
295            $self->backend->{$param->{client}}{_pid_path} = $ENV{MOJO_WEIXIN_PID_PATH};
296            $self->backend->{$param->{client}}{_qrcode_path} = $ENV{MOJO_WEIXIN_QRCODE_PATH};
297            delete $self->backend->{$param->{client}}{log_head};
298            my %client = %{ $self->backend->{$param->{client}} };
299            for(keys %client){ delete $client{$_} if substr($_,0,1) eq "_"}
300            return {code=>0,status=>'success',%client };
301        }
302        else{
303            $self->error(
304                "创建客户端失败: " .
305                $self->encode("utf8",
306                    $self->decode("gbk",Win32::FormatMessage( Win32::GetLastError() ) || 'create client fail' )
307                )
308            );
309            #$self->error(Win32::FormatMessage( Win32::GetLastError() ) );
310            return {code=>3,status=>'failure',};
311        }
312    }
313    else{#Unix
314        my $pid = fork();
315        if($pid == 0) {#new process
316            $self->server->stop;
317            $self->ioloop->stop;
318            delete $self->server->{servers};
319            my $template_path = $self->template_path;
320            undef $self;
321            exec $Config{perlpath} || 'perl',$template_path;
322        }
323        else{
324            select undef,undef,undef,0.05;
325            if($pid!~/^\d+$/){
326                return {code=>4,status=>'client pid not ok' };
327            }
328            $self->backend->{$param->{client}} = $param;
329            $self->backend->{$param->{client}}{pid} = $pid;
330            $self->backend->{$param->{client}}{port} = $backend_port;
331            $self->backend->{$param->{client}}{_tmpdir} = $ENV{MOJO_WEIXIN_TMPDIR};
332            $self->backend->{$param->{client}}{_state_path} = $ENV{MOJO_WEIXIN_STATE_PATH};
333            $self->backend->{$param->{client}}{_pid_path} = $ENV{MOJO_WEIXIN_PID_PATH};
334            $self->backend->{$param->{client}}{_qrcode_path} = $ENV{MOJO_WEIXIN_QRCODE_PATH};
335            delete $self->backend->{$param->{client}}{log_head};
336            my %client = %{ $self->backend->{$param->{client}} };
337            for(keys %client){ delete $client{$_} if substr($_,0,1) eq "_"}
338            return {code=>0,status=>'success',%client };
339        }
340    }
341}
342
343sub stop_client {
344    my $self = shift;
345    my $param = shift;
346    if(!$param->{client}){
347        return {code => 1, status=>'client not found',};
348    }
349    elsif(!exists $self->backend->{$param->{client}}){
350        return {code => 1, status=>'client not exists',};
351    }
352    my $ret = $self->kill_process( $self->backend->{$param->{client}}{pid} );
353    if ($ret){
354        my $client = $self->backend->{$param->{client}};
355        delete $self->backend->{$param->{client}};
356        for(keys %$client){ delete $client->{$_} if substr($_,0,1) eq "_"}
357        return {code=>0,status=>'success',%$client };
358    }
359    return {code=>1,status=>'failure'};
360}
361
362sub check_client {
363    my $self = shift;
364    for my $client ( keys %{ $self->backend }  ){
365        my $pid = $self->backend->{$client}->{pid};
366        return if !$pid;
367        return if $pid !~ /^\d+$/;
368        my $ret = $self->check_process($pid);
369        if(not $ret){
370            $self->warn("检测到客户端 $client\[$pid\] 不存在,删除客户端信息");
371            delete $self->backend->{$client};
372        }
373    }
374}
375sub run {
376    my $self = shift;
377    my $server =  $self->server;
378    $server->app($server->build_app("Mojo::Weixin::Controller::App"));
379    $server->app->defaults(wxc=>$self);
380    $server->app->secrets("hello world");
381    $server->app->log($self->log);
382    $server->listen([ map { 'http://' . (defined $_->{host}?$_->{host}:"0.0.0.0") .":" . (defined $_->{port}?$_->{port}:2000)} @{ $self->listen } ]) ;
383    $server->start;
384    $self->ioloop->recurring($self->check_interval || 5,sub{
385        $self->check_client();
386        $self->save_backend();
387    });
388    $self->emit('_mojo_weixin_controller_poll_over');
389    $self->ioloop->start if not $self->ioloop->is_running;
390}
391
392package Mojo::Weixin::Controller::App::Controller;
393use Mojo::JSON ();
394use Mojo::Util ();
395use base qw(Mojolicious::Controller);
396sub render{
397    my $self = shift;
398    if($_[0] eq 'json'){
399        $self->res->headers->content_type('application/json');
400        $self->SUPER::render(data=>Mojo::JSON::to_json($_[1]),@_[2..$#_]);
401    }
402    else{$self->SUPER::render(@_)}
403}
404sub safe_render{
405    my $self = shift;
406    $self->render(@_) if (defined $self->tx and !$self->tx->is_finished);
407}
408sub param{
409    my $self = shift;
410    my $data = $self->SUPER::param(@_);
411    defined $data?Mojo::Util::encode("utf8",$data):undef;
412}
413sub params {
414    my $self = shift;
415    my $hash = $self->req->params->to_hash ;
416    $self->stash('wxc')->reform($hash);
417    return $hash;
418}
419package Mojo::Weixin::Controller::App;
420use Mojolicious::Lite;
421use Mojo::Transaction::HTTP;
422app->controller_class('Mojo::Weixin::Controller::App::Controller');
423under sub {
424    my $c = shift;
425    if(ref $c->stash('wxc')->auth eq "CODE"){
426        my $hash  = $c->params;
427        my $ret = 0;
428        eval{
429            $ret = $c->stash('wxc')->auth->($hash,$c);
430        };
431        $c->stash('wxc')->warn("插件[Mojo::Weixin::Controller]认证回调执行错误: $@") if $@;
432        $c->safe_render(text=>"auth failure",status=>403) if not $ret;
433        return $ret;
434    }
435    else{return 1}
436};
437get '/openwx/start_client' => sub{
438    my $c = shift;
439    my $hash   = $c->params;
440    my $result =  $c->stash('wxc')->start_client($hash);
441    $c->safe_render(json=>$result);
442};
443get '/openwx/stop_client' => sub{
444    my $c = shift;
445    my $hash   = $c->params;
446    my $result = $c->stash('wxc')->stop_client($hash);
447    $c->safe_render(json=>$result);
448};
449get '/openwx/get_qrcode' => sub{
450    my $c = shift;
451    my $client = $c->param("client");
452    if(!$client){
453        $c->safe_render(json=>{code => 1, status=>'client not found',});
454        return;
455    }
456    elsif(!exists $c->stash('wxc')->backend->{$client}){
457        $c->safe_render(json => {code => 1, status=>'client not exists',});
458        return;
459    }
460    eval{
461        my $qrcode_path = $c->stash('wxc')->backend->{$client}{_qrcode_path};
462        my $data = $c->stash('wxc')->slurp($qrcode_path);
463        $c->res->headers->cache_control('no-cache');
464        $c->res->headers->content_type('image/jpg');
465        $c->safe_render(data=>$data,);
466    };
467    if($@){
468        $c->stash('wxc')->warn("读取客户端二维码失败: $@");
469        $c->safe_render(text=>"",status=>404);
470    }
471};
472get '/openwx/check_client' => sub{
473    my $c = shift;
474    my $client = $c->param("client");
475    if(defined $client){
476        if(!exists $c->stash('wxc')->backend->{$client}){
477            $c->safe_render(json => {code => 1, status=>'client not exists',});
478            return;
479        }
480        else{
481            eval{
482                my $state_path = $c->stash('wxc')->backend->{$client}{_state_path};
483                my $json = $c->stash('wxc')->decode_json($c->stash('wxc')->slurp($state_path));
484                $json->{port} = $c->stash('wxc')->backend->{$client}{port};
485                $c->safe_render(json=>{code=>0,client=>[$json]});
486            };
487            if($@){
488                $c->stash('wxc')->warn("读取客户端state文件失败: $@");
489                #$c->safe_render(json=>{code=>0,client=>[ $c->stash('wxc')->backend->{$client}, ] });
490                $c->safe_render(json=>{code => 1,status=>"client state file read error"});
491            }
492        }
493    }
494    else{
495        eval{
496            my @client;
497            for my $client ( values %{ $c->stash('wxc')->backend }){
498                my $state_path = $client->{_state_path};
499                my $json = $c->stash('wxc')->from_json($c->stash('wxc')->slurp($state_path));
500                $json->{port} = $client->{port};
501                push @client,$json;
502            }
503            $c->safe_render(json=>{code=>0,client=>\@client});
504        };
505        if($@){
506            $c->stash('wxc')->warn("读取客户端state文件失败: $@");
507            #$c->safe_render(json=>{code=>0,client=>[ values %{ $c->stash('wxc')->backend } ]});
508            $c->safe_render(json=>{code => 1,status=>"client state file read error"});
509        }
510    }
511};
512any '/openwx/*whatever'  => sub{
513    my $c = shift;
514    my $client = $c->param("client");
515    if(!$client){
516        $c->safe_render(json=>{code => 1, status=>'client not found',});
517        return;
518    }
519    elsif(!exists $c->stash('wxc')->backend->{$client}){
520        $c->safe_render(json => {code => 1, status=>'client not exists',});
521        return;
522    }
523    $c->inactivity_timeout(120);
524    $c->render_later;
525    my $tx = Mojo::Transaction::HTTP->new(req=>$c->req->clone);
526    $tx->req->url->host("127.0.0.1");
527    $tx->req->url->port($c->stash('wxc')->backend->{$client}->{port});
528    $tx->req->url->scheme('http');
529    $tx->req->headers->header('Host',$tx->req->url->host_port);
530    return if $c->stash('mojo.finished');
531    $c->stash('wxc')->ua->start($tx,sub{
532        my ($ua,$tx) = @_;
533        $c->tx->res($tx->res);
534        $c->rendered;
535    });
536};
537any '/*whatever'  => sub{whatever=>'',$_[0]->safe_render(json=>{code=>-1,status=>"api not found"},status=>403)};
538package Mojo::Weixin::Controller;
539
540sub can_bind {
541    my ($host, $port, $proto) = @_;
542    # The following must be split across two statements, due to
543    # https://rt.perl.org/Public/Bug/Display.html?id=124248
544    my $s = _listen_socket($host, $port, $proto);
545    return defined $s;
546}
547
548sub _listen_socket {
549    my ($host, $port, $proto) = @_;
550    $port  ||= 0;
551    $proto ||= 'tcp';
552    IO::Socket::IP->new(
553        (($proto eq 'udp') ? () : (Listen => 5)),
554        LocalAddr => $host,
555        LocalPort => $port,
556        Proto     => $proto,
557        V6Only    => 1,
558        (($^O eq 'MSWin32') ? () : (ReuseAddr => 1)),
559    );
560}
561
562sub listen_socket {
563    my ($host, $proto) = @{$_[0]}{qw(host proto)};
564    $host = '127.0.0.1' unless defined $host;
565    return _listen_socket($host, undef, $proto);
566}
567
568# get a empty port on 49152 .. 65535
569# http://www.iana.org/assignments/port-numbers
570sub empty_port {
571    my ($host, $port, $proto) = @_ && ref $_[0] eq 'HASH' ? ($_[0]->{host}, $_[0]->{port}, $_[0]->{proto}) : (undef, @_);
572    $host = '127.0.0.1'
573        unless defined $host;
574    if (defined $port) {
575        $port = 49152 unless $port =~ /^[0-9]+$/ && $port < 49152;
576    } else {
577        $port = 50000 + (int(rand()*1500) + abs($$)) % 1500;
578    }
579    $proto = $proto ? lc($proto) : 'tcp';
580
581    $port--;
582    while ( $port++ < 65000 ) {
583        # Remote checks don't work on UDP, and Local checks would be redundant here...
584        next if ($proto eq 'tcp' && check_port({ host => $host, port => $port }));
585        return $port if can_bind($host, $port, $proto);
586    }
587    return;
588}
589
590sub check_port {
591    my ($host, $port, $proto) = @_ && ref $_[0] eq 'HASH' ? ($_[0]->{host}, $_[0]->{port}, $_[0]->{proto}) : (undef, @_);
592    $host = '127.0.0.1'
593        unless defined $host;
594    $proto = $proto ? lc($proto) : 'tcp';
595
596    # for TCP, we do a remote port check
597    # for UDP, we do a local port check, like empty_port does
598    my $sock = ($proto eq 'tcp') ?
599        IO::Socket::IP->new(
600            Proto    => 'tcp',
601            PeerAddr => $host,
602            PeerPort => $port,
603            V6Only   => 1,
604        ) :
605        IO::Socket::IP->new(
606            Proto     => $proto,
607            LocalAddr => $host,
608            LocalPort => $port,
609            V6Only   => 1,
610            (($^O eq 'MSWin32') ? () : (ReuseAddr => 1)),
611        )
612    ;
613
614    if ($sock) {
615        close $sock;
616        return 1; # The port is used.
617    }
618    else {
619        return 0; # The port is not used.
620    }
621
622}
6231;
624