#!/usr/local/bin/python
"Shows the INDEX in the ports tree."
# Author: Michel Talon
# Version 1.0: Release, 14/12/2006
# Version 1.1: Html improvements by Mark Ovens, 18/12/2006
# Version 1.2: Makes use of wsgi, and fcgi, capitalizing on the example
# http://hg.moinmo.in/moin/1.8/raw-file/tip/wiki/server/test.wsgi, 08/09/2008
# Licence: BSD (revised)
# Date: September 2008
# Uses wsgi and flup, so that it can do fastcgi as a backend for apache or
# lighttpd. Here is the required configuration for lighttpd: in lighttpd.conf
# one needs mod_rewrite and mod_fastcgi, and one needs:
# fastcgi.server = (
# ...
# "/showindex.fcgi" => (
# "main" => (
# "socket" => "/tmp/showindex.sock",
# "check-local" => "disable",
# )
# ),
# ...
# )
# url.rewrite-once = (
# ...
# "^(/showindex.*)$" => "/showindex.fcgi$1",
# ...
# )
# Finally point the browser at /showindex/ to trigger the fcgi server and get
# the title page of FreeBSD ports. Please first install the flup port and
# launch this script as root. Note that the presence of an INDEX in the ports
# tree is required.
import os, sys, re
######## Configuration ###############
minspare = 1 #
maxspare = 1 #
maxthreads = 5 #
socket = '/tmp/showindex.sock' #
pidfile = '/var/run/showindex.pid' #
wwwuid = 80 #
wwwgid = 80 #
######################################
# Some global variables
portdir = "/usr/ports/" # can be replaced at will
# Colours used for alternate lines to aid readability
colour_1 = '#FFFFFF'
colour_2 = '#EEEEEE'
if portdir.find('/', -1) == -1 :
print "portdir needs a trailing / "
sys.exit()
__version__ = "1.2"
port2pkg = {} # Stores the INDEX in memory
categories = {} # Stores categories and their descriptions.
collects = [] # Stores sorted list of collections
# Regular expression pattern matching URLs.
patmat = re.compile('http://[-_.@~/a-zA-Z0-9:\?=]*[~/a-zA-Z0-9]')
# Some utility functions.
def cat(origin) :
"Displays the collection to which a port belongs."
[coll, softw] = origin.split('/')
categ = '/' + coll # / is to please http protocol
return categ
def desc_tops():
"The directories under portdir."
subdirs = os.listdir(portdir)
pipe = os.popen("cd " + portdir + " && make -V SUBDIR")
subd = pipe.readline().split()
pipe.close()
for collect in subdirs:
if collect in subd: # This is a collection
for line in file(portdir + collect +
'/Makefile').readlines() :
if line.find('COMMENT') >= 0 :
categories[collect] = \
line.strip().replace('COMMENT = ','',1)
break # Found the comment for collection
# Builds the sorted list of collections, stored in collects.
desc_tops()
collects = categories.keys()
collects.sort()
def desc_coll(collect) :
'''The ports in a collection, lexicographically sorted, as they appear
in the dictionnary port2pkg.'''
# Should be run only after port2pkg filled.
ports = []
for origin in port2pkg.keys() :
[col, softw] = origin.split('/')
if col == collect :
ports.append(softw)
ports.sort()
return ports
# We build the html pages corresponding to title page, category page and port
# page, according to the templates found in /usr/ports/Templates. In these
# templates dynamic data appear in a form such as %%SUBDIR%%. Here will fill
# all these data programmatically.
# Builds the title page.
title_page = '''
The FreeBSD Ports Collection
The FreeBSD Ports Collection
You are at the top of the ports tree.
For information on how to use the ports tree, please look at "The
Ports Collection" section of the FreeBSD handbook on your own
FreeBSD machine or
at the FreeBSD web site.
Also, if you would like to contribute a new port or fix an existing
one, please refer to the
Porting Guidelines section of the most current handbook.
Here are the one-line descriptions for each of the directories:
'''
bg = colour_1
for collec in collects :
title_page += '''
' + collec \
+ ''' | ''' + categories[collec] + ''' |
'''
if bg == colour_1 :
bg = colour_2
else :
bg = colour_1
title_page += '''
'''
# End title page
# Builds collection page for a given collection "collec".
def collect_page(collect) :
"Fills the template for a collection."
coll_page = '''
The FreeBSD Ports Collection (''' + collect + ''')
The FreeBSD Ports Collection ("''' + collect + '''")
You are now in the directory "''' + collect + '''".
This is the one-line description for this category:
''' + categories[collect] + '''
Here are the one-line descriptions for each items in this
directory:
Go to parent directory
'''
bg = colour_1
for softw in desc_coll(collect) :
origin = collect + '/' + softw
coll_page += "" + port2pkg[origin][0]\
+ " | " + port2pkg[origin][2] + " |
" + '\n'
if bg == colour_1 :
bg = colour_2
else :
bg = colour_1
# This prints each package name and description in the collection
coll_page += '''
Go to parent directory
'''
return coll_page
# End collection page.
# Builds port page for a given port.
def port_page(origin) :
"Builds the page describing a given port."
# Some ports have empty websites, which produces confusing error message.
# Better produce a meaningful error message.
webSite = port2pkg[origin][8]
if webSite == '' :
webSite = "/showindex/nowebsite"
por_page = '''
The FreeBSD Ports Collection (''' + origin + ''')
The FreeBSD Ports Collection (''' + origin + ''')
You are now in the directory for the port "''' + origin + '''"
.
The package name of this port is "''' + port2pkg[origin][0] + '''"
.
This is the one-line description for this port:
''' + port2pkg[origin][2] + '''
Please read the "
description file" for a longer description, and/or visit the
"web site" for further informations.
If needed, you may contact the
maintainer of this
port or the port mailing-list.
This port requires package(s)
''' + port2pkg[origin][6] + '''
to build.
This port requires package(s)
''' + port2pkg[origin][7] + '''
to run.
Go to the top of the ports tree for
a summary on how to use the ports collection.
Go up one level | Go to top of ports tree
'''
return por_page
# End port page.
def print_desc(path) :
"Fetches and displays the long description file for a port."
try :
f_des = file(path, 'r')
except IOError :
return "No detailed description."
desc = ""
for line in f_des.readlines() : # htmlify to remove <>
line = line.replace('&','&') # order important
line = line.replace('>','>')
line = line.replace('<','<')
# make all included WWW references clickable
line = patmat.sub('\g<0>', line)
desc += line
return " " + desc + "
"
# Now we prepare reading the INDEX file and filling accordingly the dictionary
# port2pkg which contains all the required information.
def find_index() :
"Computes the name of Index file."
pipe = os.popen("uname -r")
uname = pipe.read()
pipe.close()
if int(uname[0]) <= 4 :
indexx = 'INDEX'
else :
indexx = 'INDEX-' + uname[0]
return indexx
# Open the INDEX file.
try :
index = open(portdir + find_index(),'r')
except IOError :
print "Cannot run without an INDEX file."
sys.exit()
# Fill port2pkg with INDEX information
for indline in index :
port = indline.strip().split('|')
orig = port[1].replace(portdir, '', 1) # 1 origin
port2pkg[orig] = [port[0], # 0 pkgname -> 0
port[2], # 2 prefix -> 1
port[3], # 3 comment -> 2
port[4], # 4 description -> 3
port[5], # 5 maintainer -> 4
port[6], # 6 categories -> 5
port[7], # 7 build_deps -> 6
port[8], # 8 run_deps -> 7
port[9], # 9 website -> 8
port[10], # 10 extract_deps -> 9
port[11], # 11 patch_deps -> 10
port[12] ] # 12 fetch_deps -> 11
# This is debugging stuff, to show the content of the dictionary without
# needing to go through the html engine.
def print_port(origin) :
"For use in debug_port."
entries = ['pkgname', 'origin', 'prefix', 'comment', 'description',
'maintainer', 'categories', 'build_deps', 'run_deps', 'website',
'extract_deps', 'patch_deps', 'fetch_deps']
print entries[0] + " => " + port2pkg[origin][0]
print entries[1] + " => " + origin
for ind in range(2, 13) :
print entries[ind] + " => " + port2pkg[origin][ind - 1]
def debug_port() :
"For direct examination of a port info."
print "Port to describe, e.g. devel/clanlib."
while 1 :
ans = raw_input('--> ')
if ans in ['exit', 'quit', 'q'] :
sys.exit()
try :
print_port(ans)
except KeyError :
print "Please use name like devel/clanlib, quit to exit."
# The main routine which handles http requests, parses them and sends back the
# appropriate html pages as computed above. Using WSGI, the environment and
# the function start_response are provided by the system. By hypothesis, all
# requests are GET, with the location in the ports tree of a port.
def request_handler(environ, start_response) :
'''This sends the response code and MIME headers, plus content.'''
# The request method and the path of the request are extracted from the
# environment dictionary, filled by the wsgi system. If path is incorrect,
# provide error codes, otherwise send the appropriate page as an iterator.
status_b = "404 NOT FOUND"
status_g = "200 OK"
mime_headers = [('Content-Type','text/html'),]
path = environ['PATH_INFO']
request_method = environ['REQUEST_METHOD']
if request_method == 'GET' :
# Clean the leading /showindex/
path = path.replace('/showindex/','',1)
path = path.rstrip('/')
path = '/' + path
pth = path.split('/')
if path == '/' or path == '//showindex' :
start_response(status_g,mime_headers)
return [title_page]
elif path == '/favicon.ico' :
start_response(status_b,mime_headers)
return []
elif path == '/nowebsite' :
start_response(status_b,mime_headers)
return ['No website
']
elif len(pth) == 2 and pth[1] in collects :
start_response(status_g,mime_headers)
return [collect_page(pth[1])]
elif len(pth) == 3 and pth[1] in collects :
origin = pth[1] + '/' + pth[2]
if origin in port2pkg.keys() :
start_response(status_g,mime_headers)
return [port_page(origin)]
else :
start_response(status_b,mime_headers)
return ['No such port
']
elif len(pth) == 6 : # Full path of a port description
start_response(status_g,mime_headers)
return [print_desc(path)]
else :
start_response(status_b,mime_headers)
return ['Non existing FreeBSD port or category.
']
else :
start_response(status_b,mime_headers)
return ['Only GET requests are supported.
']
# The available "Hello World!" documentation is incorrect,one needs to set
# options in the WSGIServer invocation. I have followed what Django does in
# its fastcgi server. This becomes a very big process so get only one copy,
# plus some threads.
try:
from flup.server.fcgi import WSGIServer
except ImportError:
print "Requires flup, see www/py-flup port."
wsgi_opts = {'maxSpare' : maxspare, 'minSpare' : minspare,
'maxThreads' : maxthreads, 'bindAddress': socket}
# Now become a daemon and make the socket accessible to the web browser
pid = os.fork()
if pid > 0 : sys.exit(0) # Exit original parent
# In first child become independant
os.chdir('/') # To not ober unmounts
os.umask(0)
os.setsid() # Become its own session leader
pid = os.fork()
if pid > 0 : # In first child
import time
time.sleep(3)
while not os.access(socket, os.F_OK) :
time.sleep(1)
# the socket created by the child is made accessible to the web server
os.chown(socket, wwwuid, wwwgid)
os.chmod(socket, 0700)
sys.exit(0) # Exit first child
# In second child close stdin, stdout, stderror
for i in range(3) : os.close(i)
# and redirect stdout, stderror to /dev/null
os.open('/dev/null', os.O_RDWR) # Becomes file descriptor 0
os.dup2(0,1)
os.dup2(0,2)
# Write the pid in '/var/run/showindex.pid' for use by rc-scripts
pid = os.getpid()
print pid
fh = open(pidfile,'w')
print >> fh, pid
fh.close()
os.chmod(pidfile,0444)
os.chown(pidfile,wwwuid,wwwgid)
# And finally the routine which starts the fcgi responder, as user www
os.seteuid(wwwuid)
WSGIServer(request_handler, **wsgi_opts).run()
# Cleaning ... this seems to work after a signal killing the script.
os.seteuid(0)
os.unlink(socket)
os.unlink(pidfile)