#!/usr/bin/env python

# Author: alain@onesite.org
# License: GPL
# OS: GNU/Linux
# Version: 0.0.5

# Required unusual external programs:
# strace (package strace)
# killall (package psmisc)
# stat (package stat)

# Configuration defaults
# Don't change this file, to define new defaults values create a file /etc/makejail

class configClass:
	def __init__(self):
		self.chroot=None
		self.packages=[]
		self.useDepends=0
		self.blockDepends=[]
		self.doNotCopy=["/usr/share/doc",
						"/usr/share/info",
						"/usr/share/man",
						"/etc/fstab",
						"/etc/mtab"]
		self.forceCopy=[]
		self.cleanJailFirst=0
		self.preserve=[]
		self.testCommandsInsideJail=[]
		self.processNames=[]
		self.testCommandsOutsideJail=[]
		self.maxExecutions=100
		self.sleepAfterStartCommand=2
		self.sleepAfterTest=2
		self.sleepAfterKillall=1
		self.sleepAfterStraceAttachPid=0.2
		self.maxRemove=500
   		self.keepStraceOutputs=0
		self.promptForInteractiveTests=0
		self.promptForSomeMoreTests=0
		self.users=[]
		self.groups=[]

		self.debianDpkgInfoFile="/var/lib/dpkg/info/%s.list"
		self.etcFile="/etc/makejail"
		self.pathToLdConfig="/sbin/ldconfig"
		self.pathToLdSoConf="/etc/ld.so.conf"
		self.pathToLdSoCache="/etc/ld.so.cache"
		self.procPath="/proc"
		self.userFiles=["/etc/passwd",
						"/etc/shadow",
						"/etc/master.passwd"]
		self.groupFiles=["/etc/group",
						 "/etc/gshadow"]
		self.tempDir="/tmp/makejail_logs"

		self.psCommand="ps -e"
		self.psColumns=[1,4]

		# -e file=trace doesn't catch socket connections
		self.straceCommand="strace -f %command >/dev/null 2>>%file"
		self.straceCommandPid="strace -f -p %pid >/dev/null 2>>%file"
		self.straceCommandStop="killall -9 strace"
		self.straceCommandView=None
		self.stracePatterns=['.*\("([^"]*)",.*\) .*= -[0-9]* ENO.*',
							 '.*\("([^"]*)",.*\) .*= -[0-9]* EACCES.*']
		self.straceCreatePatterns=['.*\("([^"]*)",.*O_CREAT.*\) .* ENOENT .*',
								   'bind\(.* path="([^"]*)".* ENOENT .*']
		self.straceSocketPatterns=['connect\(.* path="([^"]*)".* ENOENT .*']

# Global variables

needLdCache=0
warnings=[]
procMounted=0
compiledRegExps={}
installedPackages=[]
installedFiles={}
indentLevel=0
doNotKillPids=[]
tmpOut=None

import stat
import sys
import imp
import string
import os
import shutil
import types
import popen2
import tempfile
import re
import glob
import time
import select
import fcntl

# May be useful to parse correctly some program outputs
os.environ["LANG"]="C"

def moveIndent(direction):
	global indentLevel
	indentLevel=indentLevel+direction

def debug(s,endLine=1):
	sys.stdout.write("   "*indentLevel+s+"\n"*endLine)

def abort(s):
	sys.stderr.write("\nERROR: %s\n" % s)
	sys.exit(1)

def compileRegExp(pattern):
	global compiledRegExps
	if compiledRegExps.has_key(pattern):
		compiled=compiledRegExps[pattern]
	else:
		compiled=re.compile(pattern)
		compiledRegExps[pattern]=compiled
	return compiled

def matchPattern(pattern,line,nextLine=None):
	# If pattern is an array of two strings, matches the first pattern
	# only if the next line matches the second pattern
	if pattern==None:
		return None
	elif type(pattern)==types.StringType:
		matchobject=compileRegExp(pattern).match(line)
		if matchobject:
			return matchobject.groups()[0]
	elif nextLine:
		(currentPattern,nextPattern)=pattern
		matchobject=compileRegExp(currentPattern).match(line)
		if matchobject and compileRegExp(nextPattern).match(nextLine):
			return matchobject.groups()[0]

def matchPatterns(patterns,line,nextLine=None):
	if patterns==None:
		return None
	for pattern in patterns:
		match=matchPattern(pattern,line,nextLine)
		if match:
			return match

def startslike(s,pattern):
	if s==pattern:
		return 1
	if len(s)<len(pattern):
		return 0
	elif s[:len(pattern)]==pattern:
		return 1
	else:
		return 0

def cleanDir(queue,dir):
	# test inode for hardlink
	debug("Entering directory %s" % dir)
	moveIndent(1)
	keepUpperDir=0
	files=os.listdir(dir)
	for file in files:
		file="%s/%s" % (dir,file)
		doNotTouchThat=0
		for path in config.preserve:
			if startslike(file,"%s%s"% (config.chroot,path)):
				doNotTouchThat=1
				break
		if doNotTouchThat:
			debug("Preserving %s" % file)
			keepUpperDir=1
			continue
		keepThisDir=0
		if os.path.isdir(file) and not(os.path.islink(file)) and file!=config.chroot:
			if file=="%s%s" % (config.chroot,config.procPath):
				debug("%s : mount point, do not try removing files deeper" % config.procPath)
			else:
				if cleanDir(queue,file):
					keepThisDir=1
					keepUpperDir=1
		if keepThisDir:
			if file!="%s%s" % (config.chroot,config.procPath):
				debug("   Not queueing for remove %s, some paths to preserve below" % file)
		else:
			debug("   Queuing for remove %s" % file)
			queue.append(file)
	moveIndent(-1)
	return keepUpperDir

def cleanJail():
	queue=[]
	debug("Cleaning jail")
	moveIndent(1)
	cleanDir(queue,config.chroot)
	moveIndent(-1)
	if len(queue)>config.maxRemove:
		abort("Found %i files to remove, the maximum it %i" % (len(queue),config.maxRemove))
	debug("Removing %i files in queue" % len(queue))
	for file in queue:
		debug("   Removing %s" % file)
		try:
			if os.path.isdir(file) and not(os.path.islink(file)):
				os.rmdir(file)
			else:
				os.unlink(file)
		except OSError:
			if file=="%s%s" % (config.chroot,config.procPath):
				debug("      Cannot remove %s, it's probably mounted" % config.procPath)
			else:
				abort("Cannot remove the path %s" % file)

def readFileLines(fileName):
	try:
		f=open(fileName,"r")
	except IOError:
		abort("Cannot read file '%s'" % fileName)
	lines=f.readlines()
	f.close()
	return map(string.strip,lines)

def dpkgInfoFiles(package):
	return readFileLines(config.debianDpkgInfoFile % package)

def copyStat(source,target):
	statInfos=os.stat(source)
	os.chmod(target,statInfos[stat.ST_MODE])
	os.chown(target,statInfos[stat.ST_UID],statInfos[stat.ST_GID])

def mountProc():
	debug("Mounting %s" % config.procPath)
	moveIndent(1)
	returnCode=os.system("mount -t proc proc %s%s" % (config.chroot,config.procPath))
	if returnCode==0:
		debug("%s mounted successfully" % config.procPath)
		warnings.append(("/proc",None))
	else:
		abort("Unable to mount %s" % config.procPath)
	moveIndent(-1)

def addPasswdFile(file,what):
	moveIndent(1)
	if what=="users":
		entries=config.users
	elif what=="groups":
		entries=config.groups
	else:
		raise ValueError
	dest="%s%s" % (config.chroot,file)
	debug("Copying with filtering on %s : %s -> %s" % (what,file,dest))
	if "*" in entries:
		debug('Entry "*" in %s, the file is simply copied' % what)
		shutil.copy(file,dest)
	else:
		f=open(file,"r")
		lines=f.readlines()
		f.close()
		d=open(dest,"w")
		matches=[]
		for line in lines:
			if not(":" in line):
				continue
			entry=string.split(line,":")[0]
			if entry in entries:
				d.write(line)
				matches.append(entry)
		d.close()
		if len(matches)>1:
			entryString="entries"
		else:
			entryString="entry"
		debug("%i %s copied : %s" % (len(matches),entryString,string.join(matches,",")))
	moveIndent(-1)

def getMajorMinor(fileName):
	# Pure python functions should be available in 2.3
	try:
		return map(lambda h: int(h,16),execute('stat -t -c "%%t %%T" %s' % fileName)[0].split())
	except:
		abort("Cannot get the major and minor codes for the file %s" % fileName)

def fileIsNewer(fileName1,fileName2):
	return os.stat(fileName1)[stat.ST_MTIME]>os.stat(fileName2)[stat.ST_MTIME]

def addFileToJail(fileName):
	global needLdCache,procMounted
	if not(fileName):
		return []
	missingFiles=[]
	if installedFiles.has_key(fileName):
		return []
	debug("Checking path '%s'" % fileName)
	if fileName==config.pathToLdSoCache:
		installedFiles[fileName]=-1
		debug("   Shared libs cache file %s marked as needed, it will be generated at the end" % config.pathToLdSoCache)
		needLdCache=1
		return []
	if config.doNotCopy:
		ignored=0
		for ignorePath in config.doNotCopy:
			if startslike(fileName,ignorePath):
				ignored=1
				break
		if ignored:
			debug("   Ignoring because of doNotCopy directive: %s" % fileName)
			installedFiles[fileName]=0
			return []
	try:
		statMode=os.stat(fileName)[stat.ST_MODE]
	except OSError:
		debug("   The path '%s' doesn't exist" % fileName)
		return []
	installedFiles[fileName]=1
	if fileName[0]!="/":
		abort("The path '%s' is not absolute" % fileName)
	elif fileName=="/.":
		return []
	if startslike(fileName,config.procPath):
		if procMounted:
			debug("   %s has just been mounted" % config.procPath)
		else:
			os.mkdir("%s%s" % (config.chroot,config.procPath))
			os.chmod("%s%s" % (config.chroot,config.procPath),0555)
			mountProc()
			procMounted=1
		return []
 	targetDirs=string.split(fileName[1:],"/")[:-1]
	checkDir=""
	for targetDir in targetDirs:
		checkDir=checkDir+"/%s" % targetDir
		fileInChroot="%s%s" % (config.chroot,checkDir)
		if not(os.path.isdir(fileInChroot)):
			debug("   Dir '%s' missing" % fileInChroot)
			moveIndent(1)
			addFileToJail(checkDir)
			moveIndent(-1)
	fileInChroot="%s%s" % (config.chroot,fileName)
	if os.path.exists(fileInChroot):
		if fileIsNewer(fileName,fileInChroot):
			debug("   File %s is newer than the %s, overwriting" % (fileName,fileInChroot))
		else:
			debug("   File %s already exists" % fileInChroot)
			return []
	elif os.path.islink(fileName):
		linkTarget=os.readlink(fileName)
		debug("   '%s' is a symlink to '%s'" % (fileName,linkTarget))
		moveIndent(1)
		fileDir=os.path.split(fileName)[0]
		if linkTarget[-1]=="/":
			linkTarget=linkTarget[:-1]
		if linkTarget[0]=="/":
			absoluteLinkTarget=linkTarget
		else:
			absoluteLinkTarget="%s/%s" % (fileDir,linkTarget)
		newFiles=addFileToJail(absoluteLinkTarget)
		missingFiles=missingFiles+newFiles
		newWorkingDir="%s/%s" % (config.chroot,fileDir[1:])
		os.chdir(newWorkingDir)
		debug("   Creating '%s' as a symlink to '%s' (pwd=%s)" % (fileName[1:],linkTarget,newWorkingDir))
		os.symlink(linkTarget,os.path.split(fileName)[1])
		os.chdir("/")
		missingFiles.append(fileName)
		moveIndent(-1)
	elif stat.S_ISSOCK(statMode):
		debug("Failed to connect to socket %s, file exists" % fileName)
		installedFiles[fileName]=-1
		warnings.append(("socket",(fileName,"exists")))
		return [fileName]
	elif os.path.isdir(fileName):
		if os.path.isdir(fileInChroot):
			debug("   Dir %s already exists" % fileInChroot)
			return []
		debug("   Making dir %s" % fileInChroot)
		os.mkdir(fileInChroot)
		missingFiles.append(fileName)
	elif stat.S_ISCHR(statMode) or stat.S_ISBLK(statMode):
		if stat.S_ISCHR(statMode):
			label="character"
			deviceType="c"
		else:
			label="block"
			deviceType="b"
		(major,minor)=getMajorMinor(fileName)
		debug("   Creating %s device %s (major=%i,minor=%i)" % (label,
																fileName,
																major,
																minor))
		os.system("mknod %s %s %i %i" % (fileInChroot,
										 deviceType,
										 major,
										 minor))
		missingFiles.append(fileName)
	elif stat.S_ISBLK(statMode):
		debug("   Creating block device : %s -> %s" % (fileName,fileInChroot))
		os.system("cp -a %s %s" % (fileName,fileInChroot))
		missingFiles.append(fileName)
	elif fileName in config.userFiles:
		missingFiles.append(fileName)
		addPasswdFile(fileName,"users")
	elif fileName in config.groupFiles:
		missingFiles.append(fileName)
		addPasswdFile(fileName,"groups")
	else:
		debug("   Copying %s -> %s" % (fileName,fileInChroot))
		shutil.copyfile(fileName,fileInChroot)
		missingFiles.append(fileName)
	copyStat(fileName,fileInChroot)
	if os.path.isfile(fileName):
		checkRequirements(fileName)
	return missingFiles

def fileReadlines(fileName):
	f=open(fileName,"r")
	lines=f.readlines()
	f.close()
	return lines

def makeNonBlocking(fd):
	fl = fcntl.fcntl(fd, fcntl.F_GETFL)
	try:
		fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NDELAY)
	except AttributeError:
		import FCNTL
		fcntl.fcntl(fd, fcntl.F_SETFL, fl | FCNTL.FNDELAY)

def execute(command):
	debug("  Executing : %s" % command)
	child=popen2.Popen3(command,1)
	child.tochild.close()
	files=(child.fromchild,child.childerr)
	fds=[files[0].fileno(),files[1].fileno()]
	for fd in fds:
		makeNonBlocking(fd)
	datas=[[],[]]
	feedbackStreams=(sys.stdout,sys.stderr)
	feedbackQueues=["",""]
	finished=[0,0]
	while 1:
		ready=select.select(fds,[],[])
		for i in (0,1):
			if fds[i] in ready[0]:
				chunk=files[i].read()
				if chunk=="":
					finished[i]=1
				feedbackQueues[i]+=chunk
		for i in (0,1):
			while "\n" in feedbackQueues[i]:
				pos=feedbackQueues[i].find("\n")
				line=feedbackQueues[i][:pos+1]
				datas[i].append(line[:-1])
				feedbackQueues[i]=feedbackQueues[i][pos+1:]
		if finished==[1,1]:
			break
		select.select([],[],[],.05)
	errCode=child.wait()
	if errCode:
		debug("  WARNING: exit code %i" % errCode)
	return datas[0]

def commandPids(processNames):
	pids=[]
	for psLine in execute(config.psCommand)[1:]:
		processCommandLine=string.split(psLine)[config.psColumns[1]-1]
		name=string.split(string.split(processCommandLine)[0],"/")[-1]
		if name in processNames:
			pid=int(string.split(psLine)[config.psColumns[0]-1])
			if not(pid in doNotKillPids):
				pids.append(int(pid))
	return pids

def fileType(file):
	return string.strip(string.split(execute("file %s" % file)[0],":")[1])

def checkRequirements(file):
	moveIndent(1)
	ft=fileType(file)
	if string.find(ft,"script")!=-1:
		f=open(file,"r")
		head=f.readline()
		f.close()
		if head[:2]=="#!":
			script=string.split(head[2:])[0]
			debug("%s is a script run with the interpreter %s" % (file,script))
			addFileToJail(script)
	else:
		sharedStrings=("shared object","dynamically linked")
		shared=None
		for sharedString in sharedStrings:
			if string.find(ft,sharedString)!=-1:
				shared=sharedString
				break
		if shared:
			moveIndent(1)
			debug("%s, checking the required libraries with ldd" % shared)
			ldd_lines=map(string.strip,execute("ldd %s" % file))
			ignoreStrings=["not a dynamic executable",
						   "statically linked"]
			for ignoreString in ignoreStrings:
				if ldd_lines[0]==ignoreString:
					debug(ignoreString)
					moveIndent(-2)
					return
			for line in ldd_lines:
				if not(line):
					continue
				if string.find(line,"=>")==-1:
					continue
				lib=string.strip(string.split(string.split(line,"=>")[1],"(")[0])
				addFileToJail(lib)
			moveIndent(-1)
	moveIndent(-1)

def addPackageToJail(package):
	global installedPackages

	if (package in installedPackages):
		return
	installedPackages.append(package)

	if (package in config.blockDepends):
		debug("The package %s is in Depends but won't be installed")
		return

	debug("Installing the package %s" % package)
	moveIndent(1)

	debug("Copying the files from the dpkg information")
	moveIndent(1)

	files=dpkgInfoFiles(package)
	for file in files:
		addFileToJail(file)

	moveIndent(-1)

	if config.useDepends:
		debug("Checking Depends")
		moveIndent(1)
		depends=debianGetDepends(package)
		for depend in depends:
			addPackageToJail(depend)
		moveIndent(-1)

	moveIndent(-1)

def addMissingFilesFromStraceLines(lines):
	missingFiles=[]
	for n in range(len(lines)):
		line=lines[n]
		if n==len(lines)-1:
			nextLine=None
		else:
			nextLine=lines[n+1]
		line=string.strip(line)

		missingFile=matchPatterns(config.stracePatterns,line,nextLine)
		if missingFile and not (missingFile in missingFiles):
			if missingFile==config.chroot:
				continue
			if string.split(missingFile,"/")[-1]=="chroot":
				continue
			try:
				statInfos=os.stat(missingFile)
				fileExists=1
			except OSError:
				fileExists=0
			if fileExists:
				moveIndent(1)
				if addFileToJail(missingFile):
					missingFiles.append(missingFile)
				moveIndent(-1)
				continue

		missingFile=matchPatterns(config.straceCreatePatterns,line,nextLine)
		if missingFile and not (missingFile in missingFiles):
			debug("Failed attempt at creating the file %s" % missingFile)
			missingFile=removeTrailingSlashes(missingFile)
			dir=string.join(string.split(missingFile,"/")[:-1],"/")
			if os.path.isdir(dir) and not(os.path.isdir(config.chroot+dir)):
				moveIndent(1)
				if addFileToJail(dir):
					missingFiles.append(dir)
				moveIndent(-1)
				continue

		missingFile=matchPatterns(config.straceSocketPatterns,line,nextLine)
		if missingFile and not(installedFiles.has_key(missingFile)):
			try:
				statMode=os.stat(missingFile)[stat.ST_MODE]
			except:
				exists="doesn't exist"
			else:
				if stat.S_ISSOCK(statMode):
					exists="exists"
				else:
					exists="exists but is not a socket ??"
			debug("Failed to connect to socket %s, file %s" % (missingFile,exists))
			installedFiles[missingFile]=-1
			warnings.append(("socket",(missingFile,exists)))

	return missingFiles

def removeTrailingSlashes(s):
	while s[-1]=="/":
		s=s[:-1]
	return s

def sleep(delay):
	if delay:
		if delay>=2.0:
			s="s"
		else:
			s=""
		debug("Sleeping for %.2f second%s" % (delay,s))
		time.sleep(delay)

def addMissingFilesFromProcess(items,testCommandsOutsideJail=[]):

	straceTempFileName=tempfile.mktemp("trace")
	straceTempFile=open(straceTempFileName,"w")
	straceTempFile.close()

	if not(type(items) in (types.ListType,types.TupleType)):
		items=[items]

	for item in items:
		if type(item) in (types.IntType,types.LongType):
			command=config.straceCommandPid
			command=string.replace(command,"%pid",str(item))
			s="Tracing process %i" % item
		elif type(item)==types.StringType:
			command=config.straceCommand
			command=string.replace(command,"%command",item)
			s="Tracing command %s" % item
		else:
			raise ValueError
		command=string.replace(command,"%file",straceTempFileName)
		debug(s)
		os.system("%s &" % command)
		if type(item)==types.StringType:
			sleep(config.sleepAfterStartCommand)
		else:
			sleep(config.sleepAfterStraceAttachPid)

	if testCommandsOutsideJail:
		if not(type(testCommandsOutsideJail in (types.ListType,types.TupleType))):
			testCommandsOutsideJail=[testCommandsOutsideJail]
		for testCommand in testCommandsOutsideJail:
			if testCommand=="interactive":
				raw_input("Interactive tests: press Enter when complete")
			else:
				debug("Executing test command '%s' ... " % testCommand)
				returnCode=os.system("%s >/dev/null 2>/dev/null" % testCommand)
				debug("   return code is %i" % returnCode)
				sleep(config.sleepAfterTest)

	debug("Stopping tracing ... ",endLine=0)
	lines=map(string.strip,execute(config.straceCommandStop))
	out=[]
	for line in lines:
		if line:
			out.append(line)
	debug(string.join(out," "))

	if config.straceCommandView:
		command=string.replace(config.straceCommandView,"%file",straceTempFileName)
		lines=execute(command)
	else:
		straceTempFile=open(straceTempFileName,"r")
		lines=straceTempFile.readlines()
		straceTempFile.close()

	if config.keepStraceOutputs:
		whatWillHappenToThisPoorTraceFile=" (available in %s)" % straceTempFileName
	else:
		whatWillHappenToThisPoorTraceFile=""
		os.unlink(straceTempFileName)

	debug("Looking for missing files in the trace file%s" % whatWillHappenToThisPoorTraceFile)
	missingFiles=addMissingFilesFromStraceLines(lines)

	return missingFiles

def killall(processNames):
	if type(processNames)==types.StringType:
		processNames=[processNames]
	if len(processNames)==1:
		s="processes named '%s'" % processNames[0]
	else:
		s="processes matching %s" % str(processNames)
	if processNames:
		debug("Killing %s ... : " % s,endLine=0)
		pids=commandPids(processNames)
		if len(pids):
			debug(string.join(map(str,pids),","))
			for pid in pids:
				out=execute("kill -9 %i 2>&1" % pid)
			sleep(config.sleepAfterKillall)
		else:
			debug("no process found")

def debianIsPackageInstalled(package):
	dpkgOut=execute("dpkg -l %s" %package)
	if not(dpkgOut):
		return 0
	lastLine=dpkgOut[-1]
	return (lastLine[0]=="i")

def debianGetDepends(package):
	depends=[]
	for line in execute("dpkg -p %s" % package):
		if startslike(line,"Depends: "):
			packs=string.split(string.replace(line[8:],"|",","),",")
			for pack in packs:
				pack=string.split(string.strip(pack)," ")[0]
				if debianIsPackageInstalled(pack):
					depends.append(pack)
	return depends

def tryExecute(commands):
	if type(commands)==types.StringType:
		commands=[commands]
	for command in commands:
		debug("Executing command '%s' (pwd=%s)" % (command,config.tempDir),endLine=0)
		os.chdir(config.tempDir)
		returnCode=os.system("%s >/dev/null 2>/dev/null &" % command)
		debug("   return code is %i " % returnCode)

def checkConfig():
	if os.geteuid()!=0:
		abort("Effective user it is not 0, this command must be run as root")
	if not(config.chroot):
		abort("You didn't defined the variable chroot")
		config.chroot(removeTrailingSlashes(config.chroot))
	if config.chroot[0]!="/":
		abort("The variable chroot must be an absolute path")
	if not(config.testCommandsInsideJail) and ("interactive" in config.testCommandsOutsideJail):
		abort("Cannot have 'interactive' in testCommandsOutsideJail if testCommandsInsideJail is not defined")

def runTests(tests,chrootCommands=None):
	finished=0
	i=1
	moveIndent(1)
	while not(finished):
		debug("Execution #%i" % i)
		moveIndent(1)
		if chrootCommands:
			sleep(config.sleepAfterStartCommand)
			pids=commandPids(config.processNames)
			if pids:
				missingFiles=addMissingFilesFromProcess(pids,tests)
			else:
				moveIndent(-1)
				return 0
		else:
			missingFiles=addMissingFilesFromProcess(tests)
		if not(missingFiles):
			debug("No missing file found")
			finished=1
		elif i>config.maxExecutions:
			debug("Still missing files after %i tries" % config.maxExecutions)
			finished=1
		i=i+1
		if chrootCommands:
			killall(config.processNames)
			tryExecute(chrootCommands)
		moveIndent(-1)
	killall(config.processNames)
	moveIndent(-1)
	return 1

def displayWarnings():
	for warning in warnings:
		(warningType,warningDetail)=warning
		if warningType=="/proc":
			s=["You'll need the filesystem procfs mounted as %s%s" % (config.chroot,config.procPath),
			   "It's mounted now, you can mount it again for example before starting the daemon with :",
			   "mount -t proc proc %s%s" % (config.chroot,config.procPath)]
		elif warningType=="socket":
			(socketFile,exists)=warningDetail
			if socketFile=="/dev/log":
				s=["Attempt to access /dev/log, a socket used by syslogd. Some suggestions:",
				   "- if your version of syslogd supports it you can tell it to listen to the",
				   "  additional socket %s/dev/log, and put it in the configuration directive 'preserve'" % config.chroot,
				   "  maybe start syslog with the option -a %s/dev/log" % config.chroot,
				   "- use a syslog proxy like holelogd",
				   "- configure the daemon to log into files instead through syslog"]
			else:
				s=["Attempt to access the socket file %s, which %s outside the jail" % (socketFile,exists),
				   "if needed it must be created inside the jail as %s%s""" % (config.chroot,socketFile)]
				s=s+["If you create the socket, put it in the configuration option 'preserve'",
					 "so it won't be deleted when you launch this script again"]
				if exists!="exists":
					s=s+["As this socket doesn't exist outside the jail, you can probably ignore this warning safely."]
		else:
			abort("No method to display the warning '%s'" % warningType)
		sys.stdout.write("\nWARNING:\n%s\n" % string.join(map(lambda l:"   %s" % l,s),"\n"))

def initRunningPids():
	debug("Initializing list of running processes")
	for psLine in execute(config.psCommand)[1:]:
		pid=int(string.split(psLine)[config.psColumns[0]-1])
		doNotKillPids.append(pid)

def makeChroot():
	global tmpOut

	debug("Chroot directory is %s" % config.chroot)

	# In the strace output, the command attempt to access
	# the directory where this script was started outside
	# the jail
	os.chdir("/")

	# See what processes are running so they don't get killed later
	initRunningPids()

	# Create temp strace dir
	if not(os.path.isdir(config.tempDir)):
		debug("Creating temp dir %s" % config.tempDir)
		os.mkdir(config.tempDir)
	tempfile.tempdir=config.tempDir

	tmpOut=tempfile.mktemp("out")

	killall(config.processNames)
	if config.cleanJailFirst:
		cleanJail()

	# === Add packages
	for package in config.packages:
		addPackageToJail(package)

	# === Copy specific paths
	for globExpression in config.forceCopy:
		debug("Adding files matching '%s'" % globExpression)
		moveIndent(1)
		for file in glob.glob(globExpression):
			addFileToJail(file)
		moveIndent(-1)

	chrootCommands=[]
	if config.testCommandsInsideJail:
		for command in config.testCommandsInsideJail:
			chrootCommands.append("chroot %s %s" % (config.chroot,command))

	# === Execute main command until there is no missing file
	if chrootCommands:
		finished=0
		i=1
		debug("Running strace on commands from inside jail")
		moveIndent(1)
		killall(config.processNames)
		while not(finished):
			debug("Execution #%i" % i)
			moveIndent(1)
			missingFiles=addMissingFilesFromProcess(chrootCommands)
			i=i+1
			if not(missingFiles):
				debug("No missing file found")
				finished=1
			if i>config.maxExecutions:
				debug("Still missing files after %i tries" % config.maxExecutions)
				finished=1
			killall(config.processNames)
			moveIndent(-1)
		moveIndent(-1)

	# === Try it
	if chrootCommands:
		tryExecute(chrootCommands)

	# === Make tests
	continueTests=1
	while continueTests:
		if config.testCommandsOutsideJail:
			debug("Running tests from outside the jail")
			if not(runTests(config.testCommandsOutsideJail,chrootCommands)):
				debug("No running process found, cannot run tests")
		if config.promptForInteractiveTests:
			debug("Running interactive tests from outside the jail")
			if chrootCommands:
				tryExecute(chrootCommands)
			if not(runTests(["interactive"],chrootCommands)):
				debug("No running process found, cannot run tests")
		continueTests=0
		if config.promptForSomeMoreTests:
			prompt="Paused to give you a chance to fix some problems, do you want to run tests again (y/n) ? "
			while prompt:
				yesNo=string.lower(raw_input(prompt))
				if yesNo=="y":
					continueTests=1
					prompt=None
				elif yesNo=="n":
					continueTests=0
					prompt=None
				else:
					prompt="Please reply with 'y' or 'n': "
		if chrootCommands:
			tryExecute(chrootCommands)

	sleep(config.sleepAfterStartCommand)
	killall(config.processNames)

	# /etc/ld.so.cache
	if needLdCache:
		debug("Generating %s" % config.pathToLdSoCache)
		moveIndent(1)
		# /etc/ld.so.conf may contains path which are invalid in chroot, ldconfig ignores them
		addFileToJail(config.pathToLdSoConf)
		newLdConfig="%s%s" % (config.chroot,config.pathToLdConfig)
		ldconfigAlreadyHere=os.path.isfile(newLdConfig)
		newFiles=addFileToJail(config.pathToLdConfig)
		tryExecute("chroot %s %s" % (config.chroot,config.pathToLdConfig))
		debug("Removing ldconfig from jail")
		moveIndent(1)
		newFiles.reverse()
		for file in newFiles:
			file="%s%s" % (config.chroot,file)
			if os.path.islink(file) or os.path.isfile(file):
				debug("Removing file %s" % file)
				os.unlink(file)
			elif os.path.isdir(file):
				debug("Removing dir %s" % file)
				os.rmdir(file)
			else:
				raise ValueError
		moveIndent(-2)

	displayWarnings()

def loadConfig(file):

	debug("Loading configuration file %s" % file)

	moveIndent(1)
	try:
		assert os.path.isfile(file)
		fp=open(file,"r")
	except:
		abort("Cannot open configuration file '%s'" % file)

	if ("/" in configFile):
		dir=string.join(string.split(file,"/")[:-1],"/")
		if dir[0]!="/":
			dir="%s/%s" % (os.getcwd,dir)
		sys.path.insert(0,dir)

	try:
		newConfig=imp.load_module("newConfig%s" % file,fp,file,("","r",1))
	except:
		if fp:
			fp.close()
		sys.stderr.write("Cannot load configuration file '%s' as a python module\n" % file)
		sys.stderr.write("Executing it with python which should display a syntax error:\n")
		os.system("python %s" % file)
		sys.exit(1)

	# Remove byte-compile file
	bcFile="%sc" % file
	if os.path.isfile(bcFile):
		os.unlink(bcFile)

	if fp:
		fp.close()

	for key in newConfig.__dict__.keys():
		if key[:2]=="__":
			continue
		if not(config.__dict__.has_key(key)):
			abort("Invalid configuration key '%s'" % key)
		s=newConfig.__dict__[key]
		if type(s)==types.StringType:
			s="'%s'" % s
		else:
			s="%s" % s
		debug("Defining %s = %s" % (key,s))
		config.__dict__[key]=newConfig.__dict__[key]
	moveIndent(-1)


if __name__=="__main__":

	config=configClass()

	try:
		configFile=sys.argv[1]
	except IndexError:
		abort("Usage: %s configFile" % sys.argv[0])

	if os.path.isfile(config.etcFile):
		loadConfig(config.etcFile)

	loadConfig(configFile)

	checkConfig()
	makeChroot()

	sys.exit(0)
