Skip to content

Commit 134497c

Browse files
author
clowwindy
committed
implement daemon
1 parent 78f93a0 commit 134497c

File tree

4 files changed

+242
-38
lines changed

4 files changed

+242
-38
lines changed

shadowsocks/daemon.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
4+
# Copyright (c) 2014 clowwindy
5+
#
6+
# Permission is hereby granted, free of charge, to any person obtaining a copy
7+
# of this software and associated documentation files (the "Software"), to deal
8+
# in the Software without restriction, including without limitation the rights
9+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
# copies of the Software, and to permit persons to whom the Software is
11+
# furnished to do so, subject to the following conditions:
12+
#
13+
# The above copyright notice and this permission notice shall be included in
14+
# all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
# SOFTWARE.
23+
24+
from __future__ import absolute_import, division, print_function, \
25+
with_statement
26+
27+
import os
28+
import sys
29+
import logging
30+
import signal
31+
import time
32+
from shadowsocks import common
33+
34+
# this module is ported from ShadowVPN daemon.c
35+
36+
37+
def daemon_exec(config):
38+
if 'daemon' in config:
39+
if os.name != 'posix':
40+
raise Exception('daemon mode is only supported in unix')
41+
command = config['daemon']
42+
if not command:
43+
command = 'start'
44+
pid_file = config['pid-file']
45+
log_file = config['log-file']
46+
command = common.to_str(command)
47+
pid_file = common.to_str(pid_file)
48+
log_file = common.to_str(log_file)
49+
if command == 'start':
50+
daemon_start(pid_file, log_file)
51+
elif command == 'stop':
52+
daemon_stop(pid_file)
53+
# always exit after daemon_stop
54+
sys.exit(0)
55+
elif command == 'restart':
56+
daemon_stop(pid_file)
57+
daemon_start(pid_file, log_file)
58+
else:
59+
raise Exception('unsupported daemon command %s' % command)
60+
61+
62+
def write_pid_file(pid_file, pid):
63+
import fcntl
64+
import stat
65+
66+
try:
67+
fd = os.open(pid_file, os.O_RDWR | os.O_CREAT,
68+
stat.S_IRUSR | stat.S_IWUSR)
69+
except OSError as e:
70+
logging.error(e)
71+
return -1
72+
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
73+
assert flags != -1
74+
flags |= fcntl.FD_CLOEXEC
75+
r = fcntl.fcntl(fd, fcntl.F_SETFD, flags)
76+
assert r != -1
77+
# There is no platform independent way to implement fcntl(fd, F_SETLK, &fl)
78+
# via fcntl.fcntl. So use lockf instead
79+
try:
80+
fcntl.lockf(fd, fcntl.LOCK_EX | fcntl.LOCK_NB, 0, 0, os.SEEK_SET)
81+
except IOError:
82+
r = os.read(fd, 32)
83+
if r:
84+
logging.error('already started at pid %s' % common.to_str(r))
85+
else:
86+
logging.error('already started')
87+
os.close(fd)
88+
return -1
89+
os.ftruncate(fd, 0)
90+
os.write(fd, common.to_bytes(str(pid)))
91+
return 0
92+
93+
94+
def freopen(f, mode, stream):
95+
oldf = open(f, mode)
96+
oldfd = oldf.fileno()
97+
newfd = stream.fileno()
98+
os.close(newfd)
99+
os.dup2(oldfd, newfd)
100+
101+
102+
def daemon_start(pid_file, log_file):
103+
# fork only once because we are sure parent will exit
104+
pid = os.fork()
105+
assert pid != -1
106+
107+
def handle_exit(signum, _):
108+
sys.exit(0)
109+
110+
if pid > 0:
111+
# parent waits for its child
112+
signal.signal(signal.SIGINT, handle_exit)
113+
time.sleep(5)
114+
sys.exit(0)
115+
116+
# child signals its parent to exit
117+
ppid = os.getppid()
118+
pid = os.getpid()
119+
if write_pid_file(pid_file, pid) != 0:
120+
os.kill(ppid, signal.SIGINT)
121+
sys.exit(1)
122+
123+
print('started')
124+
os.kill(ppid, signal.SIGINT)
125+
126+
sys.stdin.close()
127+
freopen(log_file, 'a', sys.stdout)
128+
freopen(log_file, 'a', sys.stderr)
129+
130+
131+
def daemon_stop(pid_file):
132+
import errno
133+
try:
134+
with open(pid_file) as f:
135+
buf = f.read()
136+
pid = common.to_str(buf)
137+
if not buf:
138+
logging.error('not running')
139+
except IOError as e:
140+
logging.error(e)
141+
if e.errno == errno.ENOENT:
142+
# always exit 0 if we are sure daemon is not running
143+
logging.error('not running')
144+
return
145+
sys.exit(1)
146+
pid = int(pid)
147+
if pid > 0:
148+
try:
149+
os.kill(pid, signal.SIGTERM)
150+
except OSError as e:
151+
if e.errno == errno.ESRCH:
152+
logging.error('not running')
153+
# always exit 0 if we are sure daemon is not running
154+
return
155+
logging.error(e)
156+
sys.exit(1)
157+
else:
158+
logging.error('pid is not positive: %d', pid)
159+
160+
# sleep for maximum 10s
161+
for i in range(0, 200):
162+
try:
163+
# query for the pid
164+
os.kill(pid, 0)
165+
except OSError as e:
166+
if e.errno == errno.ESRCH:
167+
break
168+
time.sleep(0.05)
169+
else:
170+
logging.error('timed out when stopping pid %d', pid)
171+
sys.exit(1)
172+
print('stopped')
173+
os.unlink(pid_file)

shadowsocks/local.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
import signal
3131

3232
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
33-
from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns
33+
from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\
34+
asyncdns
3435

3536

3637
def main():
@@ -44,6 +45,8 @@ def main():
4445

4546
config = utils.get_config(True)
4647

48+
daemon.daemon_exec(config)
49+
4750
utils.print_shadowsocks()
4851

4952
encrypt.try_cipher(config['password'], config['method'])

shadowsocks/server.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,17 @@
3030
import signal
3131

3232
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))
33-
from shadowsocks import utils, encrypt, eventloop, tcprelay, udprelay, asyncdns
33+
from shadowsocks import utils, daemon, encrypt, eventloop, tcprelay, udprelay,\
34+
asyncdns
3435

3536

3637
def main():
3738
utils.check_python()
3839

3940
config = utils.get_config(False)
4041

42+
daemon.daemon_exec(config)
43+
4144
utils.print_shadowsocks()
4245

4346
if config['port_password']:

shadowsocks/utils.py

Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,9 @@ def find_config():
7070

7171
def check_config(config):
7272
if config.get('local_address', '') in [b'0.0.0.0']:
73-
logging.warn('warning: local set to listen 0.0.0.0, which is not safe')
73+
logging.warn('warning: local set to listen on 0.0.0.0, it\'s not safe')
7474
if config.get('server', '') in [b'127.0.0.1', b'localhost']:
75-
logging.warn('warning: server set to listen %s:%s, are you sure?' %
75+
logging.warn('warning: server set to listen on %s:%s, are you sure?' %
7676
(config['server'], config['server_port']))
7777
if (config.get('method', '') or '').lower() == b'table':
7878
logging.warn('warning: table is not safe; please use a safer cipher, '
@@ -96,11 +96,11 @@ def get_config(is_local):
9696
logging.basicConfig(level=logging.INFO,
9797
format='%(levelname)-s: %(message)s')
9898
if is_local:
99-
shortopts = 'hs:b:p:k:l:m:c:t:vq'
100-
longopts = ['fast-open']
99+
shortopts = 'hd:s:b:p:k:l:m:c:t:vq'
100+
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=']
101101
else:
102-
shortopts = 'hs:p:k:m:c:t:vq'
103-
longopts = ['fast-open', 'workers=']
102+
shortopts = 'hd:s:p:k:m:c:t:vq'
103+
longopts = ['help', 'fast-open', 'pid-file=', 'log-file=', 'workers=']
104104
try:
105105
config_path = find_config()
106106
optlist, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
@@ -146,12 +146,18 @@ def get_config(is_local):
146146
config['fast_open'] = True
147147
elif key == '--workers':
148148
config['workers'] = int(value)
149-
elif key == '-h':
149+
elif key in ('-h', '--help'):
150150
if is_local:
151151
print_local_help()
152152
else:
153153
print_server_help()
154154
sys.exit(0)
155+
elif key == '-d':
156+
config['daemon'] = value
157+
elif key == '--pid-file':
158+
config['pid-file'] = value
159+
elif key == '--log-file':
160+
config['log-file'] = value
155161
elif key == '-q':
156162
v_count -= 1
157163
config['verbose'] = v_count
@@ -171,6 +177,9 @@ def get_config(is_local):
171177
config['timeout'] = int(config.get('timeout', 300))
172178
config['fast_open'] = config.get('fast_open', False)
173179
config['workers'] = config.get('workers', 1)
180+
config['pid-file'] = config.get('pid-file', '/var/run/shadowsocks.pid')
181+
config['log-file'] = config.get('log-file', '/var/log/shadowsocks.log')
182+
config['workers'] = config.get('workers', 1)
174183
config['verbose'] = config.get('verbose', False)
175184
config['local_address'] = config.get('local_address', '127.0.0.1')
176185
config['local_port'] = config.get('local_port', 1080)
@@ -231,21 +240,29 @@ def print_help(is_local):
231240
def print_local_help():
232241
print('''usage: sslocal [-h] -s SERVER_ADDR [-p SERVER_PORT]
233242
[-b LOCAL_ADDR] [-l LOCAL_PORT] -k PASSWORD [-m METHOD]
234-
[-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] [-q]
235-
236-
optional arguments:
237-
-h, --help show this help message and exit
238-
-s SERVER_ADDR server address
239-
-p SERVER_PORT server port, default: 8388
240-
-b LOCAL_ADDR local binding address, default: 127.0.0.1
241-
-l LOCAL_PORT local port, default: 1080
242-
-k PASSWORD password
243-
-m METHOD encryption method, default: aes-256-cfb
244-
-t TIMEOUT timeout in seconds, default: 300
245-
-c CONFIG path to config file
246-
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
247-
-v, -vv verbose mode
248-
-q, -qq quiet mode, only show warnings/errors
243+
[-t TIMEOUT] [-c CONFIG] [--fast-open] [-v] -[d] [-q]
244+
A fast tunnel proxy that helps you bypass firewalls.
245+
246+
You can supply configurations via either config file or command line arguments.
247+
248+
Proxy options:
249+
-h, --help show this help message and exit
250+
-c CONFIG path to config file
251+
-s SERVER_ADDR server address
252+
-p SERVER_PORT server port, default: 8388
253+
-b LOCAL_ADDR local binding address, default: 127.0.0.1
254+
-l LOCAL_PORT local port, default: 1080
255+
-k PASSWORD password
256+
-m METHOD encryption method, default: aes-256-cfb
257+
-t TIMEOUT timeout in seconds, default: 300
258+
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
259+
260+
General options:
261+
-d start/stop/restart daemon mode
262+
--pid-file PID_FILE pid file for daemon mode
263+
--log-file LOG_FILE log file for daemon mode
264+
-v, -vv verbose mode
265+
-q, -qq quiet mode, only show warnings/errors
249266
250267
Online help: <https://github.com/clowwindy/shadowsocks>
251268
''')
@@ -254,20 +271,28 @@ def print_local_help():
254271
def print_server_help():
255272
print('''usage: ssserver [-h] [-s SERVER_ADDR] [-p SERVER_PORT] -k PASSWORD
256273
-m METHOD [-t TIMEOUT] [-c CONFIG] [--fast-open]
257-
[--workers WORKERS] [-v] [-q]
258-
259-
optional arguments:
260-
-h, --help show this help message and exit
261-
-s SERVER_ADDR server address, default: 0.0.0.0
262-
-p SERVER_PORT server port, default: 8388
263-
-k PASSWORD password
264-
-m METHOD encryption method, default: aes-256-cfb
265-
-t TIMEOUT timeout in seconds, default: 300
266-
-c CONFIG path to config file
267-
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
268-
--workers WORKERS number of workers, available on Unix/Linux
269-
-v, -vv verbose mode
270-
-q, -qq quiet mode, only show warnings/errors
274+
[--workers WORKERS] [-v] [-d start] [-q]
275+
A fast tunnel proxy that helps you bypass firewalls.
276+
277+
You can supply configurations via either config file or command line arguments.
278+
279+
Proxy options:
280+
-h, --help show this help message and exit
281+
-c CONFIG path to config file
282+
-s SERVER_ADDR server address, default: 0.0.0.0
283+
-p SERVER_PORT server port, default: 8388
284+
-k PASSWORD password
285+
-m METHOD encryption method, default: aes-256-cfb
286+
-t TIMEOUT timeout in seconds, default: 300
287+
--fast-open use TCP_FASTOPEN, requires Linux 3.7+
288+
--workers WORKERS number of workers, available on Unix/Linux
289+
290+
General options:
291+
-d start/stop/restart daemon mode
292+
--pid-file PID_FILE pid file for daemon mode
293+
--log-file LOG_FILE log file for daemon mode
294+
-v, -vv verbose mode
295+
-q, -qq quiet mode, only show warnings/errors
271296
272297
Online help: <https://github.com/clowwindy/shadowsocks>
273298
''')

0 commit comments

Comments
 (0)