├── tmp
└── .empty
├── log
└── supervisor
│ └── .empty
├── src
├── utils
│ ├── __init__.py
│ └── text.py
├── config.py
├── templates
│ ├── layout.html
│ ├── index.html
│ ├── room.html
│ └── chat.html
├── gc.py
├── static
│ ├── script
│ │ ├── main.coffee
│ │ └── main.js
│ └── css
│ │ └── main.css
└── app.py
├── scripts
└── clear_key.py
├── .gitignore
├── setup.py
├── etc
└── supervisord.conf.in
├── README.md
└── bootstrap.py
/tmp/.empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/log/supervisor/.empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/utils/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/clear_key.py:
--------------------------------------------------------------------------------
1 | #coding=utf-8
2 | import redis
3 |
4 | rc = redis.Redis()
5 | for item in ['online_*', 'room_*']:
6 | for key in rc.keys(item):
7 | print 'deleting %s'%key
8 | rc.delete(key)
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *.pyo
4 | *.swp
5 | *.pid
6 | *.bak
7 | dump.rdb
8 | eggs/*
9 | *.o
10 | *.so
11 | *.egg-info
12 | build
13 | *.log
14 | .installed.cfg
15 | bin/
16 | develop-eggs/
17 | parts/
18 | supervisord.conf
19 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup, find_packages
2 |
3 | setup(
4 | name = 'comet',
5 | version = '1.0',
6 | description = 'comet test',
7 | author = 'lzyy',
8 | author_email = 'healdream@gmail.com',
9 | url = 'http://blog.leezhong.com',
10 | packages = find_packages('src'),
11 | package_dir = {'': 'src'},
12 | install_requires = [
13 | 'flask',
14 | 'redis',
15 | 'gevent',
16 | 'apscheduler',
17 | ],
18 | )
19 |
20 |
--------------------------------------------------------------------------------
/src/config.py:
--------------------------------------------------------------------------------
1 | #coding=utf-8
2 |
3 | DEBUG = True
4 | PORT = 9527
5 | SECRET_KEY = 'i have a dream'
6 | SESSION_COOKIE_HTTPONLY = False
7 |
8 | CHAT_NAME = u'谈天说地老天荒'
9 |
10 | ONLINE_USER_CHANNEL = 'online_user_channel'
11 | ROOM_ONLINE_USER_CHANNEL = 'room_online_user_channel_{room}'
12 | ROOM_CHANNEL = 'room_channel_{room}'
13 |
14 | ROOM_INCR_KEY = 'room_incr_key'
15 | ROOM_CONTENT_INCR_KEY = 'room_content_incr_key'
16 | ROOM_INFO_KEY = 'room_info_key_{room}'
17 |
18 | ONLINE_USER_SIGNAL = ONLINE_USER_CHANNEL
19 | ROOM_ONLINE_USER_SIGNAL = ROOM_ONLINE_USER_CHANNEL
20 | ROOM_SIGNAL = ROOM_CHANNEL
21 |
22 | CONN_CHANNEL_SET = 'conn_channel_set_{channel}'
23 |
24 | COMET_TIMEOUT = 30
25 | COMET_POLL_TIME = 2
26 |
27 | CHANNEL_PLACEHOLDER = 'jwdlh'
28 |
--------------------------------------------------------------------------------
/src/templates/layout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {% block title %}{% endblock %}
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 | {% block main %}
18 | {% endblock %}
19 |
20 |
21 |
22 |
23 | {% block script %}
24 | {% endblock %}
25 |
--------------------------------------------------------------------------------
/src/gc.py:
--------------------------------------------------------------------------------
1 | from apscheduler.scheduler import Scheduler
2 | import signal
3 | import redis
4 | import config
5 | import time
6 | import json
7 |
8 | rc = redis.Redis()
9 |
10 | sched = Scheduler()
11 | sched.start()
12 |
13 | def clear():
14 | current_time = time.time()
15 | affcted_num = rc.zremrangebyscore(config.ONLINE_USER_CHANNEL, '-inf', current_time - 60)
16 | if affcted_num:
17 | rc.zadd(config.ONLINE_USER_CHANNEL, config.CHANNEL_PLACEHOLDER, time.time())
18 |
19 | for key in rc.keys(config.ROOM_ONLINE_USER_CHANNEL.format(room='*')):
20 | affcted_num = rc.zremrangebyscore(key, '-inf', current_time - 60)
21 | if affcted_num:
22 | rc.zadd(key, config.CHANNEL_PLACEHOLDER, time.time())
23 |
24 | sched.add_cron_job(clear, minute='*')
25 |
26 | signal.pause()
27 |
--------------------------------------------------------------------------------
/src/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{config['CHAT_NAME']}}
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | {% for cate, message in get_flashed_messages(with_categories=true) %}
14 |
{{message}}
15 | {% endfor %}
16 |
17 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/etc/supervisord.conf.in:
--------------------------------------------------------------------------------
1 | [unix_http_server]
2 | file=/tmp/supervisor_comet.sock ; path to your socket file
3 |
4 | ;[inet_http_server]
5 | ;port = 127.0.0.1:9001
6 |
7 | [supervisorctl]
8 | serverurl=unix:///tmp/supervisor_comet.sock ; use a unix:// URL for a unix socket
9 | ;[supervisorctl]
10 | ;serverurl = http://localhost:9001
11 |
12 |
13 | [supervisord]
14 | logfile = ${buildout:directory}/log/supervisor/supervisord.log
15 | logfile_maxbytes = 50MB
16 | logfile_backups = 10
17 | loglevel = info
18 | pidfile = ${buildout:directory}/tmp/supervisord.pid
19 | nodaemon = false
20 |
21 |
22 | [rpcinterface:supervisor]
23 | supervisor.rpcinterface_factory=supervisor.rpcinterface:make_main_rpcinterface
24 |
25 | #[include]
26 | #files = *.supervisor
27 |
28 | [program:web]
29 | command=${buildout:directory}/bin/python ${buildout:directory}/src/app.py
30 | stderr_logfile = ${buildout:directory}/log/supervisor/web-stderr.log
31 | stdout_logfile = ${buildout:directory}/log/supervisor/web-stdout.log
32 |
33 | [program:gc]
34 | command=${buildout:directory}/bin/python ${buildout:directory}/src/gc.py
35 | stderr_logfile = ${buildout:directory}/log/supervisor/gc-stderr.log
36 | stdout_logfile = ${buildout:directory}/log/supervisor/gc-stdout.log
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | a live chat room built with python(flask / gevent / apscheduler) + redis
2 |
3 | Basic Architecture
4 | ==================
5 |
6 | 
7 |
8 | Screenshot
9 | ==========
10 |
11 | 
12 |
13 | Install
14 | =======
15 |
16 | - cd /path/to/source
17 | - python bootstrap.py
18 | - bin/buildout
19 | - make sure redis-server is started
20 | - bin/supervisord
21 | - [optional] bin/supervisorctl
22 | - goto localhost:9527
23 |
24 | Tips
25 | ====
26 |
27 | - open multi browser to test live communication
28 | - execute `bin/python scripts/clear_key.py` to clear all data
29 |
30 | Changes
31 | =======
32 |
33 | 0.2
34 | ---
35 |
36 | * adjust comet strategy
37 | * add admin role
38 | * fix duplicate name
39 |
40 | 0.1.1
41 | -----
42 |
43 | * adjust create room UI / UE
44 | * add rm room func
45 | * improve add chat message's response speed
46 | * bugfixes
47 |
48 | 0.1
49 | ---
50 |
51 | * add home page (all rooms in one page, and live content)
52 | * custom nickname
53 | * create room
54 | * coffee-script
55 | * bugfixes
56 |
57 | 
58 | 
59 |
--------------------------------------------------------------------------------
/src/templates/room.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 |
3 | {% block title %}
4 | {{room_name}} - {{config['CHAT_NAME']}}
5 | {% endblock %}
6 |
7 | {% block main %}
8 |
9 |
10 |
在线用户
11 |
12 | {% for user in room_online_users %}
13 | {{user}}
14 | {% endfor %}
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | | user |
23 | content |
24 | time |
25 |
26 |
27 |
28 |
29 | {% for item in room_content %}
30 |
31 | | {{item.user|safe}} |
32 | {{item.content|safe}} |
33 | {{item.created}} |
34 |
35 | {% endfor %}
36 |
37 |
38 |
39 |
44 | {% endblock %}
45 |
46 | {% block script %}
47 |
48 |
49 |
50 | {% endblock %}
51 |
--------------------------------------------------------------------------------
/src/templates/chat.html:
--------------------------------------------------------------------------------
1 | {% extends 'layout.html' %}
2 |
3 | {% block title %}
4 | {{config['CHAT_NAME']}}
5 | {% endblock %}
6 |
7 | {% block main %}
8 | {% for room in rooms %}
9 |
10 |
16 |
17 |
18 | {% for content in room.content %}
19 | - {{content.content}}
20 | {% endfor %}
21 |
22 |
23 |
24 |
25 | {% endfor %}
26 |
27 |
38 | {% endblock %}
39 |
40 | {% block script %}
41 |
42 |
43 |
44 | {% endblock %}
45 |
--------------------------------------------------------------------------------
/src/static/script/main.coffee:
--------------------------------------------------------------------------------
1 | channel = Math.floor(Math.random()*1000000)
2 |
3 | create_chat_comet = (ts = '') ->
4 | arg = "comet=room_online_users_count_all,room_content_all&channel=#{channel}&ts=#{ts}"
5 | $.getJSON "/comet?uri=#{window.uri}{arg}", (result) ->
6 | if result.type == 'room_online_users'
7 | room_online_users_count_all result.data
8 | else if result.type == 'add_content'
9 | room_content_all result.data
10 | create_chat_comet(result.ts)
11 |
12 | create_room_comet = (ts = '') ->
13 | arg = "comet=room_online_users,room_content&channel=#{channel}&room_id=#{window.room_id}&ts=#{ts}"
14 | $.getJSON "/comet?uri=#{window.uri}{arg}", (result) ->
15 | if result.type == 'online_users'
16 | online_users result.data
17 | else if result.type == 'room_online_users'
18 | room_online_users result.data
19 | else if result.type == 'add_content'
20 | room_content result.data
21 | create_room_comet(result.ts)
22 |
23 | room_online_users_count_all = (data) ->
24 | $("#room-#{data.room_id} .header span").text("(#{data.users.length}人在线)")
25 |
26 | room_content_all = (data) ->
27 | $body = $("#room-#{data.room_id} .body")
28 | for content in data.content
29 | $body.find('ul').append("#{content.content}")
30 | while $body.find('ul li').length > 5
31 | $body.find('ul li:first-child').remove()
32 |
33 | room_online_users = (data) ->
34 | html = ''
35 | for item in data.users
36 | html += "#{item}"
37 | $('#room_online_user .user_list').html(html)
38 |
39 | room_content = (data) ->
40 | console.log data
41 | for content in data.content
42 | $msg = $("#msg-#{content.id}")
43 | if not $msg.length
44 | html = "
45 | | #{content.user} |
46 | #{content.content} |
47 | #{content.created} |
48 |
49 | "
50 | $('#chat_content table tbody').append(html)
51 | $("#chat_content table tbody tr:last-child").get(0).scrollIntoView()
52 | if not window.entering_content
53 | if document.title.substr(0, 1) != '('
54 | document.title = "(1) #{document.title}"
55 | else
56 | current_title = document.title
57 | current_count = parseInt(current_title.slice(current_title.indexOf('(')+1, current_title.indexOf(')')))
58 | new_count = current_count + 1
59 | document.title = current_title.replace("(#{current_count})", "(#{new_count})")
60 |
61 | $ ->
62 | if $('#chat_content tbody tr').length
63 | $('#chat_content tbody tr:last-child').get(0).scrollIntoView()
64 |
65 | $('#post_content').bind 'submit', (evt) ->
66 | evt.preventDefault()
67 | data = $(this).serialize()
68 | if $.trim($(this).find('input[name="content"]').val()) == ''
69 | return false
70 |
71 | $.post(
72 | $(this).attr('action')
73 | data
74 | (result) ->
75 | $('#post_content input[name="content"]').val('')
76 | window.entering_content = true
77 | document.title = document.title.replace(/\([0-9]+\) /, '')
78 | room_content {'content': [result]}
79 | 'json'
80 | )
81 |
82 | $('#post_content input[name="content"]').bind 'click', (evt) ->
83 | window.entering_content = true
84 | document.title = document.title.replace(/\([0-9]+\) /, '')
85 |
86 | $('#post_content input[name="content"]').bind 'blur', (evt) ->
87 | window.entering_content = false
88 |
89 | $('.add_room').bind 'click', (evt) ->
90 | $('.chat-bubble').toggle()
91 |
92 | $('.header .close').bind 'click', (evt) ->
93 | rs = confirm('do you really want to remove this room?')
94 | if rs
95 | room_info = $(this).parent().parent().attr('id').split('-')
96 | room_id = room_info[room_info.length-1]
97 | $.post(
98 | '/rm_room'
99 | {room_id: room_id}
100 | (result) ->
101 | if result.status == 'ok'
102 | window.location = result.content.url
103 | else
104 | alert result.content.message
105 | 'json'
106 | )
107 |
--------------------------------------------------------------------------------
/src/static/script/main.js:
--------------------------------------------------------------------------------
1 | var channel, create_chat_comet, create_room_comet, room_content, room_content_all, room_online_users, room_online_users_count_all;
2 | channel = Math.floor(Math.random() * 1000000);
3 | create_chat_comet = function(ts) {
4 | var arg;
5 | if (ts == null) {
6 | ts = '';
7 | }
8 | arg = "comet=room_online_users_count_all,room_content_all&channel=" + channel + "&ts=" + ts;
9 | return $.getJSON("/comet?uri=" + window.uri + "&" + arg, function(result) {
10 | if (result.type === 'room_online_users') {
11 | room_online_users_count_all(result.data);
12 | } else if (result.type === 'add_content') {
13 | room_content_all(result.data);
14 | }
15 | return create_chat_comet(result.ts);
16 | });
17 | };
18 | create_room_comet = function(ts) {
19 | var arg;
20 | if (ts == null) {
21 | ts = '';
22 | }
23 | arg = "comet=room_online_users,room_content&channel=" + channel + "&room_id=" + window.room_id + "&ts=" + ts;
24 | return $.getJSON("/comet?uri=" + window.uri + "&" + arg, function(result) {
25 | if (result.type === 'online_users') {
26 | online_users(result.data);
27 | } else if (result.type === 'room_online_users') {
28 | room_online_users(result.data);
29 | } else if (result.type === 'add_content') {
30 | room_content(result.data);
31 | }
32 | return create_room_comet(result.ts);
33 | });
34 | };
35 | room_online_users_count_all = function(data) {
36 | return $("#room-" + data.room_id + " .header span").text("(" + data.users.length + "人在线)");
37 | };
38 | room_content_all = function(data) {
39 | var $body, content, _i, _len, _ref, _results;
40 | $body = $("#room-" + data.room_id + " .body");
41 | _ref = data.content;
42 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
43 | content = _ref[_i];
44 | $body.find('ul').append("" + content.content + "");
45 | }
46 | _results = [];
47 | while ($body.find('ul li').length > 5) {
48 | _results.push($body.find('ul li:first-child').remove());
49 | }
50 | return _results;
51 | };
52 | room_online_users = function(data) {
53 | var html, item, _i, _len, _ref;
54 | html = '';
55 | _ref = data.users;
56 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
57 | item = _ref[_i];
58 | html += "" + item + "";
59 | }
60 | return $('#room_online_user .user_list').html(html);
61 | };
62 | room_content = function(data) {
63 | var $msg, content, current_count, current_title, html, new_count, _i, _len, _ref, _results;
64 | console.log(data);
65 | _ref = data.content;
66 | _results = [];
67 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
68 | content = _ref[_i];
69 | $msg = $("#msg-" + content.id);
70 | _results.push(!$msg.length ? (html = " | " + content.user + " | " + content.content + " | " + content.created + " |
", $('#chat_content table tbody').append(html), $("#chat_content table tbody tr:last-child").get(0).scrollIntoView(), !window.entering_content ? document.title.substr(0, 1) !== '(' ? document.title = "(1) " + document.title : (current_title = document.title, current_count = parseInt(current_title.slice(current_title.indexOf('(') + 1, current_title.indexOf(')'))), new_count = current_count + 1, document.title = current_title.replace("(" + current_count + ")", "(" + new_count + ")")) : void 0) : void 0);
71 | }
72 | return _results;
73 | };
74 | $(function() {
75 | if ($('#chat_content tbody tr').length) {
76 | $('#chat_content tbody tr:last-child').get(0).scrollIntoView();
77 | }
78 | $('#post_content').bind('submit', function(evt) {
79 | var data;
80 | evt.preventDefault();
81 | data = $(this).serialize();
82 | if ($.trim($(this).find('input[name="content"]').val()) === '') {
83 | return false;
84 | }
85 | return $.post($(this).attr('action'), data, function(result) {
86 | $('#post_content input[name="content"]').val('');
87 | window.entering_content = true;
88 | document.title = document.title.replace(/\([0-9]+\) /, '');
89 | return room_content({
90 | 'content': [result]
91 | });
92 | }, 'json');
93 | });
94 | $('#post_content input[name="content"]').bind('click', function(evt) {
95 | window.entering_content = true;
96 | return document.title = document.title.replace(/\([0-9]+\) /, '');
97 | });
98 | $('#post_content input[name="content"]').bind('blur', function(evt) {
99 | return window.entering_content = false;
100 | });
101 | $('.add_room').bind('click', function(evt) {
102 | return $('.chat-bubble').toggle();
103 | });
104 | return $('.header .close').bind('click', function(evt) {
105 | var room_id, room_info, rs;
106 | rs = confirm('do you really want to remove this room?');
107 | if (rs) {
108 | room_info = $(this).parent().parent().attr('id').split('-');
109 | room_id = room_info[room_info.length - 1];
110 | return $.post('/rm_room', {
111 | room_id: room_id
112 | }, function(result) {
113 | if (result.status === 'ok') {
114 | return window.location = result.content.url;
115 | } else {
116 | return alert(result.content.message);
117 | }
118 | }, 'json');
119 | }
120 | });
121 | });
--------------------------------------------------------------------------------
/src/utils/text.py:
--------------------------------------------------------------------------------
1 | #coding=utf-8
2 |
3 | import re
4 |
5 | def escape_text(txt):
6 | return txt.replace('<', '<').replace('>', '>')
7 |
8 | def make_re_url():
9 | url = r"""
10 | (?xi)
11 | \b
12 | ( # Capture 1: entire matched URL
13 | (?:
14 | https?:// # http or https protocol
15 | | # or
16 | www\d{0,3}[.] # "www.", "www1.", "www2." … "www999."
17 | | # or
18 | [a-z0-9.\-]+[.][a-z]{2,4}/ # looks like domain name followed by a slash
19 | )
20 | (?: # One or more:
21 | [^\s()<>]+ # Run of non-space, non-()<>
22 | | # or
23 | \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
24 | )+
25 | (?: # End with:
26 | \(([^\s()<>]+|(\([^\s()<>]+\)))*\) # balanced parens, up to 2 levels
27 | | # or
28 | [^\s`!()\[\]{};:'".,<>?«»“”‘’] # not a space or one of these punct chars
29 | )
30 | )
31 | """
32 | return re.compile(url, re.VERBOSE | re.MULTILINE)
33 |
34 | RE_URL = make_re_url()
35 |
36 | def linkify(txt,
37 | shorten = True,
38 | target_blank = False,
39 | require_protocol = False,
40 | permitted_protocols = ["http", "https"],
41 | local_domain = None):
42 | """Converts plain txt into HTML with links. back ported from tornado 2.0
43 |
44 | For example: ``linkify("Hello http://tornadoweb.org!")`` would return
45 | ``Hello http://tornadoweb.org!``
46 |
47 | Parameters:
48 |
49 | shorten: Long urls will be shortened for display.
50 |
51 | extra_params: Extra txt to include in the link tag,
52 | e.g. linkify(txt, extra_params='rel="nofollow" class="external"')
53 |
54 | require_protocol: Only linkify urls which include a protocol. If this is
55 | False, urls such as www.facebook.com will also be linkified.
56 |
57 | permitted_protocols: List (or set) of protocols which should be linkified,
58 | e.g. linkify(txt, permitted_protocols=["http", "ftp", "mailto"]).
59 | It is very unsafe to include protocols such as "javascript".
60 | local_domain: domain link
61 | """
62 |
63 | if txt is None or not txt.strip():
64 | return txt
65 | extra_params = ' rel="nofollow"'
66 |
67 | def make_link(m):
68 | tb = target_blank
69 | url = m.group(1)
70 | proto = m.group(2)
71 | if require_protocol and not proto:
72 | return url # not protocol, no linkify
73 |
74 | if proto and proto not in permitted_protocols:
75 | return url # bad protocol, no linkify
76 |
77 | href = m.group(1)
78 | #href = xhtml_unescape(href).strip()
79 | if not proto:
80 | href = "http://" + href # no proto specified, use http
81 |
82 | params = extra_params
83 | if proto:
84 | proto_len = len(proto) + 1 + len(m.group(3) or "") # +1 for :
85 | else:
86 | proto_len = 0
87 |
88 | parts = url[proto_len:].split("/")
89 |
90 | proto_part = url[:proto_len] if proto != 'http' else ''
91 | host_part = parts[0]
92 |
93 | if host_part.startswith('www.'):
94 | host_part = '.'.join(host_part.split('.')[1:]) # add extra idnetification for external link
95 | if not local_domain or not host_part.endswith(local_domain):
96 | params += ' class="external" '
97 | tb = True
98 |
99 | if tb:
100 | params += 'target="_blank"'
101 |
102 | # clip long urls. max_len is just an approximation
103 | max_len = 30
104 | if shorten and len(url) > max_len:
105 | before_clip = url[proto_len:]
106 | url = proto_part + host_part
107 | #if len(parts) > 2:
108 | # Grab the whole host part plus the first bit of the path
109 | # The path is usually not that interesting once shortened
110 | # (no more slug, etc), so it really just provides a little
111 | # extra indication of shortening.
112 | for n,p in enumerate(parts[1:]):
113 | if n:
114 | cut = 6
115 | else:
116 | cut = 8
117 | url += '/' + p[:cut].split('?')[0].split('.')[0]
118 | if len(p) < 4:
119 | continue
120 | break
121 | #url = proto_part + host_part + "/" + \
122 | #parts[1][:8].split('?')[0].split('.')[0]
123 |
124 | if len(url) > max_len * 1.5: # still too long
125 | url = url[:max_len]
126 |
127 | if url != before_clip:
128 | amp = url.rfind('&')
129 | # avoid splitting html char entities
130 | if amp > max_len - 5:
131 | url = url[:amp]
132 | url += "..."
133 |
134 | if len(url) >= len(before_clip):
135 | url = before_clip
136 | else:
137 | # full url is visible on mouse-over (for those who don't
138 | # have a status bar, such as Safari by default)
139 | params += ' title="%s"' % href
140 |
141 | return u'%s' % (href, params, url)
142 |
143 | return RE_URL.sub(make_link, txt)
144 |
145 | if __name__ == '__main__':
146 | txt = 'have a link test www.zhihu.com/question/19550224?noti_id=123 how about?'
147 | txt1 = 'hello http://tornadoweb.org!'
148 | print linkify(txt)
149 |
--------------------------------------------------------------------------------
/src/static/css/main.css:
--------------------------------------------------------------------------------
1 | p {padding:0; margin:0}
2 | body, html {margin:0; padding:0; color:#222}
3 | body {font:13px/22px 'Helvetica Neue',Helvetica,Arial,Sans-serif}
4 | ul, li {padding:0; margin:0; list-style:none}
5 |
6 | .error {color: red}
7 |
8 | li.new {background: #ffe}
9 |
10 | #header {
11 | border-bottom:1px solid #ccc;
12 | height: 50px;
13 | }
14 |
15 | #header .inner {
16 | width: 960px;
17 | margin: 0 auto;
18 | }
19 |
20 | #header .inner #logo {
21 | float:left;
22 | font-size: 18px;
23 | font-weight: bold;
24 | line-height: 50px;
25 | }
26 |
27 | #header .inner #logo a {
28 | text-decoration: none;
29 | color: #666;
30 | }
31 |
32 | #header .user_info {
33 | float:right;
34 | line-height: 50px;
35 | color: #666;
36 | }
37 |
38 | #main {
39 | width: 960px;
40 | margin: 20px auto;
41 | overflow: hidden;
42 | }
43 |
44 | .add_room {
45 | position: absolute;
46 | left: 41px;
47 | top: 3px;
48 | border: 1px solid #eee;
49 | background: #ffc;
50 | padding: 10px 15px;
51 | -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
52 | -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
53 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
54 | -webkit-border-radius: 3px;
55 | -moz-border-radius: 3px;
56 | -o-border-radius: 3px;
57 | color: #666;
58 | font-size: 18px;
59 | float: left;
60 | margin: 80px 0px 80px 50px;
61 | cursor: pointer;
62 | }
63 |
64 | .box {
65 | -webkit-border-radius: 3px;
66 | -moz-border-radius: 3px;
67 | -o-border-radius: 3px;
68 | border-radius: 3px;
69 | -webkit-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
70 | -moz-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
71 | -o-box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
72 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.15);
73 | background-color: white;
74 | border: 1px solid #eee;
75 | margin: 0 55px 55px 0;
76 | }
77 |
78 | .box .header {
79 | background: #f8f8f8;
80 | padding: 5px;
81 | font-size: 14px;
82 | border-bottom: 1px solid #ccc;
83 | }
84 |
85 | .box .header a {
86 | color: #444;
87 | text-decoration: none;
88 | }
89 |
90 | .box .header a:hover {
91 | text-decoration: underline;
92 | }
93 |
94 | .box .header span {
95 | font-size: 12px;
96 | color: #999;
97 | padding-left: 3px;
98 | }
99 |
100 | .box .body {
101 | height: 160px;
102 | position: relative;
103 | }
104 |
105 | .box .body .chat_line {
106 | }
107 |
108 | .box .body .chat_line li {
109 | padding: 5px;
110 | border-bottom: 1px solid #eee;
111 | color: #666;
112 | height: 21px;
113 | overflow: hidden;
114 | }
115 |
116 | .box .body .chat_line li:last-child {
117 | border:none;
118 | }
119 |
120 | .box .body .single_input {
121 | width: 235px;
122 | margin: 7px 5px;
123 | position: absolute;
124 | bottom: 0;
125 | }
126 |
127 | #post_content {
128 | }
129 |
130 | .user_status {border:1px solid #ccc; padding:0px; overflow:hidden; margin:5px 0 10px 0}
131 | .user_status p {float: left;padding:5px; border-right:1px solid #ccc}
132 | .user_list {
133 | float:left;
134 | color: #666;
135 | }
136 |
137 | .vh_center {
138 | height: 100%;
139 | width: 100%;
140 | overflow: visible;
141 | position:relative;
142 | }
143 |
144 | .vh_center .inner {
145 | position: absolute;
146 | top: 50%;
147 | margin-top: -30px;
148 | left: 50%;
149 | margin-left: -100px;
150 | }
151 |
152 | .user_list span {
153 | color: #105CB6;
154 | background: #E1F0F7;
155 | padding: 0 10px;
156 | -webkit-border-radius: 10px;
157 | -moz-border-radius: 10px;
158 | -o-border-radius: 10px;
159 | border-radius: 10px;
160 | text-decoration: none;
161 | margin: 5px 1px 5px 5px;
162 | display: inline-block;
163 | }
164 |
165 | #chat_content table thead tr {
166 | display: block;
167 | position: relative;
168 | }
169 |
170 | #chat_content table thead th, #chat_content table tbody td {
171 | width: 155px;
172 | }
173 |
174 | #chat_content table thead th + th, #chat_content table tbody td + td {
175 | width: 600px;
176 | }
177 |
178 | #chat_content table thead th + th + th, #chat_content table tbody td + td + td {
179 | width: 150px;
180 | }
181 |
182 | #chat_content table tbody{
183 | max-height: 400px;
184 | overflow: auto;
185 | width: 100%;
186 | display: block
187 | }
188 |
189 | #chat_content table tbody td + td + td {
190 | color: #999;
191 | }
192 |
193 | table {border-collapse: separate; margin-bottom:15px}
194 | table th {text-align:left; background: #f4f4f4; padding:5px}
195 | table td {padding:5px; border-bottom:1px solid #f4f4f4}
196 |
197 | input[type="text"] {padding:5px;font-size:14px}
198 | input[type="submit"] {
199 | background: #F8F8F9;
200 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#f8f8f9',endColorstr='#e6e6e8');
201 | background: -webkit-gradient(linear,0 0,0 100%,from(#F8F8F9),to(#E6E6E8));
202 | background: -moz-linear-gradient(top,#F8F8F9,#E6E6E8);
203 | -webkit-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1);
204 | -moz-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1);
205 | -o-box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1);
206 | box-shadow: 0 1px 0 #fff inset,0 1px 0 rgba(0,0,0,.1);
207 | text-shadow: 0 1px 0 white;
208 | border: 1px solid #BBB;
209 | color: #666!important;
210 | font: normal 14px/22px "Helvetica Neue",Arial,sans-serif;
211 | text-decoration: none!important;
212 | vertical-align: middle;
213 | display: inline-block;
214 | text-align: center;
215 | padding: 4px 10px;
216 | cursor: pointer;
217 | -webkit-border-radius: 3px;
218 | -moz-border-radius: 3px;
219 | -o-border-radius: 3px;
220 | margin:-4px 0 0 7px;
221 | }
222 |
223 | .chat-bubble {
224 | background-color:#f8f8f8;
225 | border:1px solid #eee;
226 | line-height:1.3em;
227 | margin:10px auto;
228 | padding:10px;
229 | position:relative;
230 | text-align:center;
231 | width:200px;
232 | -moz-border-radius:10px;
233 | -webkit-border-radius:10px;
234 | }
235 | .chat-bubble-arrow-border {
236 | border-color: #eee transparent transparent transparent;
237 | border-style: solid;
238 | border-width: 10px;
239 | height:0;
240 | width:0;
241 | position:absolute;
242 | bottom:-21px;
243 | left:100px;
244 | }
245 | .chat-bubble-arrow {
246 | border-color: #f8f8f8 transparent transparent transparent;
247 | border-style: solid;
248 | border-width: 10px;
249 | height:0;
250 | width:0;
251 | position:absolute;
252 | bottom:-19px;
253 | left:100px;
254 | }
255 |
256 | #add_room_wrapper {
257 | position:relative;
258 | float:left;
259 | height:200px;
260 | }
261 |
262 | .header .close {
263 | float: right;
264 | color: #ddd;
265 | font-size: 14px;
266 | cursor: pointer;
267 | }
268 |
--------------------------------------------------------------------------------
/src/app.py:
--------------------------------------------------------------------------------
1 | #coding=utf-8
2 |
3 | from flask import Flask, request, session, render_template, Response, jsonify, redirect, flash
4 | from gevent.wsgi import WSGIServer
5 | from utils.text import linkify, escape_text
6 | import gevent
7 | import redis
8 | import time
9 | import config
10 | import json
11 |
12 | app = Flask(__name__)
13 | app.config.from_object(config)
14 | app.debug = True
15 |
16 | rc = redis.Redis()
17 |
18 | def is_admin():
19 | if session.get('admin'):
20 | return True
21 | return False
22 |
23 | def is_duplicate_name():
24 | user_name = session.get('user', '')
25 | for online_user in rc.zrange(config.ONLINE_USER_CHANNEL, 0, -1):
26 | if online_user == user_name.encode('utf-8'):
27 | flash(u'该名(%s)已被抢占,换一个吧'%user_name, 'error')
28 | session.pop('user', None)
29 | return True
30 | return False
31 |
32 | @app.route('/adm1n')
33 | def admin():
34 | session['admin'] = 1
35 | return redirect('/chat')
36 |
37 | @app.route('/')
38 | def index():
39 | if session.get('user'):
40 | return redirect('/chat')
41 | return render_template('index.html')
42 |
43 | @app.route('/change_name')
44 | def change_name():
45 | session.pop('user', None)
46 | return redirect('/')
47 |
48 | @app.route('/login', methods=['POST'])
49 | def login():
50 | user_name = request.form.get('user_name', '')
51 | session['user'] = user_name
52 | if is_duplicate_name():
53 | return redirect('/')
54 | return redirect('/chat')
55 |
56 | @app.route('/chat', methods=['GET', 'POST'])
57 | def chat():
58 | if not session.get('user'):
59 | return redirect('/')
60 |
61 | if request.method == 'POST':
62 | title = request.form.get('title', '')
63 | if not title:
64 | return jsonify(status='error', message={'title': 'empty title'})
65 |
66 | room_id = rc.incr(config.ROOM_INCR_KEY)
67 | rc.set(config.ROOM_INFO_KEY.format(room=room_id),
68 | json.dumps({'title': title,
69 | 'room_id': room_id,
70 | 'user': session['user'],
71 | 'created': time.time()
72 | }))
73 | return redirect('/chat')
74 |
75 | rooms = []
76 | room_info_keys = config.ROOM_INFO_KEY.format(room='*')
77 | for room_info_key in rc.keys(room_info_keys):
78 | room_info = json.loads(rc.get(room_info_key))
79 | users = rc.zrevrange(config.ROOM_ONLINE_USER_CHANNEL.format(room=room_info['room_id']), 0, -1)
80 | rm_channel_placeholder(users)
81 | rooms.append({
82 | 'id': room_info['room_id'],
83 | 'creator': room_info['user'],
84 | 'content': map(json.loads, reversed(rc.zrevrange(config.ROOM_CHANNEL.format(room=room_info['room_id']), 0, 4))),
85 | 'title': room_info['title'],
86 | 'users': users,
87 | })
88 |
89 | return render_template('chat.html',
90 | rooms = rooms,
91 | uri = request.path,
92 | is_admin = is_admin(),
93 | )
94 |
95 | @app.route('/rm_room', methods=['POST'])
96 | def rm_room():
97 | if not session.get('user'):
98 | return redirect('/')
99 |
100 | room_id = request.form.get('room_id')
101 | room_key = config.ROOM_INFO_KEY.format(room=room_id)
102 | room_channel = config.ROOM_CHANNEL.format(room=room_id)
103 | room = json.loads(rc.get(room_key))
104 | if not is_admin():
105 | return jsonify(status='error', content={'message': 'permission denied'})
106 |
107 | rc.delete(room_key)
108 | rc.delete(room_channel)
109 | return jsonify(status='ok', content={'url': '/chat'})
110 |
111 | @app.route('/chat/')
112 | def chat_room(room_id):
113 | if not session.get('user'):
114 | return redirect('/')
115 |
116 | user_name = session['user']
117 | room = json.loads(rc.get(config.ROOM_INFO_KEY.format(room=room_id)))
118 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id)
119 | room_online_user_signal = config.ROOM_ONLINE_USER_SIGNAL.format(room=room_id)
120 |
121 | rc.zadd(config.ONLINE_USER_CHANNEL, user_name, time.time())
122 | rc.zadd(room_online_user_channel, user_name, time.time())
123 | rc.publish(config.ONLINE_USER_SIGNAL, '')
124 | rc.publish(room_online_user_signal, json.dumps({'room_id':room_id}))
125 |
126 | room_content = reversed(rc.zrevrange(config.ROOM_CHANNEL.format(room=room_id), 0, 200, withscores=True))
127 | room_content_list = []
128 | for item in room_content:
129 | room_content_list.append(json.loads(item[0]))
130 |
131 | room_online_users =[]
132 | for user in rc.zrange(room_online_user_channel, 0, -1):
133 | if user == config.CHANNEL_PLACEHOLDER:
134 | continue
135 | room_online_users.append(user.decode('utf-8'))
136 |
137 | return render_template('room.html',
138 | room_content = room_content_list,
139 | uri = request.path,
140 | room_name = room['title'],
141 | room_id = room_id,
142 | room_online_users = room_online_users)
143 |
144 | @app.route('/post_content', methods=['POST'])
145 | def post_content():
146 | if not session.get('user'):
147 | return redirect('/')
148 |
149 | room_id = request.form.get('room_id')
150 | data = {'user': session.get('user'),
151 | 'content': linkify(escape_text(request.form.get('content', ''))),
152 | 'created': time.strftime('%m-%d %H:%M:%S'),
153 | 'room_id': room_id,
154 | 'id': rc.incr(config.ROOM_CONTENT_INCR_KEY),
155 | }
156 | rc.zadd(config.ROOM_CHANNEL.format(room=room_id), json.dumps(data), time.time())
157 | return jsonify(**data)
158 |
159 | @app.route('/comet')
160 | def comet():
161 | uri = request.args.get('uri', '')
162 | room_id = request.args.get('room_id', '')
163 | comet = request.args.get('comet', '').split(',')
164 | ts = request.args.get('ts', time.time())
165 | channel = config.CONN_CHANNEL_SET.format(channel=request.args.get('channel'))
166 |
167 | cmt = Comet()
168 |
169 | result = cmt.check(channel, comet, ts, room_id)
170 | if result:
171 | return jsonify(**result)
172 |
173 | passed_time = 0
174 | while passed_time < config.COMET_TIMEOUT:
175 | comet = rc.smembers(config.CONN_CHANNEL_SET.format(channel=channel))
176 | result = cmt.check(channel, comet, ts, room_id)
177 | if result:
178 | return jsonify(**result)
179 | passed_time += config.COMET_POLL_TIME
180 | gevent.sleep(config.COMET_POLL_TIME)
181 |
182 | if room_id:
183 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id)
184 | rc.zadd(room_online_user_channel, session['user'], time.time())
185 | rc.zadd(config.ONLINE_USER_CHANNEL, session['user'], time.time())
186 |
187 | return jsonify(ts=time.time())
188 |
189 | def rm_channel_placeholder(data):
190 | for index, item in enumerate(data):
191 | if item == config.CHANNEL_PLACEHOLDER:
192 | data.pop(index)
193 |
194 | class Comet(object):
195 | def check(self, channel, comet, ts, room_id = 0):
196 | conn_channel_set = config.CONN_CHANNEL_SET.format(channel=channel)
197 | if 'online_users' in comet:
198 | rc.sadd(conn_channel_set, 'online_users')
199 | new_data = rc.zrangebyscore(config.ONLINE_USER_CHANNEL, ts, '+inf')
200 | if new_data:
201 | data=rc.zrevrange(config.ONLINE_USER_CHANNEL, 0, -1)
202 | data.pop(0) if data[0] == config.CHANNEL_PLACEHOLDER else True
203 | return dict(data=data,
204 | ts=time.time(), type='online_users')
205 |
206 | if 'room_online_users' in comet:
207 | rc.sadd(conn_channel_set, 'room_online_users')
208 | room_online_user_channel = config.ROOM_ONLINE_USER_CHANNEL.format(room=room_id)
209 | new_data = rc.zrangebyscore(room_online_user_channel, ts, '+inf')
210 | if new_data:
211 | users=rc.zrevrange(room_online_user_channel, 0, -1)
212 | rm_channel_placeholder(users)
213 | data = {'room_id': room_id, 'users': users}
214 | return dict(data=data,
215 | ts=time.time(), type='room_online_users')
216 |
217 | if 'room_content' in comet:
218 | rc.sadd(conn_channel_set, 'room_content')
219 | room_channel = config.ROOM_CHANNEL.format(room=room_id)
220 | new_data = rc.zrangebyscore(room_channel, ts, '+inf')
221 | if new_data:
222 | data = {'room_id': room_id, 'content':[]}
223 | for item in new_data:
224 | data['content'].append(json.loads(item))
225 | return dict(data=data, ts=time.time(), type='add_content')
226 |
227 | if 'room_online_users_count_all' in comet:
228 | rc.sadd(conn_channel_set, 'room_online_users_count_all')
229 | room_online_user_channels = config.ROOM_ONLINE_USER_CHANNEL.format(room='*')
230 | for room_online_user_channel in rc.keys(room_online_user_channels):
231 | new_data = rc.zrangebyscore(room_online_user_channel, ts, '+inf')
232 | if new_data:
233 | users=rc.zrevrange(room_online_user_channel, 0, -1)
234 | rm_channel_placeholder(users)
235 | room_id = room_online_user_channel.split('_')[-1]
236 | data = {'room_id': room_id, 'users': users}
237 | return dict(data=data, ts=time.time(), type='room_online_users')
238 |
239 | if 'room_content_all' in comet:
240 | rc.sadd(conn_channel_set, 'room_content_all')
241 | room_channels = config.ROOM_CHANNEL.format(room='*')
242 | for room_channel in rc.keys(room_channels):
243 | new_data = rc.zrangebyscore(room_channel, ts, '+inf')
244 | if new_data:
245 | room_id = room_channel.split('_')[-1]
246 | data = {'room_id': room_id, 'content':[]}
247 | for item in new_data:
248 | data['content'].append(json.loads(item))
249 | return dict(data=data, ts=time.time(), type='add_content')
250 |
251 | def run():
252 | http_server = WSGIServer(('', config.PORT), app)
253 | http_server.serve_forever()
254 | #app.run(port=config.PORT)
255 |
256 | if __name__ == '__main__':
257 | run()
258 |
--------------------------------------------------------------------------------
/bootstrap.py:
--------------------------------------------------------------------------------
1 | ##############################################################################
2 | #
3 | # Copyright (c) 2006 Zope Foundation and Contributors.
4 | # All Rights Reserved.
5 | #
6 | # This software is subject to the provisions of the Zope Public License,
7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
11 | # FOR A PARTICULAR PURPOSE.
12 | #
13 | ##############################################################################
14 | """Bootstrap a buildout-based project
15 |
16 | Simply run this script in a directory containing a buildout.cfg.
17 | The script accepts buildout command-line options, so you can
18 | use the -c option to specify an alternate configuration file.
19 | """
20 |
21 | import os, shutil, sys, tempfile, urllib, urllib2, subprocess
22 | from optparse import OptionParser
23 |
24 | if sys.platform == 'win32':
25 | def quote(c):
26 | if ' ' in c:
27 | return '"%s"' % c # work around spawn lamosity on windows
28 | else:
29 | return c
30 | else:
31 | quote = str
32 |
33 | # See zc.buildout.easy_install._has_broken_dash_S for motivation and comments.
34 | stdout, stderr = subprocess.Popen(
35 | [sys.executable, '-Sc',
36 | 'try:\n'
37 | ' import ConfigParser\n'
38 | 'except ImportError:\n'
39 | ' print 1\n'
40 | 'else:\n'
41 | ' print 0\n'],
42 | stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()
43 | has_broken_dash_S = bool(int(stdout.strip()))
44 |
45 | # In order to be more robust in the face of system Pythons, we want to
46 | # run without site-packages loaded. This is somewhat tricky, in
47 | # particular because Python 2.6's distutils imports site, so starting
48 | # with the -S flag is not sufficient. However, we'll start with that:
49 | if not has_broken_dash_S and 'site' in sys.modules:
50 | # We will restart with python -S.
51 | args = sys.argv[:]
52 | args[0:0] = [sys.executable, '-S']
53 | args = map(quote, args)
54 | os.execv(sys.executable, args)
55 | # Now we are running with -S. We'll get the clean sys.path, import site
56 | # because distutils will do it later, and then reset the path and clean
57 | # out any namespace packages from site-packages that might have been
58 | # loaded by .pth files.
59 | clean_path = sys.path[:]
60 | import site # imported because of its side effects
61 | sys.path[:] = clean_path
62 | for k, v in sys.modules.items():
63 | if k in ('setuptools', 'pkg_resources') or (
64 | hasattr(v, '__path__') and
65 | len(v.__path__) == 1 and
66 | not os.path.exists(os.path.join(v.__path__[0], '__init__.py'))):
67 | # This is a namespace package. Remove it.
68 | sys.modules.pop(k)
69 |
70 | is_jython = sys.platform.startswith('java')
71 |
72 | setuptools_source = 'http://peak.telecommunity.com/dist/ez_setup.py'
73 | distribute_source = 'http://python-distribute.org/distribute_setup.py'
74 |
75 |
76 | # parsing arguments
77 | def normalize_to_url(option, opt_str, value, parser):
78 | if value:
79 | if '://' not in value: # It doesn't smell like a URL.
80 | value = 'file://%s' % (
81 | urllib.pathname2url(
82 | os.path.abspath(os.path.expanduser(value))),)
83 | if opt_str == '--download-base' and not value.endswith('/'):
84 | # Download base needs a trailing slash to make the world happy.
85 | value += '/'
86 | else:
87 | value = None
88 | name = opt_str[2:].replace('-', '_')
89 | setattr(parser.values, name, value)
90 |
91 | usage = '''\
92 | [DESIRED PYTHON FOR BUILDOUT] bootstrap.py [options]
93 |
94 | Bootstraps a buildout-based project.
95 |
96 | Simply run this script in a directory containing a buildout.cfg, using the
97 | Python that you want bin/buildout to use.
98 |
99 | Note that by using --setup-source and --download-base to point to
100 | local resources, you can keep this script from going over the network.
101 | '''
102 |
103 | parser = OptionParser(usage=usage)
104 | parser.add_option("-v", "--version", dest="version",
105 | help="use a specific zc.buildout version")
106 | parser.add_option("-d", "--distribute",
107 | action="store_true", dest="use_distribute", default=False,
108 | help="Use Distribute rather than Setuptools.")
109 | parser.add_option("--setup-source", action="callback", dest="setup_source",
110 | callback=normalize_to_url, nargs=1, type="string",
111 | help=("Specify a URL or file location for the setup file. "
112 | "If you use Setuptools, this will default to " +
113 | setuptools_source + "; if you use Distribute, this "
114 | "will default to " + distribute_source + "."))
115 | parser.add_option("--download-base", action="callback", dest="download_base",
116 | callback=normalize_to_url, nargs=1, type="string",
117 | help=("Specify a URL or directory for downloading "
118 | "zc.buildout and either Setuptools or Distribute. "
119 | "Defaults to PyPI."))
120 | parser.add_option("--eggs",
121 | help=("Specify a directory for storing eggs. Defaults to "
122 | "a temporary directory that is deleted when the "
123 | "bootstrap script completes."))
124 | parser.add_option("-t", "--accept-buildout-test-releases",
125 | dest='accept_buildout_test_releases',
126 | action="store_true", default=False,
127 | help=("Normally, if you do not specify a --version, the "
128 | "bootstrap script and buildout gets the newest "
129 | "*final* versions of zc.buildout and its recipes and "
130 | "extensions for you. If you use this flag, "
131 | "bootstrap and buildout will get the newest releases "
132 | "even if they are alphas or betas."))
133 | parser.add_option("-c", None, action="store", dest="config_file",
134 | help=("Specify the path to the buildout configuration "
135 | "file to be used."))
136 |
137 | options, args = parser.parse_args()
138 |
139 | # if -c was provided, we push it back into args for buildout's main function
140 | if options.config_file is not None:
141 | args += ['-c', options.config_file]
142 |
143 | if options.eggs:
144 | eggs_dir = os.path.abspath(os.path.expanduser(options.eggs))
145 | else:
146 | eggs_dir = tempfile.mkdtemp()
147 |
148 | if options.setup_source is None:
149 | if options.use_distribute:
150 | options.setup_source = distribute_source
151 | else:
152 | options.setup_source = setuptools_source
153 |
154 | if options.accept_buildout_test_releases:
155 | args.append('buildout:accept-buildout-test-releases=true')
156 | args.append('bootstrap')
157 |
158 | try:
159 | import pkg_resources
160 | import setuptools # A flag. Sometimes pkg_resources is installed alone.
161 | if not hasattr(pkg_resources, '_distribute'):
162 | raise ImportError
163 | except ImportError:
164 | ez_code = urllib2.urlopen(
165 | options.setup_source).read().replace('\r\n', '\n')
166 | ez = {}
167 | exec ez_code in ez
168 | setup_args = dict(to_dir=eggs_dir, download_delay=0)
169 | if options.download_base:
170 | setup_args['download_base'] = options.download_base
171 | if options.use_distribute:
172 | setup_args['no_fake'] = True
173 | ez['use_setuptools'](**setup_args)
174 | if 'pkg_resources' in sys.modules:
175 | reload(sys.modules['pkg_resources'])
176 | import pkg_resources
177 | # This does not (always?) update the default working set. We will
178 | # do it.
179 | for path in sys.path:
180 | if path not in pkg_resources.working_set.entries:
181 | pkg_resources.working_set.add_entry(path)
182 |
183 | cmd = [quote(sys.executable),
184 | '-c',
185 | quote('from setuptools.command.easy_install import main; main()'),
186 | '-mqNxd',
187 | quote(eggs_dir)]
188 |
189 | if not has_broken_dash_S:
190 | cmd.insert(1, '-S')
191 |
192 | find_links = options.download_base
193 | if not find_links:
194 | find_links = os.environ.get('bootstrap-testing-find-links')
195 | if find_links:
196 | cmd.extend(['-f', quote(find_links)])
197 |
198 | if options.use_distribute:
199 | setup_requirement = 'distribute'
200 | else:
201 | setup_requirement = 'setuptools'
202 | ws = pkg_resources.working_set
203 | setup_requirement_path = ws.find(
204 | pkg_resources.Requirement.parse(setup_requirement)).location
205 | env = dict(
206 | os.environ,
207 | PYTHONPATH=setup_requirement_path)
208 |
209 | requirement = 'zc.buildout'
210 | version = options.version
211 | if version is None and not options.accept_buildout_test_releases:
212 | # Figure out the most recent final version of zc.buildout.
213 | import setuptools.package_index
214 | _final_parts = '*final-', '*final'
215 |
216 | def _final_version(parsed_version):
217 | for part in parsed_version:
218 | if (part[:1] == '*') and (part not in _final_parts):
219 | return False
220 | return True
221 | index = setuptools.package_index.PackageIndex(
222 | search_path=[setup_requirement_path])
223 | if find_links:
224 | index.add_find_links((find_links,))
225 | req = pkg_resources.Requirement.parse(requirement)
226 | if index.obtain(req) is not None:
227 | best = []
228 | bestv = None
229 | for dist in index[req.project_name]:
230 | distv = dist.parsed_version
231 | if _final_version(distv):
232 | if bestv is None or distv > bestv:
233 | best = [dist]
234 | bestv = distv
235 | elif distv == bestv:
236 | best.append(dist)
237 | if best:
238 | best.sort()
239 | version = best[-1].version
240 | if version:
241 | requirement = '=='.join((requirement, version))
242 | cmd.append(requirement)
243 |
244 | if is_jython:
245 | import subprocess
246 | exitcode = subprocess.Popen(cmd, env=env).wait()
247 | else: # Windows prefers this, apparently; otherwise we would prefer subprocess
248 | exitcode = os.spawnle(*([os.P_WAIT, sys.executable] + cmd + [env]))
249 | if exitcode != 0:
250 | sys.stdout.flush()
251 | sys.stderr.flush()
252 | print ("An error occurred when trying to install zc.buildout. "
253 | "Look above this message for any errors that "
254 | "were output by easy_install.")
255 | sys.exit(exitcode)
256 |
257 | ws.add_entry(eggs_dir)
258 | ws.require(requirement)
259 | import zc.buildout.buildout
260 | zc.buildout.buildout.main(args)
261 | if not options.eggs: # clean up temporary egg directory
262 | shutil.rmtree(eggs_dir)
263 |
--------------------------------------------------------------------------------