1#!/usr/bin/perl 2 3use YAML; 4use strict; 5use Data::Dumper; 6use File::Path; 7use Time::HiRes 'usleep'; 8use Cwd; 9 10my $tmp_dir = "/tmp"; 11 12my %default_options = ( 13 config => 'conf/ngs.conf', 14 port => 8080, 15 host => '127.0.0.1', 16 bind => '127.0.0.1', 17 worker_processes => 5, 18 worker_connections => 1024, 19 access_log_path => '/dev/stdout', 20 error_log_path => '/dev/stdout', 21 nginx => '/usr/local/nginx/sbin/nginx', 22); 23 24my $tmp_path = "$tmp_dir/td-$$"; 25 26my $is_daemon = 0; 27 28$SIG{TERM} = \&cleanup; 29$SIG{INT} = \&cleanup; 30 31# our primary dispatched sub 32sub run 33{ 34 my $self = shift; 35 my $cwd = cwd; 36 37 $self->parse_options; 38 39 # load yaml 40 $self->load_config; 41 42 for my $key (keys %default_options) 43 { 44 $self->{options}{$key} = $default_options{$key} 45 unless $self->{options}{$key} or $self->{config}{$key}; 46 } 47 48 return $self->help if $self->{options}{help}; 49 return $self->list_processes if $self->{options}{list}; 50 return $self->prune if $self->{options}{prune}; 51 return $self->stop if $self->{options}{stop}; 52 53 # hack for --single to work 54 $self->{options}{worker_processes} = 1 55 if $self->{options}{single}; 56 57 # apache httpd holdover 58 $self->{options}{worker_processes} = 1 59 if $self->{options}{X}; 60 61 # hack for --start to work 62 $self->{options}{daemon} = 1 63 if $self->{options}{start}; 64 65 $self->prune 66 if $self->{options}{start}; 67 68 return $self->write_nginx_config(dump => 1) 69 if $self->{options}{dump_config}; 70 71 if ($self->config_option('daemon')) 72 { 73 my $basename = (split('/', $0))[-1]; 74 warn "[$basename] - started as daemon\n"; 75 exit(0) if fork; 76 $is_daemon = 1; 77 } 78 79 # reassert PID, in case we fork. 80 $tmp_path = "$tmp_dir/td-$$"; 81 mkdir($tmp_path) unless -d $tmp_path; 82 83 my $config_file = $self->config_file; 84 my $cwd = ($config_file !~ /^\//) ? cwd : ''; 85 86 open(MT, ">$tmp_path/config.path"); 87 print MT "$cwd/$config_file\n"; 88 close(MT); 89 90 open(MT, ">$tmp_path/td.pid"); 91 print MT "$$\n"; 92 close(MT); 93 94 # write mime types, if needed 95 $self->write_mime_types; 96 $self->write_nginx_config; 97 98 $self->start_nginx; 99 100 # wait! (no need to take up 100% of cpu) 101 while (getc()) {}; 102} 103 104sub stop 105{ 106 my $self = shift; 107 108 my @procs = $self->get_active_pids; 109 my $config_file = $self->config_file; 110 my $cwd = ($config_file !~ /^\//) ? cwd : ''; 111 my $full_config = "$cwd/$config_file"; 112 113 my $killed = 0; 114 for my $proc (@procs) 115 { 116 $full_config =~ s{//}{/}; 117 118 next unless $proc->{config_file} eq $full_config; 119 kill(15, $proc->{pid}); 120 print "killed: [pid: $proc->{pid}] $proc->{config_file}\n"; 121 $killed++; 122 } 123 124 $self->prune; 125 126 print "No active sessions.\n" unless $killed; 127} 128 129sub list_processes 130{ 131 my $self = shift; 132 133 my @procs = $self->get_active_pids; 134 135 unless (@procs) 136 { 137 print "No active sessions.\n"; 138 return; 139 } 140 141 print "Active sessions:\n"; 142 for my $proc (@procs) 143 { 144 my $pid = $proc->{pid} || 'ZOMBIE'; 145 print " [pid: $pid] $proc->{config_file} [$proc->{dir}]\n"; 146 } 147} 148 149sub get_active_pids 150{ 151 my $self = shift; 152 153 opendir(DIR, $tmp_dir); 154 my @dirs = grep { -d "$tmp_dir/$_" and $_ =~ /^td-\d+$/ } readdir(DIR); 155 closedir(DIR); 156 157 my @pid_data; 158 for my $dir (@dirs) 159 { 160 # is this process running? 161 open (NGPID, "$tmp_dir/$dir/nginx.pid"); 162 my $pid = <NGPID>; 163 close(NGPID); 164 165 # clense 166 $pid =~ s/[\n\r]//g; 167 168 next unless -d "/proc/$pid"; 169 170 open(CF, "$tmp_dir/$dir/config.path"); 171 my $config_location = <CF>; 172 close(CF); 173 174 chomp($config_location); 175 176 # avoid path starting with // 177 $config_location =~ s{//}{/}; 178 179 push @pid_data, { 180 config_file => $config_location, 181 pid => $pid, 182 dir => "$tmp_dir/$dir/", 183 }; 184 } 185 186 return @pid_data; 187} 188 189sub prune 190{ 191 my $self = shift; 192 193 opendir(DIR, $tmp_dir); 194 my @dirs = grep { -d "$tmp_dir/$_" and $_ =~ /^td-\d+$/ } readdir(DIR); 195 closedir(DIR); 196 197 for my $dir (@dirs) 198 { 199 # is this process running? 200 open (NGPID, "$tmp_dir/$dir/nginx.pid"); 201 my $pid = <NGPID>; 202 close(NGPID); 203 204 # clense 205 $pid =~ s/[\n\r]//g; 206 207 next if -d "/proc/$pid" and $pid; 208 209 open(CF, "$tmp_dir/$dir/nginx.pid"); 210 my $nginx_pid = <CF>; 211 close(CF); 212 213 kill(15, $nginx_pid) 214 if $nginx_pid and -d "/proc/$nginx_pid"; 215 216 rmtree("$tmp_dir/$dir"); 217 print "Pruned: $tmp_dir/$dir\n"; 218 } 219} 220 221sub cleanup 222{ 223 my $self = shift; 224 my $daemon = $is_daemon; 225 226 if (-e "$tmp_path/nginx.pid") 227 { 228 open(PID, "$tmp_path/nginx.pid"); 229 my $pid = <PID>; 230 close(PID); 231 232 kill(15, $pid) if $pid; 233 } 234 235 unless ($daemon) 236 { 237 usleep(200000); 238 } 239 240 unlink("$tmp_path/nginx.conf"); 241 unlink("$tmp_path/error.log"); 242 unlink("$tmp_path/access.log"); 243 unlink("$tmp_path/mime.types"); 244 rmtree($tmp_path); 245 246 if ($daemon) 247 { 248 exit(0); 249 } 250 else 251 { 252 die "\n\nNginx stopped successfully.\n"; 253 } 254} 255 256sub start_nginx 257{ 258 my $self = shift; 259 my $path = $self->config_option('nginx'); 260 261 my $daemon = $self->config_option('daemon'); 262 my $project = $self->config_option('project'); 263 264 warn "Starting $project...\n\n" if $project and not $daemon; 265 266 system($path, '-c', "$tmp_path/nginx.conf"); 267 268 my $bind = $self->config_option('bind'); 269 my $ssl_port = $self->config_option('ssl_port'); 270 271 my $o_bind = $bind; 272 $bind = 'localhost' if $bind eq 'all'; 273 274 unless ($daemon) 275 { 276 my $port = $self->config_option('port'); 277 warn "Nginx is running on: http://$bind:$port/\n"; 278 warn " * Running with SSL on port $ssl_port.\n" if $ssl_port; 279 warn " * Bound to all sockets.\n" if $o_bind eq 'all'; 280 warn "\n"; 281 } 282} 283 284sub config_option 285{ 286 my ($self, $key) = @_; 287 288 my $config = $self->{config}; 289 my $options = $self->{options}; 290 291 return $options->{$key} || $config->{$key} || ''; 292} 293 294sub write_mime_types 295{ 296 my $self = shift; 297 298 return if -e ">$tmp_path/mime.types"; 299 300 my $txt = q[types { 301 text/html html htm shtml; 302 text/css css; 303 text/xml xml rss; 304 image/gif gif; 305 image/jpeg jpeg jpg; 306 application/x-javascript js; 307 application/atom+xml atom; 308 309 text/mathml mml; 310 text/plain txt; 311 text/vnd.sun.j2me.app-descriptor jad; 312 text/vnd.wap.wml wml; 313 text/x-component htc; 314 315 image/png png; 316 image/tiff tif tiff; 317 image/vnd.wap.wbmp wbmp; 318 image/x-icon ico; 319 image/x-jng jng; 320 image/x-ms-bmp bmp; 321 image/svg+xml svg; 322 323 application/java-archive jar war ear; 324 application/mac-binhex40 hqx; 325 application/msword doc; 326 application/pdf pdf; 327 application/postscript ps eps ai; 328 application/rtf rtf; 329 application/vnd.ms-excel xls; 330 application/vnd.ms-powerpoint ppt; 331 application/vnd.wap.wmlc wmlc; 332 application/vnd.wap.xhtml+xml xhtml; 333 application/x-cocoa cco; 334 application/x-java-archive-diff jardiff; 335 application/x-java-jnlp-file jnlp; 336 application/x-makeself run; 337 application/x-perl pl pm; 338 application/x-pilot prc pdb; 339 application/x-rar-compressed rar; 340 application/x-redhat-package-manager rpm; 341 application/x-sea sea; 342 application/x-shockwave-flash swf; 343 application/x-stuffit sit; 344 application/x-tcl tcl tk; 345 application/x-x509-ca-cert der pem crt; 346 application/x-xpinstall xpi; 347 application/zip zip; 348 349 application/octet-stream bin exe dll; 350 application/octet-stream deb; 351 application/octet-stream dmg; 352 application/octet-stream eot; 353 application/octet-stream iso img; 354 application/octet-stream msi msp msm; 355 356 audio/midi mid midi kar; 357 audio/mpeg mp3; 358 audio/x-realaudio ra; 359 360 video/3gpp 3gpp 3gp; 361 video/mpeg mpeg mpg; 362 video/quicktime mov; 363 video/x-flv flv; 364 video/x-mng mng; 365 video/x-ms-asf asx asf; 366 video/x-ms-wmv wmv; 367 video/x-msvideo avi; 368} 369]; 370 371 open(MT, ">$tmp_path/mime.types"); 372 print MT $txt; 373 close(MT); 374} 375 376sub base_path 377{ 378 my $self = shift; 379 380 return $self->config_option('root') 381 if $self->config_option('root'); 382 383 my $config_file = $self->config_file; 384 my $cwd = ($config_file !~ /^\//) ? cwd : ''; 385 my $full_config = "$cwd/$config_file"; 386 387 my @paths = split ('/', $full_config); 388 pop @paths; # rid of file name 389 pop @paths if $paths[-1] eq 'conf'; # rid of config directory 390 391 return join '/', @paths; 392} 393 394sub write_nginx_config 395{ 396 my ($self, %params) = @_; 397 my $base_path = $self->base_path; 398 my $worker_processes = $self->config_option('worker_processes'); 399 my $worker_connections = $self->config_option('worker_connections'); 400 my $require_module = $self->config_option('require_module'); 401 my $handler = $self->config_option('handler'); 402 my $app_path = $self->config_option('app_path'); 403 404 my @lj = grep { $_ } ( 405 $self->config_option('bind'), $self->config_option('port') 406 ); 407 408 $self->{options}{ssl_port} = $self->config_option('port') + 1000 409 if $self->config_option('ssl') and not $self->config_option('ssl_port'); 410 411 # remove when bind is 'all' 412 shift @lj if $self->config_option('bind') eq 'all'; 413 414 my $listen = join(':', @lj); 415 my $host = $self->config_option('host'); 416 my $port = $self->config_option('port'); 417 418 my @required = @{$self->{config}{require_modules} || [ ]}; 419 my $perl_requires = ''; 420 for my $req (@required) 421 { 422 $perl_requires .= "perl_require $req;\n"; 423 } 424 425 my $locations = ''; 426 my @locations = @{$self->{config}{locations} || [ ]}; 427 for my $location (@locations) 428 { 429 if ($location->{handler}) 430 { 431 $locations .= "\n"; 432 $locations .= "\tlocation $location->{path} {\n"; 433 $locations .= "\t\tperl $location->{handler};\n"; 434 $locations .= "\t}\n"; 435 } 436 else 437 { 438 $locations .= "\n"; 439 $locations .= "\tlocation $location->{path} {\n"; 440 $locations .= "\t\troot $base_path/$location->{root};\n"; 441 $locations .= "\t\tindex $location->{index};\n"; 442 $locations .= "\t}\n"; 443 } 444 } 445 446 $locations .= $self->config_option('location_raw') 447 if $self->config_option('location_raw'); 448 449 my $ssl_port = $self->config_option('ssl_port'); 450 my $ssl = ''; 451 if ($ssl_port) 452 { 453 my @slj = grep { $_ } ( 454 $self->config_option('bind'), $self->config_option('ssl_port') 455 ); 456 457 # remove when bind is 'all' 458 shift @slj if $self->config_option('bind') eq 'all'; 459 460 my $ssl_listen = join(':', @slj); 461 462 $ssl = qq| 463 server { 464 listen $ssl_listen; 465 set \$is_ssl 1; 466 set \$ssl_port $ssl_port; 467 server_name $host; 468 469 error_page 404 /404.html; 470 error_page 500 502 503 504 /50x.html; 471 472 ssl on; 473 ssl_certificate $base_path/ssl/cert.pem; 474 ssl_certificate_key $base_path/ssl/cert.key; 475 ssl_session_timeout 5m; 476 ssl_protocols SSLv2 SSLv3 TLSv1; 477 ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP; 478 ssl_prefer_server_ciphers on; 479 gzip on; 480 481 $locations 482 } 483|; 484 } 485 486 my $access_log_path = $self->config_option('access_log_path'); 487 my $error_log_path = $self->config_option('error_log_path'); 488 489 my $access_log = $self->config_option('access_log') eq 'off' 490 ? 'access_log /dev/null main;' : "access_log $access_log_path main;"; 491 492 my $error_log = $self->config_option('error_log') eq 'off' 493 ? 'error_log /dev/null;' : "error_log $error_log_path;"; 494 495 my $set_ssl_port = $ssl_port ? " set \$ssl_port $ssl_port; " : ''; 496 497 my $template = qq| 498worker_processes $worker_processes; 499 500pid $tmp_path/nginx.pid; 501 502$error_log 503 504events { 505 worker_connections $worker_connections; 506} 507 508http { 509 include $tmp_path/mime.types; 510 default_type application/octet-stream; 511 512 log_format main '[\$time_local] \$remote_addr - \$request ' 513 '"\$status" \$body_bytes_sent'; 514 515 $access_log 516 517 sendfile on; 518 keepalive_timeout 65; 519 520 perl_modules $base_path/lib; 521 522 $perl_requires 523 524 server { 525 listen $listen; 526 server_name $host; 527 $set_ssl_port 528 gzip on; 529 530 error_page 404 /404.html; 531 error_page 500 502 503 504 /50x.html; 532 533 $locations 534 } 535 536 $ssl 537} 538|; 539 540 if ($params{dump}) 541 { 542 print $template; 543 } 544 else 545 { 546 open(CT, ">$tmp_path/nginx.conf"); 547 print CT $template; 548 close(CT); 549 } 550} 551 552sub load_config 553{ 554 my $self = shift; 555 my $config_file = $self->config_file; 556 557 my $str_data; 558 open(CF, $config_file); 559 read(CF, $str_data, -s $config_file); 560 close(CF); 561 562 my $config = Load($str_data); 563 564 $self->{config} = $config; 565} 566 567sub config_file 568{ 569 my $self = shift; 570 my $file = $self->{options}{config} || 'conf/ngs.conf'; 571 if ($file) 572 { 573 $self->error("'$file' does not exist.") 574 unless -e $file; 575 } 576 else 577 { 578 $self->error("You must specify a config path."); 579 } 580 581 return $file; 582} 583 584sub error 585{ 586 my ($self, $error) = @_; 587 my $basename = (split('/', $0))[-1]; 588 589 die "[$basename] fatal error: $error\n"; 590} 591 592sub help 593{ 594 my $basename = (split('/', $0))[-1]; 595 my $usage = "usage: $basename [ options ]\n"; 596 $usage .= " Options:\n"; 597 $usage .= " --help (displays this message)\n"; 598 $usage .= " --dump_config (dumps generated nginx.conf only)\n"; 599 $usage .= " --bind=[address|all]\n"; 600 $usage .= " --config=path/to/conf (default conf/ngs.conf)\n"; 601 $usage .= " --port=[port] (default 8080)\n"; 602 $usage .= " --access_log=[on|off] (default on)\n"; 603 $usage .= " --error_log=[on|off] (default on)\n"; 604 $usage .= " --access_log_path=/pa (default /dev/stdout)\n"; 605 $usage .= " --error_log_path=/pa (default /dev/stdout)\n"; 606 $usage .= " --ssl (auto enable ssl on port 9080)\n"; 607 $usage .= " --ssl_port=[port] (enables ssl)\n"; 608 $usage .= " --worker_processes=[#processes]\n"; 609 $usage .= " --worker_connections=[#connections]\n"; 610 $usage .= " --single (only run one worker process)\n"; 611 $usage .= " --nginx=/usr/local/nginx/sbin/nginx\n"; 612 $usage .= " Daemon Mode:\n"; 613 $usage .= " --daemon|--start (default off)\n"; 614 $usage .= " --list (list all active sessions)\n"; 615 $usage .= " --prune (cleanup all defunct sessions)\n"; 616 $usage .= " --stop (stop session based on config)\n"; 617 618 die $usage; 619} 620 621# quick and dirty 622sub parse_options 623{ 624 my $self = shift; 625 626 my %options; 627 628 my @acceptable_options = qw( 629 bind port access_log error_log 630 ssl ssl_port worker_processes single 631 nginx help worker_connections config 632 host X access_log_path error_log_path 633 daemon list prune stop 634 start dump_config location_raw 635 ); 636 637 for my $arg (@ARGV) 638 { 639 # cleanse all parameters of all unrighteousness 640 # `--` & `-` any parameter shall be removed 641 $arg =~ s/^--//; 642 $arg =~ s/^-//; 643 644 # does this carry an assignment? 645 if ($arg =~ /=/) 646 { 647 my ($key, $value) = split('=', $arg); 648 649 $options{$key} = $value; 650 } 651 else 652 { 653 $options{$arg} = 1; 654 } 655 } 656 657 for my $option (keys %options) 658 { 659 $self->error("`$option` is an invalid option") 660 unless (grep { $_ eq $option } @acceptable_options) 661 } 662 663 $self->{options} = \%options; 664 665 return \%options; 666} 667 668# BANG! 669my $run = {}; 670bless($run); 671$run->run; 672 6731; 674