From 134497c24ffc4a62452cd839f50bc6e4bfab885c Mon Sep 17 00:00:00 2001 From: clowwindy Date: Sun, 21 Dec 2014 12:47:07 +0800 Subject: [PATCH] implement daemon --- shadowsocks/daemon.py | 173 ++++++++++++++++++++++++++++++++++++++++++ shadowsocks/local.py | 5 +- shadowsocks/server.py | 5 +- shadowsocks/utils.py | 97 ++++++++++++++--------- 4 files changed, 242 insertions(+), 38 deletions(-) create mode 100644 shadowsocks/daemon.py diff --git a/shadowsocks/daemon.py b/shadowsocks/daemon.py new file mode 100644 index 0000000..d4320e0 --- /dev/null +++ b/shadowsocks/daemon.py @@ -0,0 +1,173 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2014 clowwindy +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +from __future__ import absolute_import, division, print_function, \ + with_statement + +import os +import sys +import logging +import signal +import time +from shadowsocks import common + +# this module is ported from ShadowVPN daemon.c + + +def daemon_exec(config): + if 'daemon' in config: + if os.name != 'posix': + raise Exception('daemon mode is only supported in unix') + command = config['daemon'] + if not command: + command = 'start' + pid_file = config['pid-file'] + log_file = config['log-file'] + command = common.to_str(command) + pid_file = common.to_str(pid_file) + log_file = common.to_str(log_file) + if command == 'start': + daemon_start(pid_file, log_file) + elif command == 'stop': + daemon_stop(pid_file) + # always exit after daemon_stop + sys.exit(0) + elif command == 'restart': + daemon_stop(pid_file) + daemon_start(pid_file, log_file) + else: + raise Exception('unsupported daemon command %s' % command) + + +def write_pid_file(pid_file, pid): + import fcntl + import stat + + try: + fd = os.open(pid_file, os.O_RDWR | os.O_CREAT, + stat.S_IRUSR | stat.S_IWUSR) + except OSError as e: + logging.error(e) + return -1 + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + assert flags != -1 + flags |= fcntl.FD_CLOEXEC + r = fcntl.fcntl(fd, fcntl.F_SETFD, flags) + assert r != -1 + # There is no platform independent way to implement fcntl(fd, F_SETLK, &fl) + # via fcntl.fcntl. So use lockf instead + try: + fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET) + except IOError: + r = os.read(fd, 32) + if r: + logging.error('already started at pid %s' % common.to_str(r)) + else: + logging.error('already started') + os.close(fd) + return -1 + os.ftruncate(fd, 0) + os.write(fd, common.to_bytes(str(pid))) + return 0 + + +def freopen(f, mode, stream): + oldf = open(f, mode) + oldfd = oldf.fileno() + newfd = stream.fileno() + os.close(newfd) + os.dup2(oldfd, newfd) + + +def daemon_start(pid_file, log_file): + # fork only once because we are sure parent will exit + pid = os.fork() + assert pid != -1 + + def handle_exit(signum, _): + sys.exit(0) + + if pid > 0: + # parent waits for its child + signal.signal(signal.SIGINT, handle_exit) + time.sleep(5) + sys.exit(0) + + # child signals its parent to exit + ppid = os.getppid() + pid = os.getpid() + if write_pid_file(pid_file, pid) != 0: + os.kill(ppid, signal.SIGINT) + sys.exit(1) + + print('started') + os.kill(ppid, signal.SIGINT) + + sys.stdin.close() + freopen(log_file, 'a', sys.stdout) + freopen(log_file, 'a', sys.stderr) + + +def daemon_stop(pid_file): + import errno + try: + with open(pid_file) as f: + buf = f.read() + pid = common.to_str(buf) + if not buf: + logging.error('not running') + except IOError as e: + logging.error(e) + if e.errno == errno.ENOENT: + # always exit 0 if we are sure daemon is not running + logging.error('not running') + return + sys.exit(1) + pid = int(pid) + if pid > 0: + try: + os.kill(pid, signal.SIGTERM) + except OSError as e: + if e.errno == errno.ESRCH: + logging.error('not running') + # always exit 0 if we are sure daemon is not running + return + logging.error(e) + sys.exit(1) + else: + logging.error('pid is not positive: %d', pid) + + # sleep for maximum 10s + for i in range(0, 200): + try: + # query for the pid + os.kill(pid, 0) + except OSError as e: + if e.errno == errno.ESRCH: + break + time.sleep(0.05) + else: + logging.error('timed out when stopping pid %d', pid) + sys.exit(1) + print('stopped') + os.unlink(pid_file) diff --git a/shadowsocks/local.py b/shadowsocks/local.py index e778ea7..6a97f07 100755 --- a/shadowsocks/local.py +++ b/shadowsocks/local.py @@ -30,7 +30,8 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns +from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ + asyncdns def main(): @@ -44,6 +45,8 @@ def main(): config = utils.get_config(True) + daemon.daemon_exec(config) + utils.print_shadowsocks() encrypt.try_cipher(config['password'], config['method']) diff --git a/shadowsocks/server.py b/shadowsocks/server.py index 9abdf9c..e7acc5e 100755 --- a/shadowsocks/server.py +++ b/shadowsocks/server.py @@ -30,7 +30,8 @@ import logging import signal sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../')) -from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns +from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\ + asyncdns def main(): @@ -38,6 +39,8 @@ def main(): config = utils.get_config(False) + daemon.daemon_exec(config) + utils.print_shadowsocks() if config['port_password']: diff --git a/shadowsocks/utils.py b/shadowsocks/utils.py index 7808d8f..7caa243 100644 --- a/shadowsocks/utils.py +++ b/shadowsocks/utils.py @@ -70,9 +70,9 @@ def find_config(): def check_config(config): if config.get('local_address', '') in [b'0.0.0.0']: - logging.warn('warning: local set to listen 0.0.0.0, which is not safe') + logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe') if config.get('server', '') in [b'127.0.0.1', b'localhost']: - logging.warn('warning: server set to listen %s:%s, are you sure?' % + logging.warn('warning: server set to listen on %s:%s, are you sure?' % (config['server'], config['server_port'])) if (config.get('method', '') or '').lower() == b'table': logging.warn('warning: table is not safe; please use a safer cipher, ' @@ -96,11 +96,11 @@ def get_config(is_local): logging.basicConfig(level=logging.INFO, format='%(levelname)-s: %(message)s') if is_local: - shortopts = 'hs:b:p:k:l:m:c:t:vq' - longopts = ['fast-open'] + shortopts = 'hd:s:b:p:k:l:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file='] else: - shortopts = 'hs:p:k:m:c:t:vq' - longopts = ['fast-open', 'workers='] + shortopts = 'hd:s:p:k:m:c:t:vq' + longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers='] try: config_path = find_config() optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts) @@ -146,12 +146,18 @@ def get_config(is_local): config['fast_open'] = True elif key == '--workers': config['workers'] = int(value) - elif key == '-h': + elif key in ('-h', '--help'): if is_local: print_local_help() else: print_server_help() sys.exit(0) + elif key == '-d': + config['daemon'] = value + elif key == '--pid-file': + config['pid-file'] = value + elif key == '--log-file': + config['log-file'] = value elif key == '-q': v_count -= 1 config['verbose'] = v_count @@ -171,6 +177,9 @@ def get_config(is_local): config['timeout'] = int(config.get('timeout', 300)) config['fast_open'] = config.get('fast_open', False) config['workers'] = config.get('workers', 1) + config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid') + config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log') + config['workers'] = config.get('workers', 1) config['verbose'] = config.get('verbose', False) config['local_address'] = config.get('local_address', '127.0.0.1') config['local_port'] = config.get('local_port', 1080) @@ -231,21 +240,29 @@ def print_help(is_local): def print_local_help(): print('''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT] [-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD] - [-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] [-q] - -optional arguments: - -h, --help show this help message and exit - -s SERVER_ADDR server address - -p SERVER_PORT server port, default: 8388 - -b LOCAL_ADDR local binding address, default: 127.0.0.1 - -l LOCAL_PORT local port, default: 1080 - -k PASSWORD password - -m METHOD encryption method, default: aes-256-cfb - -t TIMEOUT timeout in seconds, default: 300 - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ - -v, -vv verbose mode - -q, -qq quiet mode, only show warnings/errors + [-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] -[d] [-q] +A fast tunnel proxy that helps you bypass firewalls. + +You can supply configurations via either config file or command line arguments. + +Proxy options: + -h, --help show this help message and exit + -c CONFIG path to config file + -s SERVER_ADDR server address + -p SERVER_PORT server port, default: 8388 + -b LOCAL_ADDR local binding address, default: 127.0.0.1 + -l LOCAL_PORT local port, default: 1080 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + +General options: + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors Online help: ''') @@ -254,20 +271,28 @@ Online help: def print_server_help(): print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD -m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open] - [--workers WORKERS] [-v] [-q] - -optional arguments: - -h, --help show this help message and exit - -s SERVER_ADDR server address, default: 0.0.0.0 - -p SERVER_PORT server port, default: 8388 - -k PASSWORD password - -m METHOD encryption method, default: aes-256-cfb - -t TIMEOUT timeout in seconds, default: 300 - -c CONFIG path to config file - --fast-open use TCP_FASTOPEN, requires Linux 3.7+ - --workers WORKERS number of workers, available on Unix/Linux - -v, -vv verbose mode - -q, -qq quiet mode, only show warnings/errors + [--workers WORKERS] [-v] [-d start] [-q] +A fast tunnel proxy that helps you bypass firewalls. + +You can supply configurations via either config file or command line arguments. + +Proxy options: + -h, --help show this help message and exit + -c CONFIG path to config file + -s SERVER_ADDR server address, default: 0.0.0.0 + -p SERVER_PORT server port, default: 8388 + -k PASSWORD password + -m METHOD encryption method, default: aes-256-cfb + -t TIMEOUT timeout in seconds, default: 300 + --fast-open use TCP_FASTOPEN, requires Linux 3.7+ + --workers WORKERS number of workers, available on Unix/Linux + +General options: + -d start/stop/restart daemon mode + --pid-file PID_FILE pid file for daemon mode + --log-file LOG_FILE log file for daemon mode + -v, -vv verbose mode + -q, -qq quiet mode, only show warnings/errors Online help: ''')