chiark / gitweb /
Last missing bit of Popen
[fdroidserver.git] / fdroidserver / stats.py
1 #!/usr/bin/env python2
2 # -*- coding: utf-8 -*-
3 #
4 # stats.py - part of the FDroid server tools
5 # Copyright (C) 2010-13, Ciaran Gultnieks, ciaran@ciarang.com
6 #
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as published by
9 # the Free Software Foundation, either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 # GNU Affero General Public License for more details.
16 #
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
19
20 import sys
21 import os
22 import re
23 import time
24 import traceback
25 import glob
26 from optparse import OptionParser
27 import paramiko
28 import socket
29
30 import common, metadata
31 from common import FDroidPopen
32
33 def carbon_send(key, value):
34     s = socket.socket()
35     s.connect((config['carbon_host'], config['carbon_port']))
36     msg = '%s %d %d\n' % (key, value, int(time.time()))
37     s.sendall(msg)
38     s.close()
39
40 options = None
41 config = None
42
43 def main():
44
45     global options, config
46
47     # Parse command line...
48     parser = OptionParser()
49     parser.add_option("-v", "--verbose", action="store_true", default=False,
50                       help="Spew out even more information than normal")
51     parser.add_option("-d", "--download", action="store_true", default=False,
52                       help="Download logs we don't have")
53     parser.add_option("--nologs", action="store_true", default=False,
54                       help="Don't do anything logs-related")
55     (options, args) = parser.parse_args()
56
57     config = common.read_config(options)
58
59     if not config['update_stats']:
60         print "Stats are disabled - check your configuration"
61         sys.exit(1)
62
63     # Get all metadata-defined apps...
64     metaapps = metadata.read_metadata(options.verbose)
65
66     statsdir = 'stats'
67     logsdir = os.path.join(statsdir, 'logs')
68     datadir = os.path.join(statsdir, 'data')
69     if not os.path.exists(statsdir):
70         os.mkdir(statsdir)
71     if not os.path.exists(logsdir):
72         os.mkdir(logsdir)
73     if not os.path.exists(datadir):
74         os.mkdir(datadir)
75
76     if options.download:
77         # Get any access logs we don't have...
78         ssh = None
79         ftp = None
80         try:
81             print 'Retrieving logs'
82             ssh = paramiko.SSHClient()
83             ssh.load_system_host_keys()
84             ssh.connect('f-droid.org', username='fdroid', timeout=10,
85                     key_filename=config['webserver_keyfile'])
86             ftp = ssh.open_sftp()
87             ftp.get_channel().settimeout(60)
88             print "...connected"
89
90             ftp.chdir('logs')
91             files = ftp.listdir()
92             for f in files:
93                 if f.startswith('access-') and f.endswith('.log.gz'):
94
95                     destpath = os.path.join(logsdir, f)
96                     destsize = ftp.stat(f).st_size
97                     if (not os.path.exists(destpath) or
98                             os.path.getsize(destpath) != destsize):
99                         print "...retrieving " + f
100                         ftp.get(f, destpath)
101         except Exception:
102             traceback.print_exc()
103             sys.exit(1)
104         finally:
105             #Disconnect
106             if ftp is not None:
107                 ftp.close()
108             if ssh is not None:
109                 ssh.close()
110
111     knownapks = common.KnownApks()
112     unknownapks = []
113
114     if not options.nologs:
115         # Process logs
116         if options.verbose:
117             print 'Processing logs...'
118         apps = {}
119         appsVer = {}
120         logexpr = '(?P<ip>[.:0-9a-fA-F]+) - - \[(?P<time>.*?)\] "GET (?P<uri>.*?) HTTP/1.\d" (?P<statuscode>\d+) \d+ "(?P<referral>.*?)" "(?P<useragent>.*?)"'
121         logsearch = re.compile(logexpr).search
122         for logfile in glob.glob(os.path.join(logsdir,'access-*.log.gz')):
123             if options.verbose:
124                 print '...' + logfile
125             p = FDroidPopen(["zcat", logfile])
126             matches = (logsearch(line) for line in p.stdout)
127             for match in matches:
128                 if match and match.group('statuscode') == '200':
129                     uri = match.group('uri')
130                     if not uri.endswith('.apk'):
131                         continue
132                     _, apkname = os.path.split(uri)
133                     app = knownapks.getapp(apkname)
134                     if app:
135                         appid, _ = app
136                         if appid in apps:
137                             apps[appid] += 1
138                         else:
139                             apps[appid] = 1
140                         # Strip the '.apk' from apkname
141                         appVer = apkname[:-4]
142                         if appVer in appsVer:
143                             appsVer[appVer] += 1
144                         else:
145                             appsVer[appVer] = 1
146                     else:
147                         if not apkname in unknownapks:
148                             unknownapks.append(apkname)
149
150         # Calculate and write stats for total downloads...
151         lst = []
152         alldownloads = 0
153         for app, count in apps.iteritems():
154             lst.append(app + " " + str(count))
155             if config['stats_to_carbon']:
156                 carbon_send('fdroid.download.' + app.replace('.', '_'), count)
157             alldownloads += count
158         lst.append("ALL " + str(alldownloads))
159         f = open('stats/total_downloads_app.txt', 'w')
160         f.write('# Total downloads by application, since October 2011\n')
161         for line in sorted(lst):
162             f.write(line + '\n')
163         f.close()
164
165         f = open('stats/total_downloads_app_version.txt', 'w')
166         f.write('# Total downloads by application and version, since October 2011\n')
167         lst = []
168         for appver, count in appsVer.iteritems():
169             lst.append(appver + " " + str(count))
170         for line in sorted(lst):
171             f.write(line + "\n")
172         f.close()
173
174     # Calculate and write stats for repo types...
175     if options.verbose:
176         print "Processing repo types..."
177     repotypes = {}
178     for app in metaapps:
179         if len(app['Repo Type']) == 0:
180             rtype = 'none'
181         else:
182             if app['Repo Type'] == 'srclib':
183                 rtype = common.getsrclibvcs(app['Repo'])
184             else:
185                 rtype = app['Repo Type']
186         if rtype in repotypes:
187             repotypes[rtype] += 1;
188         else:
189             repotypes[rtype] = 1
190     f = open('stats/repotypes.txt', 'w')
191     for rtype, count in repotypes.iteritems():
192         f.write(rtype + ' ' + str(count) + '\n')
193     f.close()
194
195     # Calculate and write stats for update check modes...
196     if options.verbose:
197         print "Processing update check modes..."
198     ucms = {}
199     for app in metaapps:
200         checkmode = app['Update Check Mode'].split('/')[0]
201         if checkmode in ucms:
202             ucms[checkmode] += 1;
203         else:
204             ucms[checkmode] = 1
205     f = open('stats/update_check_modes.txt', 'w')
206     for checkmode, count in ucms.iteritems():
207         f.write(checkmode + ' ' + str(count) + '\n')
208     f.close()
209
210     if options.verbose:
211         print "Processing categories..."
212     ctgs = {}
213     for app in metaapps:
214         if app['Categories'] is None:
215             continue
216         categories = [c.strip() for c in app['Categories'].split(',')]
217         for category in categories:
218             if category in ctgs:
219                 ctgs[category] += 1;
220             else:
221                 ctgs[category] = 1
222     f = open('stats/categories.txt', 'w')
223     for category, count in ctgs.iteritems():
224         f.write(category + ' ' + str(count) + '\n')
225     f.close()
226
227     if options.verbose:
228         print "Processing antifeatures..."
229     afs = {}
230     for app in metaapps:
231         if app['AntiFeatures'] is None:
232             continue
233         antifeatures = [a.strip() for a in app['AntiFeatures'].split(',')]
234         for antifeature in antifeatures:
235             if antifeature in afs:
236                 afs[antifeature] += 1;
237             else:
238                 afs[antifeature] = 1
239     f = open('stats/antifeatures.txt', 'w')
240     for antifeature, count in afs.iteritems():
241         f.write(antifeature + ' ' + str(count) + '\n')
242     f.close()
243
244     # Calculate and write stats for licenses...
245     if options.verbose:
246         print "Processing licenses..."
247     licenses = {}
248     for app in metaapps:
249         license = app['License']
250         if license in licenses:
251             licenses[license] += 1;
252         else:
253             licenses[license] = 1
254     f = open('stats/licenses.txt', 'w')
255     for license, count in licenses.iteritems():
256         f.write(license + ' ' + str(count) + '\n')
257     f.close()
258
259     # Write list of latest apps added to the repo...
260     if options.verbose:
261         print "Processing latest apps..."
262     latest = knownapks.getlatest(10)
263     f = open('stats/latestapps.txt', 'w')
264     for app in latest:
265         f.write(app + '\n')
266     f.close()
267
268     if unknownapks:
269         print '\nUnknown apks:'
270         for apk in unknownapks:
271             print apk
272
273     print "Finished."
274
275 if __name__ == "__main__":
276     main()
277