import os
import stat
import commands
import popen2
import time
import logging
import MySQLdb
import atexit

def shell_quote(string):
	"""Returns a string suitable to form a command line parameter
	for the shell."""
	return "'%s'" % string.replace("'","'\\''")

def is_valid_database_name(attrname):
	if len(attrname) == 0: return False
	if len(attrname) > 29: return False
	for a in attrname:
		if a.lower() not in "abcdefghijklmnopqrstuvwxyz_":
			return False
	if attrname[0] == "_": return False
	return True
	
def is_running(pid,cmd):
	"""Return True if a process with PID pid and executable name cmd is
	running, False	otherwise, raise IOError or OSError if it cannot be
	checked due to permissions in /proc."""

	assert type(pid) is int
	assert type(cmd) is str and len(cmd) > 0
	try:
		procpath = "/proc/%s" % pid
		if os.listdir(procpath):
			f = file(os.path.join(procpath,"cmdline"))
			cmdline = f.readlines()[0]
			pidcommand = cmdline.split("\0")[0]
			if cmd in pidcommand: return True
	except OSError,e:
		if e.errno == 2: return False
		else: raise
	return False

# FIXME make companion PGDatabase for this, so PGCatalog can refer to a PGDatabse instead of connection strings and the like

class MyDatabaseCluster:
	"""Represents a single MySQL database cluster, backed by an
	on-disk directory of data files.  The port is incrementally chosen
	and available as self.port.  If the directory does not exist, a
	brand new database cluster will be created.
	
	Only a single database server can be started using a database
	cluster.  Attempts to start two database servers using a single
	database cluster will fail and raise an OSError with errno EALREADY."""

	directory = None
	port = None
	_admin_username = "root"

	def __init__(self,directory):
		"""Initializes a cluster on the given directory."""
		self.directory = directory
		self.pidfile = os.path.join(directory,"mysql.pid")
		self.datadir = directory
		self.socket = os.path.join(directory,"mysql.sock")
		self.err_log = "/dev/null" # FIXME make configurable by the user
		self.configfile = os.path.join(directory,"my.cnf")
		import pwd
		self.unix_user = pwd.getpwuid(os.getuid())[0]
		
		logname = "MyDatabaseCluster."+ self.directory.replace(".","_")
		self.log = logging.getLogger(logname)
		
		if self.running():
			import errno
			e = OSError()
			e.errno = errno.EALREADY
			e.strerror =  "A database server is already serving %s"%self.directory
			raise OSError, e
		
		if not os.path.isdir(self.directory): self.__initdb()
		self.make_configfile()
		
	def get_pid(self):
		"""Gets the process ID of the running database server."""
		pidfile = self.pidfile
		pid = None
		try:
			f = file(pidfile,"rb")
			pid = int(f.readlines()[0].strip())
			f.close()
		except IOError,e :
			if e.errno == 2:
				# if there is no pid file, there is no pidfile, so we can go on
				return None
			# if there is another error, just raise it
			raise
		except IndexError:
			# thie pid file is empty, return none because it may be being created just right now
			f.close()
			return None
		if pid is not None:
			if is_running(pid,"mysqld"): return pid
			else:
				self.log.debug("Found corpse of database cluster, removing stale PID file")
				try: os.unlink(pidfile) # we'll try and remove the pid file just in case
				except: pass
		
	def running(self):
		"""Returns True if the database server is running"""
		return self.get_pid() is not None

	def __initdb(self):
		
		assert not self.running()
		self.log.debug("Creating new database cluster")
		old_umask = os.umask(63) # u+rw,go-rwx
		os.mkdir(self.directory)
		self.make_configfile()
		os.umask(old_umask)
		
		command = "mysql_install_db --defaults-file=%s"%shell_quote(self.configfile)
		old_umask = os.umask(63) # u+rw,go-rwx
		status,output = commands.getstatusoutput(command)
		os.umask(old_umask)
		if status != 0:
			self.log.error("mysql_install_db returned error code %s, message %s",status,output)
			raise Exception, "Cannot create database cluster %s"%self.directory
		else:
			self.log.debug("Database cluster successfully created")
	
	def make_configfile(self):
		cfg = """[mysqld]
datadir=%(datadir)s
socket=%(socket)s
port=-1

[mysql.server]
basedir=%(basedir)s

[safe_mysqld]
err-log=%(errlog)s
pid-file=%(pidfile)s
"""

		cfg = cfg%({"datadir":self.directory,"socket":self.socket,"basedir":self.directory,"errlog":self.err_log,"pidfile":self.pidfile})
		
		self.log.debug("Creating configuration file %s",self.configfile)
		
		f = file(self.configfile,"wb")
		f.write(cfg)
		f.close()
		

	def start(self):
		"""Starts the database server on the database cluster."""
		
		assert not self.running()
		
		#set a new umask
		pid = os.fork()
		if pid == 0: # child process
			command = "safe_mysqld --defaults-file=%s  --user=%s"%(shell_quote(self.configfile),shell_quote(self.unix_user))
			old_umask = os.umask(63) # u+rw,go-rwx
			self.log.debug("Starting database server with command %s",command)
			status,output = commands.getstatusoutput(command) # tu muere aqui
			if status != 0:
				try: self.log.error("Failed to start database server, message: %s",output)
				except: pass
			os._exit(0)
# 			os.umask(old_umask)

# here we will ping mysqld

		attempts = 0
		max_attempts = 20
		seconds_between_attempts = 1
		while True:
			command = "mysqladmin -S %s ping" % shell_quote(self.socket)
			try:
				status,output = commands.getstatusoutput(command)
				if status != 0: raise Exception,output
				break
			except Exception,e:
				self.log.debug("MySQL server not yet started: %s",e)
				time.sleep(seconds_between_attempts)
				attempts = attempts + seconds_between_attempts
				pass
			if attempts >= max_attempts:
				raise Exception, "Cannot start server for database cluster %s"%self.directory

		atexit.register(self.stop_abruptly_if_running)
		self.log.debug("Database server successfully started")

	def stop_if_started(self):
		"""Conditionally stops the server if the server is running."""
		if self.running(): self.stop()
	
	def stop_abruptly_if_running(self):
		"""Kills the server if the server is still running."""
		if self.running(): self.stop() # forced=True was removed because it caused safe_mysqld to restart the process

	def stop(self,forced=False):
		"""Stops the database server.  If forced is True, the
		database server will be killed instead of stopped.
		
		When the server is stopped, all connections will be
		disconnected, so make sure you have disconnected all
		connections before stopping the server.
		
		If you do not stop the server with stop() or stop_if_started
		then, when your application exits, it will be stopped by
		using forced=True."""

		assert self.running()
		
		try: self.log.debug("Stopping database server...")
		except: pass
		
		pid = self.get_pid()
		signal = 15
		if forced: signal = 9
		os.kill(pid,signal)
		
		tries = 0; maxtries = 20
		while self.get_pid() is not None and tries < maxtries:
			try: self.log.debug("Waiting for database server to stop...")
			except: pass
			time.sleep(1)
			tries += 1
		if tries >= maxtries:
			try: self.log.error("Could not stop database server")
			except: pass
		else:
			try: self.log.debug("Database server successfully stopped")
			except: pass

	def get_database(self,database_name):
		"""Returns a database connection string suitable for use
		with MySQLdb.connect or MyCatalog.  Creates the database
		if it does not exist at call time."""
		# FIXME validate the database name is not invalid identifier name , just like attrname in Catalog.py
		assert self.running() is True
		if not is_valid_database_name(database_name):
			raise ValueError, "Invalid database name: %s"%database_name
		
		#return a dictionary for parameters of connection string for the database
		#if the database does not exist, CREATE it
		d = {}
		d["unix_socket"] = self.socket
		d["db"] = "mysql"
		d["user"] = self._admin_username

		c = None
		cursor = None
		try:
			c = MySQLdb.connect(**d)
			sql = "show databases"
			cursor = c.cursor()
			cursor.execute(sql)
			results = cursor.fetchall()
			found = False
			for a in results:
				if a[0] == database_name: found = True
			cursor.close()
		except:
			if cursor: cursor.close()
			if c: c.close()
			raise
		
		if not found:
			self.log.debug("Creating database %s...",database_name)
			sql = "create database %s"%(database_name)
			cursor = c.cursor()
			cursor.execute(sql)
			cursor.close()
			self.log.debug("Database %s successfully created",database_name)
		d["db"] = database_name
		c.close()
		return d


def __test():
	log = logging.getLogger()
	log.setLevel(logging.DEBUG)
	log.addHandler(logging.StreamHandler())
	d = MyDatabaseCluster("/var/lib/metadata-service/catalog")
	d.start()
	print d.get_database("catalog")
	time.sleep(10)
	d.stop()

if __name__ == "__main__":
	__test()
