[IMP] better connection pool
bzr revid: chs@tinyerp.com-20091123170734-usmmwc5uzoik73hs
This commit is contained in:
parent
7d9177fc2d
commit
86beb91507
|
@ -97,8 +97,8 @@ class ir_cron(osv.osv, netsvc.Agent):
|
||||||
db, pool = pooler.get_db_and_pool(db_name)
|
db, pool = pooler.get_db_and_pool(db_name)
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
cr = db.cursor()
|
||||||
try:
|
try:
|
||||||
cr = db.cursor()
|
|
||||||
if not pool._init:
|
if not pool._init:
|
||||||
now = DateTime.now()
|
now = DateTime.now()
|
||||||
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
|
cr.execute('select * from ir_cron where numbercall<>0 and active and nextcall<=now() order by priority')
|
||||||
|
|
|
@ -71,13 +71,14 @@ class db(netsvc.Service):
|
||||||
self.actions[id] = {'clean': False}
|
self.actions[id] = {'clean': False}
|
||||||
|
|
||||||
db = sql_db.db_connect('template1')
|
db = sql_db.db_connect('template1')
|
||||||
cr = db.serialized_cursor()
|
cr = db.cursor()
|
||||||
try:
|
try:
|
||||||
cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
|
cr.autocommit(True) # avoid transaction block
|
||||||
cr.execute('CREATE DATABASE "%s" ENCODING \'unicode\'' % db_name)
|
cr.execute('CREATE DATABASE "%s" ENCODING \'unicode\'' % db_name)
|
||||||
finally:
|
finally:
|
||||||
cr.close()
|
cr.close()
|
||||||
sql_db.close_db('template1')
|
del db
|
||||||
|
|
||||||
class DBInitialize(object):
|
class DBInitialize(object):
|
||||||
def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
|
def __call__(self, serv, id, db_name, demo, lang, user_password='admin'):
|
||||||
cr = None
|
cr = None
|
||||||
|
@ -152,8 +153,8 @@ class db(netsvc.Service):
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
db = sql_db.db_connect('template1')
|
db = sql_db.db_connect('template1')
|
||||||
cr = db.serialized_cursor()
|
cr = db.cursor()
|
||||||
cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
|
cr.autocommit(True) # avoid transaction block
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cr.execute('DROP DATABASE "%s"' % db_name)
|
cr.execute('DROP DATABASE "%s"' % db_name)
|
||||||
|
@ -165,9 +166,7 @@ class db(netsvc.Service):
|
||||||
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
'DROP DB: %s' % (db_name))
|
'DROP DB: %s' % (db_name))
|
||||||
finally:
|
finally:
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
cr.close()
|
||||||
sql_db.close_db('template1')
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _set_pg_psw_env_var(self):
|
def _set_pg_psw_env_var(self):
|
||||||
|
@ -221,13 +220,13 @@ class db(netsvc.Service):
|
||||||
raise Exception, "Database already exists"
|
raise Exception, "Database already exists"
|
||||||
|
|
||||||
db = sql_db.db_connect('template1')
|
db = sql_db.db_connect('template1')
|
||||||
cr = db.serialized_cursor()
|
cr = db.cursor()
|
||||||
cr.autocommit(True) # XXX inhibit the effect of a serialized cursor. is it what we want ?
|
cr.autocommit(True) # avoid transaction block
|
||||||
try:
|
try:
|
||||||
cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % db_name)
|
cr.execute("""CREATE DATABASE "%s" ENCODING 'unicode' TEMPLATE "template0" """ % db_name)
|
||||||
finally:
|
finally:
|
||||||
cr.close()
|
cr.close()
|
||||||
sql_db.close_db('template1')
|
del db
|
||||||
|
|
||||||
cmd = ['pg_restore', '--no-owner']
|
cmd = ['pg_restore', '--no-owner']
|
||||||
if tools.config['db_user']:
|
if tools.config['db_user']:
|
||||||
|
@ -266,7 +265,7 @@ class db(netsvc.Service):
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
|
|
||||||
db = sql_db.db_connect('template1')
|
db = sql_db.db_connect('template1')
|
||||||
cr = db.serialized_cursor()
|
cr = db.cursor()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
|
cr.execute('ALTER DATABASE "%s" RENAME TO "%s"' % (old_name, new_name))
|
||||||
|
@ -282,9 +281,7 @@ class db(netsvc.Service):
|
||||||
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
logger.notifyChannel("web-services", netsvc.LOG_INFO,
|
||||||
'RENAME DB: %s -> %s' % (old_name, new_name))
|
'RENAME DB: %s -> %s' % (old_name, new_name))
|
||||||
finally:
|
finally:
|
||||||
cr.commit()
|
|
||||||
cr.close()
|
cr.close()
|
||||||
sql_db.close_db('template1')
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def db_exist(self, db_name):
|
def db_exist(self, db_name):
|
||||||
|
@ -319,7 +316,6 @@ class db(netsvc.Service):
|
||||||
res = []
|
res = []
|
||||||
finally:
|
finally:
|
||||||
cr.close()
|
cr.close()
|
||||||
sql_db.close_db('template1')
|
|
||||||
res.sort()
|
res.sort()
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
218
bin/sql_db.py
218
bin/sql_db.py
|
@ -19,10 +19,12 @@
|
||||||
#
|
#
|
||||||
##############################################################################
|
##############################################################################
|
||||||
|
|
||||||
|
__all__ = ['db_connect', 'close_db']
|
||||||
|
|
||||||
import netsvc
|
import netsvc
|
||||||
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE
|
from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT, ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE
|
||||||
from psycopg2.pool import ThreadedConnectionPool
|
|
||||||
from psycopg2.psycopg1 import cursor as psycopg1cursor
|
from psycopg2.psycopg1 import cursor as psycopg1cursor
|
||||||
|
from psycopg2.pool import PoolError
|
||||||
|
|
||||||
import psycopg2.extensions
|
import psycopg2.extensions
|
||||||
|
|
||||||
|
@ -48,26 +50,23 @@ psycopg2.extensions.register_type(psycopg2.extensions.new_type((700, 701, 1700,)
|
||||||
|
|
||||||
|
|
||||||
import tools
|
import tools
|
||||||
import re
|
from tools.func import wraps
|
||||||
|
from datetime import datetime as mdt
|
||||||
|
import threading
|
||||||
|
|
||||||
from mx import DateTime as mdt
|
import re
|
||||||
re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$');
|
re_from = re.compile('.* from "?([a-zA-Z_0-9]+)"? .*$');
|
||||||
re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$');
|
re_into = re.compile('.* into "?([a-zA-Z_0-9]+)"? .*$');
|
||||||
|
|
||||||
|
|
||||||
def log(msg, lvl=netsvc.LOG_DEBUG):
|
def log(msg, lvl=netsvc.LOG_DEBUG):
|
||||||
logger = netsvc.Logger()
|
logger = netsvc.Logger()
|
||||||
logger.notifyChannel('sql', lvl, msg)
|
logger.notifyChannel('sql', lvl, msg)
|
||||||
|
|
||||||
class Cursor(object):
|
class Cursor(object):
|
||||||
IN_MAX = 1000
|
IN_MAX = 1000
|
||||||
sql_from_log = {}
|
|
||||||
sql_into_log = {}
|
|
||||||
sql_log = False
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
def check(f):
|
|
||||||
from tools.func import wraps
|
|
||||||
|
|
||||||
|
def check(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapper(self, *args, **kwargs):
|
def wrapper(self, *args, **kwargs):
|
||||||
if self.__closed:
|
if self.__closed:
|
||||||
|
@ -75,36 +74,41 @@ class Cursor(object):
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
def __init__(self, pool, serialized=False):
|
def __init__(self, pool, dbname, serialized=False):
|
||||||
|
self.sql_from_log = {}
|
||||||
|
self.sql_into_log = {}
|
||||||
|
self.sql_log = False
|
||||||
|
self.sql_log_count = 0
|
||||||
|
|
||||||
|
self.__closed = True # avoid the call of close() (by __del__) if an exception
|
||||||
|
# is raised by any of the following initialisations
|
||||||
self._pool = pool
|
self._pool = pool
|
||||||
|
self.dbname = dbname
|
||||||
self._serialized = serialized
|
self._serialized = serialized
|
||||||
self._cnx = pool.getconn()
|
self._cnx = pool.borrow(dsn(dbname))
|
||||||
self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor)
|
self._obj = self._cnx.cursor(cursor_factory=psycopg1cursor)
|
||||||
self.__closed = False
|
self.__closed = False # real initialisation value
|
||||||
self.autocommit(False)
|
self.autocommit(False)
|
||||||
self.dbname = pool.dbname
|
|
||||||
|
|
||||||
if tools.config['log_level'] in (netsvc.LOG_DEBUG, netsvc.LOG_DEBUG_RPC):
|
if tools.config['log_level'] in (netsvc.LOG_DEBUG, netsvc.LOG_DEBUG_RPC):
|
||||||
from inspect import stack
|
from inspect import stack
|
||||||
self.__caller = tuple(stack()[2][1:3])
|
self.__caller = tuple(stack()[2][1:3])
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
if not self.__closed:
|
if not self.__closed:
|
||||||
|
# Oops. 'self' has not been closed explicitly.
|
||||||
|
# The cursor will be deleted by the garbage collector,
|
||||||
|
# but the database connection is not put back into the connection
|
||||||
|
# pool, preventing some operation on the database like dropping it.
|
||||||
|
# This can also lead to a server overload.
|
||||||
if tools.config['log_level'] in (netsvc.LOG_DEBUG, netsvc.LOG_DEBUG_RPC):
|
if tools.config['log_level'] in (netsvc.LOG_DEBUG, netsvc.LOG_DEBUG_RPC):
|
||||||
# Oops. 'self' has not been closed explicitly.
|
|
||||||
# The cursor will be deleted by the garbage collector,
|
|
||||||
# but the database connection is not put back into the connection
|
|
||||||
# pool, preventing some operation on the database like dropping it.
|
|
||||||
# This can also lead to a server overload.
|
|
||||||
msg = "Cursor not closed explicitly\n" \
|
msg = "Cursor not closed explicitly\n" \
|
||||||
"Cursor was created at %s:%s" % self.__caller
|
"Cursor was created at %s:%s" % self.__caller
|
||||||
|
|
||||||
log(msg, netsvc.LOG_WARNING)
|
log(msg, netsvc.LOG_WARNING)
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
@check
|
@check
|
||||||
def execute(self, query, params=None):
|
def execute(self, query, params=None):
|
||||||
self.count+=1
|
|
||||||
if '%d' in query or '%f' in query:
|
if '%d' in query or '%f' in query:
|
||||||
log(query, netsvc.LOG_WARNING)
|
log(query, netsvc.LOG_WARNING)
|
||||||
log("SQL queries mustn't contain %d or %f anymore. Use only %s", netsvc.LOG_WARNING)
|
log("SQL queries mustn't contain %d or %f anymore. Use only %s", netsvc.LOG_WARNING)
|
||||||
|
@ -124,7 +128,7 @@ class Cursor(object):
|
||||||
|
|
||||||
if self.sql_log:
|
if self.sql_log:
|
||||||
log("query: %s" % self._obj.query)
|
log("query: %s" % self._obj.query)
|
||||||
self.count+=1
|
self.sql_log_count+=1
|
||||||
res_from = re_from.match(query.lower())
|
res_from = re_from.match(query.lower())
|
||||||
if res_from:
|
if res_from:
|
||||||
self.sql_from_log.setdefault(res_from.group(1), [0, 0])
|
self.sql_from_log.setdefault(res_from.group(1), [0, 0])
|
||||||
|
@ -138,6 +142,9 @@ class Cursor(object):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def print_log(self):
|
def print_log(self):
|
||||||
|
if not self.sql_log:
|
||||||
|
return
|
||||||
|
|
||||||
def process(type):
|
def process(type):
|
||||||
sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log}
|
sqllogs = {'from':self.sql_from_log, 'into':self.sql_into_log}
|
||||||
if not sqllogs[type]:
|
if not sqllogs[type]:
|
||||||
|
@ -149,17 +156,23 @@ class Cursor(object):
|
||||||
for r in sqllogitems:
|
for r in sqllogitems:
|
||||||
log("table: %s: %s/%s" %(r[0], str(r[1][1]), r[1][0]))
|
log("table: %s: %s/%s" %(r[0], str(r[1][1]), r[1][0]))
|
||||||
sum+= r[1][1]
|
sum+= r[1][1]
|
||||||
log("SUM:%s/%d" % (sum, self.count))
|
log("SUM:%s/%d" % (sum, self.sql_log_count))
|
||||||
sqllogs[type].clear()
|
sqllogs[type].clear()
|
||||||
process('from')
|
process('from')
|
||||||
process('into')
|
process('into')
|
||||||
self.count = 0
|
self.sql_log_count = 0
|
||||||
self.sql_log = False
|
self.sql_log = False
|
||||||
|
|
||||||
@check
|
@check
|
||||||
def close(self):
|
def close(self):
|
||||||
self.rollback() # Ensure we close the current transaction.
|
if not self._obj:
|
||||||
|
return
|
||||||
|
|
||||||
self.print_log()
|
self.print_log()
|
||||||
|
|
||||||
|
if not self._serialized:
|
||||||
|
self.rollback() # Ensure we close the current transaction.
|
||||||
|
|
||||||
self._obj.close()
|
self._obj.close()
|
||||||
|
|
||||||
# This force the cursor to be freed, and thus, available again. It is
|
# This force the cursor to be freed, and thus, available again. It is
|
||||||
|
@ -169,8 +182,8 @@ class Cursor(object):
|
||||||
# part because browse records keep a reference to the cursor.
|
# part because browse records keep a reference to the cursor.
|
||||||
del self._obj
|
del self._obj
|
||||||
self.__closed = True
|
self.__closed = True
|
||||||
self._pool.putconn(self._cnx)
|
self._pool.give_back(self._cnx)
|
||||||
|
|
||||||
@check
|
@check
|
||||||
def autocommit(self, on):
|
def autocommit(self, on):
|
||||||
offlevel = [ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE][bool(self._serialized)]
|
offlevel = [ISOLATION_LEVEL_READ_COMMITTED, ISOLATION_LEVEL_SERIALIZABLE][bool(self._serialized)]
|
||||||
|
@ -188,63 +201,128 @@ class Cursor(object):
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
return getattr(self._obj, name)
|
return getattr(self._obj, name)
|
||||||
|
|
||||||
|
|
||||||
class ConnectionPool(object):
|
class ConnectionPool(object):
|
||||||
def __init__(self, pool, dbname):
|
|
||||||
|
def locked(fun):
|
||||||
|
@wraps(fun)
|
||||||
|
def _locked(self, *args, **kwargs):
|
||||||
|
self._lock.acquire()
|
||||||
|
try:
|
||||||
|
return fun(self, *args, **kwargs)
|
||||||
|
finally:
|
||||||
|
self._lock.release()
|
||||||
|
return _locked
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, maxconn=64):
|
||||||
|
self._connections = []
|
||||||
|
self._maxconn = max(maxconn, 1)
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
self._logger = netsvc.Logger()
|
||||||
|
|
||||||
|
def _log(self, msg):
|
||||||
|
#self._logger.notifyChannel('ConnectionPool', netsvc.LOG_INFO, msg)
|
||||||
|
pass
|
||||||
|
def _debug(self, msg):
|
||||||
|
#self._logger.notifyChannel('ConnectionPool', netsvc.LOG_DEBUG, msg)
|
||||||
|
pass
|
||||||
|
|
||||||
|
@locked
|
||||||
|
def borrow(self, dsn):
|
||||||
|
self._log('Borrow connection to %s' % (dsn,))
|
||||||
|
|
||||||
|
result = None
|
||||||
|
for i, (cnx, used) in enumerate(self._connections):
|
||||||
|
if not used and cnx.dsn == dsn:
|
||||||
|
self._debug('Existing connection found at index %d' % i)
|
||||||
|
|
||||||
|
self._connections.pop(i)
|
||||||
|
self._connections.append((cnx, True))
|
||||||
|
|
||||||
|
result = cnx
|
||||||
|
break
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
|
||||||
|
if len(self._connections) >= self._maxconn:
|
||||||
|
# try to remove the older connection not used
|
||||||
|
for i, (cnx, used) in enumerate(self._connections):
|
||||||
|
if not used:
|
||||||
|
self._debug('Removing old connection at index %d: %s' % (i, cnx.dsn))
|
||||||
|
self._connections.pop(i)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# note: this code is called only if the for loop has completed (no break)
|
||||||
|
raise PoolError('Connection Pool Full')
|
||||||
|
|
||||||
|
self._debug('Create new connection')
|
||||||
|
result = psycopg2.connect(dsn=dsn)
|
||||||
|
self._connections.append((result, True))
|
||||||
|
return result
|
||||||
|
|
||||||
|
@locked
|
||||||
|
def give_back(self, connection):
|
||||||
|
self._log('Give back connection to %s' % (connection.dsn,))
|
||||||
|
for i, (cnx, used) in enumerate(self._connections):
|
||||||
|
if cnx is connection:
|
||||||
|
self._connections.pop(i)
|
||||||
|
self._connections.append((cnx, False))
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise PoolError('This connection does not below to the pool')
|
||||||
|
|
||||||
|
@locked
|
||||||
|
def close_all(self, dsn):
|
||||||
|
for i, (cnx, used) in tools.reverse_enumerate(self._connections):
|
||||||
|
if cnx.dsn == dsn:
|
||||||
|
cnx.close()
|
||||||
|
self._connections.pop(i)
|
||||||
|
|
||||||
|
|
||||||
|
class Connection(object):
|
||||||
|
__LOCKS = {}
|
||||||
|
|
||||||
|
def __init__(self, pool, dbname, unique=False):
|
||||||
self.dbname = dbname
|
self.dbname = dbname
|
||||||
self._pool = pool
|
self._pool = pool
|
||||||
|
self._unique = unique
|
||||||
|
if unique:
|
||||||
|
if dbname not in self.__LOCKS:
|
||||||
|
self.__LOCKS[dbname] = threading.Lock()
|
||||||
|
self.__LOCKS[dbname].acquire()
|
||||||
|
|
||||||
def cursor(self):
|
def __del__(self):
|
||||||
return Cursor(self)
|
if self._unique:
|
||||||
|
self.__LOCKS[self.dbname].release()
|
||||||
|
|
||||||
|
def cursor(self, serialized=False):
|
||||||
|
return Cursor(self._pool, self.dbname, serialized=serialized)
|
||||||
|
|
||||||
def serialized_cursor(self):
|
def serialized_cursor(self):
|
||||||
return Cursor(self, True)
|
return self.cursor(True)
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self._pool, name)
|
|
||||||
|
|
||||||
class PoolManager(object):
|
_dsn = ''
|
||||||
_pools = {}
|
for p in ('host', 'port', 'user', 'password'):
|
||||||
_dsn = None
|
cfg = tools.config['db_' + p]
|
||||||
maxconn = int(tools.config['db_maxconn']) or 64
|
if cfg:
|
||||||
|
_dsn += '%s=%s ' % (p, cfg)
|
||||||
@classmethod
|
|
||||||
def dsn(cls, db_name):
|
|
||||||
if cls._dsn is None:
|
|
||||||
cls._dsn = ''
|
|
||||||
for p in ('host', 'port', 'user', 'password'):
|
|
||||||
cfg = tools.config['db_' + p]
|
|
||||||
if cfg:
|
|
||||||
cls._dsn += '%s=%s ' % (p, cfg)
|
|
||||||
return '%s dbname=%s' % (cls._dsn, db_name)
|
|
||||||
|
|
||||||
@classmethod
|
def dsn(db_name):
|
||||||
def get(cls, db_name):
|
return '%sdbname=%s' % (_dsn, db_name)
|
||||||
if db_name not in cls._pools:
|
|
||||||
logger = netsvc.Logger()
|
|
||||||
try:
|
|
||||||
logger.notifyChannel('dbpool', netsvc.LOG_INFO, 'Connecting to %s' % (db_name,))
|
|
||||||
cls._pools[db_name] = ConnectionPool(ThreadedConnectionPool(1, cls.maxconn, cls.dsn(db_name)), db_name)
|
|
||||||
except Exception, e:
|
|
||||||
logger.notifyChannel('dbpool', netsvc.LOG_ERROR, 'Unable to connect to %s: %s' %
|
|
||||||
(db_name, str(e)))
|
|
||||||
raise
|
|
||||||
return cls._pools[db_name]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def close(cls, db_name):
|
_Pool = ConnectionPool(int(tools.config['db_maxconn']))
|
||||||
if db_name in cls._pools:
|
|
||||||
logger = netsvc.Logger()
|
|
||||||
logger.notifyChannel('dbpool', netsvc.LOG_INFO, 'Closing all connections to %s' % (db_name,))
|
|
||||||
cls._pools[db_name].closeall()
|
|
||||||
del cls._pools[db_name]
|
|
||||||
|
|
||||||
def db_connect(db_name):
|
def db_connect(db_name):
|
||||||
return PoolManager.get(db_name)
|
unique = db_name in ['template1', 'template0']
|
||||||
|
return Connection(_Pool, db_name, unique)
|
||||||
|
|
||||||
def close_db(db_name):
|
def close_db(db_name):
|
||||||
PoolManager.close(db_name)
|
_Pool.close_all(dsn(db_name))
|
||||||
tools.cache.clean_caches_for_db(db_name)
|
tools.cache.clean_caches_for_db(db_name)
|
||||||
|
|
||||||
|
|
||||||
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue