diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml new file mode 100644 index 0000000..0017c8e --- /dev/null +++ b/.github/workflows/ubuntu.yml @@ -0,0 +1,48 @@ +name: Tests Ubuntu + +# Run this workflow every time a new commit pushed to your repository +on: [push, pull_request] + +jobs: + # Set the job key. The key is displayed as the job name + # when a job name is not provided + test: + # Name the Job + name: Tests Ubuntu + # Set the type of machine to run on + runs-on: ubuntu-latest + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ROOT_PASSWORD: root + ports: + - 3306:3306 + steps: + # Checks out a copy of your repository on the ubuntu-latest machine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '2.x' + + - name: Install Python modules + run: pip install twisted mysqlclient + + - name: Test SQLite + run: python tests/main_test.py + + - name: Verify MySQL connection from host + run: mysql --host 127.0.0.1 --port 3306 -uroot -proot -e "CREATE DATABASE base" + + - name: Test MySQL + run: | + echo "[server]" > coalition.ini + echo "db_type=mysql" >> coalition.ini + echo "db_mysql_host=127.0.0.1" >> coalition.ini + echo "db_mysql_user=root" >> coalition.ini + echo "db_mysql_password=root" >> coalition.ini + echo "db_mysql_base=base" >> coalition.ini + python tests/main_test.py diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml new file mode 100644 index 0000000..e2c3f08 --- /dev/null +++ b/.github/workflows/win.yml @@ -0,0 +1,29 @@ +name: Tests Windows + +# Run this workflow every time a new commit pushed to your repository +on: [push, pull_request] + +jobs: + # Set the job key. The key is displayed as the job name + # when a job name is not provided + test: + # Name the Job + name: Tests Windows + # Set the type of machine to run on + runs-on: windows-latest + + steps: + # Checks out a copy of your repository on the ubuntu-latest machine + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '2.x' + + - name: Install Python modules + run: pip install twisted pypiwin32 + + - name: Test + run: python tests/main_test.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a3798e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +*.pyc +*.log +log/* +docs/* +*.db +*.cache/ +_build +_static +_templates diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..c46d46b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +language: python +cache: pip +python: +- "2.7" +services: + - mysql +before_install: +- sudo apt-get update -qq +- sudo apt-get install -qq python-sqlite +- mysql -e 'CREATE DATABASE base;' +install: "pip install -r requirements.txt" +script: +- echo [server] > coalition.ini +- echo db_type=sqlite >> coalition.ini +- tests/main_test.py +- echo [server] > coalition.ini +- echo db_type=mysql >> coalition.ini +- echo db_mysql_user=travis >> coalition.ini +- tests/main_test.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..457244c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM debian:buster-slim +WORKDIR /usr/src/app + +RUN apt-get update && apt-get install -y python python-pip python-mysqldb libldap2-dev libsasl2-dev && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt ./ +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . +CMD [ "python", "./server.py", "--verbose" ] +EXPOSE 19211 diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..06c756c --- /dev/null +++ b/README.rst @@ -0,0 +1,48 @@ +.. |badge-doc| image:: https://readthedocs.org/projects/coalition/badge/?version=latest + :target: http://coalition.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +.. |badge-size| image:: https://reposs.herokuapp.com/?path=https://github.com/MercenariesEngineering/coalition + +.. |badge-version| image:: https://badge.fury.io/gh/MercenariesEngineering%2Fcoalition.svg + :target: https://badge.fury.io/gh/MercenariesEngineering%2Fcoalition + +.. |badge-coverage| image:: https://coveralls.io/repos/github/MercenariesEngineering/coalition/badge.svg?branch=development + :target: https://coveralls.io/github/MercenariesEngineering/coalition?branch=development + +.. |badge-tests| image:: https://travis-ci.org/MercenariesEngineering/coalition.svg?branch=master + +|badge-doc| |badge-size| |badge-version| |badge-coverage| |badge-tests| + +`Full online documentation is availlble on ReadTheDocs `_. + +Coalition +========= + +**Coalition** is a lightweight open source **job manager** client-server application whose role is to control **job execution in a set of computers**. A computer is acting as a **server** centralizing the list of jobs to be done. A set of physical (or virtual, eg. in the cloud) computers acting as **workers** shall be deployed, raising the global grid system ressources. + +The server waits for incoming workers connections. Workers ask the server for a job to do. When the server is asked by a worker for a job, he decides which job to attribute according to simple **affinity rules**. The worker is now aware of which job it has to do. The worker executes the job. When the job is done, the worker informs the server of the job's execution status and ask for a new job. + +*Coalition* should not be used on the public Internet but on **private LANs**, **cloud VLANs** or **VPN** for security reasons. + +*Coalition* has been successfully used in production notably for **renderfarms**. + +*Coalition* provides: + +- **Broadcast discovery** for workers to find the server whithout configuration; +- **RESTfull python API** based on `Twisted matrix `_ for program to program communication; +- **Cloud ready** configuration to manage starting/termination of workers in the cloud; +- **Web interface** for humans to control jobs, workers, affinities and view status and logs; +- **Database** interface for sqlite and mysql; +- **Logging** system; +- **Email notification** system; +- **Access Control List** when connected to a **LDAP** server; +- **Unittests** of critical code parts; +- **Source code** and **documentation** on `the development platform `_. + +The current stable version are 3.8 and 3.10. + +The development version is |current-version|. + +.. |current-version| include:: version + diff --git a/__init.py__ b/__init.py__ new file mode 100644 index 0000000..e69de29 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/_cloud_aws.ini b/_cloud_aws.ini new file mode 100644 index 0000000..58f4d14 --- /dev/null +++ b/_cloud_aws.ini @@ -0,0 +1,62 @@ +# Configuration file for aws cloud + +[authentication] +# Aws ssh key pair name +keyname= +# Accesskey +accesskey= +# Secretkey +secretaccesskey= + +[storage] +# Storage name +name= +# Mountpoint in the worker +mountpoint=/mnt/bucket +# Location of the guerilla installer in the storage +guerillarenderfilename=srv/guerilla_render_2.0.0a13_linux64.tar.gz +# Location of the coalition installer in the storage +coalitionfilename=srv/coalition.tar.gz + +[coalition] +# Coalition server IP +ip = +# Coalition server port +port = 19211 +# Maximum number of simultaneous workers +workerinstancemax=3 +# Delay in seconds between creation of instances. +# This prevents massive instances creation for big list of short time jobs. +# Default is 30 seconds. +workerinstancestartdelay=30 +# Minimum lifetime in seconds before allowing the termination of useless +# worker instances. Since an instance requires several minutes to start, +# this option offers the possibility of keeping instances ready even during +# a short time without jobs. +# Default is 900 seconds = 15 minutes. +workerinstanceminimumlifetime=900 + +[worker] +# Prefix for the new instance name +nameprefix=cloud- +spot=true +# Instance type +# https://aws.amazon.com/ec2/instance-types/ +# http://www.ec2instances.info/ +instancetype=m3.medium +# Aws image, for instance debian-stretch-amd64-hvm-2016-09-23-08-48-ebs +imageid=ami-2f40bd40 +# Aws subnet +subnetid= +# Aws instance profile +iaminstanceprofile= +# Aws security group +securitygroupid= +availabilityzone= +[spot] +# http://docs.aws.amazon.com/cli/latest/reference/ec2/request-spot-instances.html +# https://aws.amazon.com/ec2/spot/pricing/ +spotprice=10 +instancecount=1 +type=one-time + diff --git a/_cloud_gcloud.ini b/_cloud_gcloud.ini new file mode 100644 index 0000000..ed3d0a5 --- /dev/null +++ b/_cloud_gcloud.ini @@ -0,0 +1,58 @@ +# Configuration file for google cloud + +[authentication] +# Project name +;project=guerilla-cloud +# Location of json key file for service user got from developper interface +;keyfile=guerilla-cloud-34bf64e0149b.json +# Service account +;serviceaccount=19254862847-compute@developer.gserviceaccount.com +;scopes=default + + +[storage] +# Storage name +;name=guerilla-cloud-bucket +# Mountpoint in the worker +;mountpoint=/mnt/bucket +# Location of the coalition installer in the storage +;coalitionpackage=srv/coalition.tar.gz + +[coalition] +# Coalition server IP +;ip = 10.132.0.2 +# Coalition server port +;port = 19211 +# Maximum number of simultaneous workers +;workerinstancemax=3 +# Delay in seconds between creation of instances. +# This prevents massive instances creation for big list of short time jobs. +# Default is 30 seconds. +;workerinstancestartdelay=30 +# Minimum lifetime in seconds before allowing the termination of useless +# worker instances. Since an instance requires several minutes to start, +# this option offers the possibility of keeping instances ready even during +# a short time without jobs. +# Default is 900 seconds = 15 minutes. +;workerinstanceminimumlifetime=900 + +[main_program] +;package=srv/guerilla_render_2.0.0a13_linux64.tar.gz +;environment=GUERILLA=/usr/local/bin/guerillarender/data/usr/local/guerilla GUERILLA_CLOUD_ROOT=/mnt/bucket + +[worker] +# Install dir fr coalition and main program +;installdir=/usr/local/bin +# Prefix for the new instance name +;nameprefix=cloud- +;zone=europe-west1-d +;machinetype=f1-micro +;subnet=default +;preemptible=true +# maintenancepolicy must be TERMINATE if preemptible is true +;maintenancepolicy=TERMINATE +;image=debian-8-jessie-v20170308 +;imageproject=debian-cloud +;bootdisksize=10 +;bootdisktype=pd-standard + diff --git a/_cloud_qarnot.ini b/_cloud_qarnot.ini new file mode 100644 index 0000000..237e018 --- /dev/null +++ b/_cloud_qarnot.ini @@ -0,0 +1,51 @@ +# Configuration file for qarnot cloud + +[authentication] +; client_token = + +[storage] +# Storage name +;name=guerilla-cloud-bucket +# Mountpoint in the worker +;mountpoint=/mnt/bucket +# Location of the coalition installer in the storage +;coalitionpackage=srv/coalition.tar.gz + +[coalition] +# Coalition server IP +;ip = 10.132.0.2 +# Coalition server port +;port = 19211 +# Maximum number of simultaneous workers +;workerinstancemax=3 +# Delay in seconds between creation of instances. +# This prevents massive instances creation for big list of short time jobs. +# Default is 30 seconds. +;workerinstancestartdelay=30 +# Minimum lifetime in seconds before allowing the termination of useless +# worker instances. Since an instance requires several minutes to start, +# this option offers the possibility of keeping instances ready even during +# a short time without jobs. +# Default is 900 seconds = 15 minutes. +;workerinstanceminimumlifetime=900 + +[main_program] +;package=srv/guerilla_render_2.0.0a13_linux64.tar.gz +;environment=GUERILLA=/usr/local/bin/guerillarender/data/usr/local/guerilla GUERILLA_CLOUD_ROOT=/mnt/bucket + +[worker] +# Install dir fr coalition and main program +;installdir=/usr/local/bin +# Prefix for the new instance name +;nameprefix=cloud- +;zone=europe-west1-d +;machinetype=f1-micro +;subnet=default +;preemptible=true +# maintenancepolicy must be TERMINATE if preemptible is true +;maintenancepolicy=TERMINATE +;image=debian-8-jessie-v20170308 +;imageproject=debian-cloud +;bootdisksize=10 +;bootdisktype=pd-standard + diff --git a/_coalition.ini b/_coalition.ini new file mode 100644 index 0000000..7053906 --- /dev/null +++ b/_coalition.ini @@ -0,0 +1,176 @@ + +[server] +# Server configuration + +# Type of database to use. "sqlite" for a file based database, "mysql" for an external mysql server. +#db_type=sqlite + +# The sqlite database file +#db_sqlite_file=coalition.db + +# The mysql server +#db_mysql_host=127.0.0.1 +#db_mysql_user= +#db_mysql_password= +#db_mysql_base=base +#db_mysql_install=1 + +# Server port (default is 19211) +#port=19211 + +# Server mode [normal|aws|gcloud] (default is normal) +# If cloud mode is selected (all but "normal"), the corresponding +# configuration file has to be edited. +# eg. the file "aws_cloud.ini" for servermode="aws". +servermode=normal + +# Worker time out in seconds, time lapse after a worker missing heartbeats is considered out (default is 10) +#timeout=10 + +# Run the server as service (Windows only) +#service=0 + +# Display verbose logs +#verbose=0 + +# Notify the user after the N first children jobs have been finished. 0 disables this notification. +#notifyafter=10 + +# Decrease the priority of a parent job after N errors. +#decreasepriorityafter=10 + +# SMTP server hostname, emails disabled if empty +#smtphost= + +# SMTP server port +#smtpport=587 + +# SMTP use a TLS connection +#smtptls=1 + +# SMTP sender email +#smtpsender= + +# SMTP server login, no authentification if empty +#smtplogin= + +# SMTP server password, no authentification if empty +#smtppasswd= + +### LDAP configuration ### +# LDAP server to use for authentication +# If not empty, coalition will require a login and a password at every requests +; ldaphost=ldap://localhost + +# Set to True to prevent password validation for API requests. +# Useful for scripts using API as it prevents hard-writing passwords. +# Authentification is still required while serving index.html to force web frontend users login. +; ldapunsafeapi=True + +# LDAP base (used for searches) +; ldapbase=dc=ldap,dc=localhost,dc=lan + +# LDAP template used to validate the user, eg. +# uid=cn=__login__,ou=people,dc=example,dc=com +# where __login__ will be replaced by the user login +; ldaptemplatelogin = cn=__login__,dc=ldap,dc=localhost,dc=lan + +### Group permissions ### +# Permissions are defined following the generic CRUD actions. +# Two major modes are predefined: per user or global. +# Per user mode offers CRUD actions only for jobs owned by the user. +# Global mode offers CRUD actions to the user for any job. +# +# Per user actions: +# createjob: User can create jobs owned by himself. +# viewjob: User can see his jobs. +# editjob: User can edit his jobs. +# deletejob: User can delete his jobs. +# +# Global actions: +# createjobglobal: User can create a job owned by any other user. +# viewjobglobal: User can see any job. +# editjobglobal: User can edit any job. +# deletejobglobal: User can delete any job. +# +# LDAP template are used to validate that the user belongs to a specific group +# +# For instance, 3 permission groups can be defined in LDAP this way: +# 1. administrators: Can create, view, edit and delete any job. +# 2. wranglers: Can create, view and edit any job (but not delete). +# 3. artists: Can create, view, edit and delete only his own jobs. +# +# __login__ is replaced by the username. + +; ldaptemplatecreatejob=(& (cn=artists) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplateviewjob= (& (cn=artists) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplateeditjob= (& (cn=artists) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplatedeletejob=(& (cn=artists) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) + +; ldaptemplatecreatejobglobal=(& (| (cn=administrators) (cn=wranglers) ) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplateviewjobglobal= (& (| (cn=administrators) (cn=wranglers) ) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplateeditjobglobal= (& (| (cn=administrators) (cn=wranglers) ) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) +; ldaptemplatedeletejobglobal=(& (cn=administrators) (member=cn=__login__,dc=ldap,dc=localhost,dc=lan) ) + +# Command white list. +# For global permission, use "global" as first parameter +# For per user permission, use "" as first paramater + +# Global and per user command while list. +#commandwhitelist=global command1 regexp +# global command2 regexp +# @user1 +# user1 command1 regexp The command white list can be global or per user. +# user1 command2 regexp +# @user2 +# user2 command1 regexp +# user2 command2 regexp + +[worker] +# Worker configuration + +# Server URL, like http://serverhost:19211, let it blank to use autodetection by broadcasting. +#serverUrl= + +# Number of simultaneous workers on this system (default is 1) +#workers=1 + +# Worker name (default is host name) +#name=MyWorker + +# Sleep time between two heartbeats in seconds (default is 2) +#sleep=2 + +# Maximum number of cpus per worker, will override the number of workers when defined (Windows only) +#cpus=None + +# Command to execute at worker startup +#startup= + +# Display verbose logs +#verbose=0 + +# Customize the command used to run the job. +# +# The command set in runcommand is responsible for : +# * changing the user +# * changing the working directory +# * run the job command with the current environment +# +# The following pattern will be replaced: +# __user__ : the job user name +# __dir__ : the job directory +# __cmd__ : the job command +# +# If runcommand is blank, the worker set the working directory to the job directory +# and run the command using the worker permissions. + +# Default run command +#runcommand= + +# Run the jobs using sudo +#runcommand=sudo -u __user__ -E -- sh -c 'cd __dir__; __cmd__' + +# Workers log file +#logfile=./worker.log + diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/coalition.py b/api/coalition.py new file mode 100644 index 0000000..ad28901 --- /dev/null +++ b/api/coalition.py @@ -0,0 +1,235 @@ +# -*- coding: utf-8 -*- + +import httplib, urllib, json, sys + + +class CoalitionError(Exception): + pass + + + + +class Connection(object): + """A connection to the coalition server. + + :param str host: The coalition server hostname. + :param int port: The coalition server port. + """ + + def __init__(self, host='localhost', port=19211): + """Setup http connection.""" + self.IntoWith = False + self._Conn = httplib.HTTPConnection (host, port) + + def _send (self, method, command, params=None): + """Send message to server. + + :param str method: Http request method between "GET", "PUT", "POST" and "DELETE". + :param str command: REST api URL. + :param str params: Optional parameters. + :return: A string or an error. + :rtype: str or :class:CoalitionError + """ + + if params: + params = json.dumps (params) + headers = {'Content-Type': 'application/json'} + self._Conn.request (method, command, params, headers) + res = self._Conn.getresponse() + if res.status == 200: + return res.read () + else: + raise CoalitionError (res.read()) + + def newJob(self, parent=0, title='', command='', dir='', environment='', + state="WAITING", paused=False, priority=1000, timeout=0, + affinity='', user='', progress_pattern='', dependencies=[]): + """Create a :class:`Job`. + + :param int parent: The parent :class:`Job` id. + :param str title: The :class:`Job` title. + :param str command: The :class:`Job` command, or an empty string for a parent node. + :param str dir: The :class:`Job` directory. This is the current directory when the :class:`Job` is run. + :param str environment: The :class:`Job` environment variables. + :param str state: The :class:`Job` initial state. It must be "WAITING" or "PAUSED". If the state is "WAITING", the :class:`Job` will start as soon as possible. If the state is "PAUSED", the :class:`Job` won't start until it is started or reset. + :param int priority: The :class:`Job` priority. For a given :class:`Job` hierarchy level, the :class:`Job` with the biggest priority is taken first. + :param int timeout: The maximum duration a :class:`Job` run can take in seconds. If timeout=0, no limit on the :class:`Job` run. + :param str affinity: The :class:`Job` affinity string. Affinities are coma separated keywords. To run a :class:`Job`, the worker affinities must match all the :class:`Job` affinities. + :param str user: The :class:`Job` user name. + :param str progress_pattern: A regexp pattern which filters the logs and return the progression. The pattern must include a '%percent' or a '%one' keyword. + :param list(int) dependencies: The :class:`Job` ids on which the new :class:`Job` has dependencies. The :class:`Job` will run when the dependency jobs have been completed without error. + :return: The :class:`Job` id. + :rtype: int + """ + + params = locals().copy () + del params['self'] + res = self._send ("PUT", '/api/jobs', params) + return int(res) + + def getJob (self, id): + """Get a :class:`Job` instance. + + :param int id: The id of the :class:`Job`. + :return: A :class:`Job` instance. + :rtype: :class:`Job` + """ + + res = self._send('GET', '/api/jobs/' + str(id)) + return Job (json.loads(res), self) + + def getJobChildren (self, id): + """Get :class:`Job` children instances. + + :param int id: The parent :class:`Job` id. + :return: The list of children :class:`Job` instances. + :rtype: list(:class:`Job`) + """ + + res = self._send('GET', '/api/jobs/{id}/children'.format(id=id)) + return [Job(r, self) for r in json.loads(res)] + + def getJobDependencies (self, id): + """Get the :class:`Job` dependencies. + Alternatively, the dependencies attribute of a :class:`Job` contains the list + of dependent jobs ids. + + :param str id: The :class:`Job` id having dependencies. + :return: The :class:`Job` instances on which the :class:`Job` has dependencies. + :rtype: list(:class:`Job`) + """ + + res = self._send('GET', '/api/jobs/{}/dependencies'.format(id)) + return [Job(r, self) for r in json.loads(res)] + + def setJobDependencies (self, id, ids): + '''Set the :class:`Job` objects on which a job has a dependency. + Alternatively, one can set the dependencies attribute of a Job. + + :param id int: the id of the job with dependencies + :param ids [int]: the list of job.id (int) on which the job depends + ''' + res = self._send ("POST", '/api/jobs/'+str(id)+'/dependencies', ids) + return res + + def setAffinities( self, data ): + '''Set the affinities. + Affinities need to be set before they can be assigned to :class:`Job` or Worker. + + :param data: a dictionnary of affinities + ''' + res = self._send( "POST", "/api/affinities", data ) + return res + + def getAffinities( self ): + '''Get the affinities. + Affinities need to be set before they can be assigned to :class:`Job` or Worker. + + :param data: a dictionnary of affinities + ''' + + res = self._send( "GET", "/api/affinities" ) + res = json.loads( res ) + return res + + def getWorkers ( self ): + '''Returns the :class:`Worker` objects. + Workers are identified by an index. + + :rtype: the list of :class:`Worker` objects. + ''' + + res = self._send ("GET", '/api/workers') + res = json.loads( res ) + return res + + def editWorkers( self, workers ): + '''Set the :class:`Worker` objects. + All the workers' attributes are updated. + + :param data: a dictionnary of workers. + ''' + + res = self._send( "POST", '/api/workers', workers ) + return res + + def __enter__(self): + self.Jobs = {} + self.Workers = {} + self.IntoWith = True + + def __exit__(self, type, value, traceback): + self.IntoWith = False + + # Convert an object in dict + def convobj (o): + d = o.__dict__.copy() + del d['Conn'] + return d + + if not isinstance(value, TypeError): + if len(self.Jobs) > 0: + self._send ("POST", '/api/jobs', self.Jobs) + if len(self.Workers) > 0: + self._send ("POST", '/api/workers', self.Workers) + + +class Job(object): + '''A job object returned by the :class:`Connection`. Don't create such objects yourself. + Job properties should be modified into a Connection with block. Don't modify the id or the state properties directly. + ''' + + def __init__ (self, d, conn): + assert (conn) + self.Conn = False + self.__dict__.update (d) + self.Conn = conn + """:var int id: the job id + :var int parent: the parent job id + :var str title: the job title + :var str command: the job command to execute, or an empty string if the job is a parent node. + :var str dir: the job working directory + :var str environment: the job environment + :var str state: the job state. It can be "WAITING", "PAUSED", "WORKING", "PENDING", "FINISHED" or "ERROR" + :var str paused: the job is paused, which is an alias for state == "PAUSED". + :var str worker: the last worker name who took the job + :var int start_time: the job start time (in seconds after epoch) + :var int duration: the job duration (in seconds) + :var int ping_time: the last time a worker ping on this job (in seconds after epoch) + :var int run_done: number of run done on this job + :var int timeout: maximum duration a job run can take in seconds. If timeout=0, no limit on the job run. + :var int priority: the job priority. For a given job hierarchy level, the job with the biggest priority is taken first. + :var str affinity: the job affinity string. Affinities are coma separated keywords. To run a job, the worker affinities must match all the job affinities. + :var str user: the job user name. + :var int finished: number of finished children jobs. For parent node only. + :var int errors: number of faulty children jobs. For parent node only. + :var int working: number of working children jobs. For parent node only. + :var int total: number of total (grand)children jobs. For parent node only. + :var int total_finished: number of finished (grand)children jobs. For parent node only. + :var int total_errors: number of faulty (grand)children jobs. For parent node only. + :var int total_working: number of working (grand)children jobs. For parent node only. + :var array dependencies: the ids of jobs this job is dependent on. + :var str url: an URL to the job result. If available, a link to this URL will be shown in the interface. + :var float progress: the job progression between 0 and 1. + :var str progress_pattern: a regexp pattern which filters the logs and return the progression. The pattern must include a '%percent' or a '%one' keyword. + """ + + def __setattr__(self, attr, value): + if attr != "Conn" and self.Conn: + if self.Conn.IntoWith: + w = self.Conn.Jobs.get(self.id) + if not w: + w = {} + self.Conn.Jobs[self.id] = w + w[attr] = value + else: + raise CoalitionError("Can't write attributes outside a connection block") + super(Job, self).__setattr__(attr, value) + + +class CoalitionError(Exception): + pass + + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/build_win32_installer.bat b/build_win32_installer.bat index d4685a3..3a2d2e3 100644 --- a/build_win32_installer.bat +++ b/build_win32_installer.bat @@ -1,2 +1,2 @@ -python install/win32/build_installer.py -pause +python install/win32/build_installer.py +pause diff --git a/cloud/__init__.py b/cloud/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/cloud/aws.py b/cloud/aws.py new file mode 100644 index 0000000..dbbe4de --- /dev/null +++ b/cloud/aws.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +""" +This module provides functions used for aws service. +""" + + +from cloud import common +import subprocess +import json +from string import Template +from base64 import encodestring + + +def startInstance(name, config): + """ + Run the aws command to start a worker instance. + Return the created instanceid in case of dedicated ec2 instance or the spotinstancerequestid + in case of a spot instance. + """ + + if config.get("worker", "spot"): + cmd = ["aws", "ec2", "request-spot-instances", + "--spot-price", config.get("spot", "spotprice"), + "--instance-count", config.get("spot", "instancecount"), + "--type", config.get("spot", "type"), + "--launch-specification", _getLaunchSpecification(name, config),] + else: + cmd = ["aws", "ec2", "run-instances", + "--key-name", config.get("authentication", "keyname"), + "--image-id", config.get("worker", "imageid"), + "--instance-type", config.get("worker", "instancetype"), + "--subnet-id", config.get("worker", "subnetid"), + "--security-group-ids", + config.get("worker", "securitygroupid"), + "--iam-instance-profile", + "Arn=%s" % config.get("worker", "iaminstanceprofile"), + "--user-data", _getUserData(name, config),] + common._run_or_none(cmd) + + +def stopInstance(name, config): + """Run the aws command to terminate the instance.""" + cmd = ["aws", "ec2", "terminate-instances", "--instance-ids", + _getInstanceIdByName(name)] + common._run_or_none(cmd) + + +def _getLaunchSpecification(name, config): + with open("cloud/aws_worker_spot_launchspecification.json.template", 'r') as f: + template = Template(f.read()) + values = { + "image_id": config.get("worker", "imageid"), + "keyname": config.get("authentication", "keyname"), + "security_group_id": config.get("worker", "securitygroupid"), + "instance_type": config.get("worker", "instancetype"), + "user_data": encodestring(_getUserData(name, config)), } + return template.substitute(values).replace('\n', '') + + +def _getUserData(name, config): + """ + Prepare the user-data script in cloud-init syntax. + Return the script as a string. + """ + + with open("cloud/aws_worker_cloud_init.template", 'r') as f: + template = Template(f.read()) + values = { + "hostname": name, + "region": config.get("authentication", "region"), + "access_key": config.get("authentication", "accesskey"), + "secret_access_key": + config.get("authentication", "secretaccesskey"), + "bucket_name": config.get("storage", "name"), + "mount_point": config.get("storage", "mountpoint"), + "guerilla_render_filename": + config.get("storage", "guerillarenderfilename"), + "coalition_filename": + config.get("storage", "coalitionfilename"), + "coalition_server_ip": config.get("coalition", "ip"), + "coalition_server_port": config.get("coalition", "port"),} + return template.substitute(values) + + +def _getInstanceIdByName(name): + """Return instanceid from name.""" + cmd = ["aws", "ec2", "describe-instances"] + output = common._check_output_or_none(cmd) + if output: + for resources in json.loads(output)["Reservations"]: + instance = resources["Instances"][0] + if instance.has_key("Tags"): + for tags in instance["Tags"]: + if tags["Key"] == "Name" and tags["Value"] == name: + return instance["InstanceId"] + return None + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/cloud/aws_worker_cloud_init.template b/cloud/aws_worker_cloud_init.template new file mode 100644 index 0000000..e46462d --- /dev/null +++ b/cloud/aws_worker_cloud_init.template @@ -0,0 +1,35 @@ +#cloud-config + +# This cloud-init template is used for aws workers's startup configuration. +# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html +# http://cloudinit.readthedocs.io/ + +fqdn: $hostname + +repo_update: true +repo_upgrade: all + +packages: + - python2.7 + - python-pip + - python-twisted + - python-twisted-web + - python-mysqldb + - curl + - s3fs + +runcmd: + - pip install awscli + - AWS_ACCESS_KEY_ID=$access_key AWS_SECRET_ACCESS_KEY=$secret_access_key aws ec2 --region $region create-tags --resources $$(curl http://instance-data/latest/meta-data/instance-id) --tags Key=Name,Value=$hostname + - mkdir -p $mount_point + - chmod a+w $mount_point + - echo $bucket_name:$access_key:$secret_access_key > /etc/passwd-s3fs + - chmod 0640 /etc/passwd-s3fs + - s3fs -o url=https://s3.amazonaws.com,enable_content_md5 $bucket_name $mount_point + - cat $mount_point/$guerilla_render_filename | tar xzf - -C /tmp/ + - mv /tmp/guerillarender/data/usr/local/guerilla /usr/local/bin/ + - rm -rf /tmp/guerillarender + - cat $mount_point/$coalition_filename | tar xzf - -C /tmp/ + - mv /tmp/coalition /usr/local/bin/ + - GUERILLA=/usr/local/bin/guerilla GUERILLA_CLOUD_ROOT=$mount_point /usr/bin/python2.7 /usr/local/bin/coalition/worker.py http://$coalition_server_ip:$coalition_server_port + diff --git a/cloud/aws_worker_spot_launchspecification.json.template b/cloud/aws_worker_spot_launchspecification.json.template new file mode 100644 index 0000000..209631d --- /dev/null +++ b/cloud/aws_worker_spot_launchspecification.json.template @@ -0,0 +1,8 @@ +{ + "ImageId": "$image_id", + "KeyName": "$keyname", + "SecurityGroupIds": [ "$security_group_id" ], + "InstanceType": "$instance_type", + "UserData": "$user_data" +} + diff --git a/cloud/common.py b/cloud/common.py new file mode 100644 index 0000000..70d4a59 --- /dev/null +++ b/cloud/common.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +""" +This module contains functions common to various cloud providers. +""" + +from time import time +import subprocess + + +def createWorkerInstanceName(prefix): + """Return a unique name based on prefix and timestamp.""" + return "%s%s" % (prefix, int(time())) + + +def _run_or_none(cmd): + """Execute command. Returns None in case of exception.""" + try: + return subprocess.Popen(cmd, stderr=subprocess.STDOUT, + universal_newlines=True) + except Exception as e: + print(e) + return None + +def _check_output_or_none(cmd): + """Execute command. Returns None in case of exception.""" + try: + return subprocess.check_output(cmd, stderr=subprocess.STDOUT, + universal_newlines=True) + except Exception as e: + print(e) + return None + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/cloud/gcloud.py b/cloud/gcloud.py new file mode 100644 index 0000000..092c6c7 --- /dev/null +++ b/cloud/gcloud.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +""" +This module provides functions used for google cloud service. +""" + + +from cloud import common +import subprocess +import json +from string import Template +import tempfile +from base64 import encodestring +import os + + +def startInstance(name, config): + """ + Run the gcloud command to start a worker instance. + Return the created FIXME + """ + + # gcloud command line tool is picky with params escaping (eg. key-id in json) + # So we use a real temporary file + startup_script_file = tempfile.NamedTemporaryFile(delete=False) + startup_script_file.write(_getStartupScript(name, config)) + startup_script_file.flush() + os.fsync(startup_script_file.fileno()) + + + cmd = ["gcloud", "compute", "--project", config.get("authentication", "project"), + "instances", "create", name, + "--zone", config.get("worker", "zone"), + "--machine-type", config.get("worker", "machinetype"), + "--subnet", config.get("worker", "subnet"), + "--maintenance-policy", config.get("worker", "maintenancepolicy"), + "--service-account", config.get("authentication", "serviceaccount"), + "--scopes", config.get("authentication", "scopes"), + "--image", config.get("worker", "image"), + "--image-project", config.get("worker", "imageproject"), + "--boot-disk-size", config.get("worker", "bootdisksize"), + "--boot-disk-type", config.get("worker", "bootdisktype"), + "--boot-disk-device-name", name, + "--metadata-from-file", "startup-script={}".format(startup_script_file.name),] + if config.getboolean("worker", "preemptible") == True: + cmd.append("--preemptible") + common._run_or_none(cmd) + + +def stopInstance(name, config): + """Run the gcloud command to terminate the instance.""" + zone = config.get("worker", "zone") + cmd = ["gcloud", "compute", "instances", "delete", "--quiet", "--zone", zone, name] + common._run_or_none(cmd) + + +def _getStartupScript(name, config): + """ + Prepare the startup-script in bash script syntax. + Return the script as a string. + """ + + with open(config.get("authentication", "keyfile"), 'r') as f: + key_id_data = f.read() + + with open("cloud/gcloud_worker_startup_script.template", 'r') as f: + template = Template(f.read()) + values = { + "key_id_json": key_id_data, + "hostname": name, + "mount_point": config.get("storage", "mountpoint"), + "bucket_name": config.get("storage", "name"), + "install_dir": config.get("worker", "installdir"), + "coalition_package": config.get("storage", "coalitionpackage"), + "main_program_package": config.get("main_program", "package"), + "main_program_environment": config.get("main_program", "environment"), + "coalition_server_ip": config.get("coalition", "ip"), + "coalition_server_port": config.get("coalition", "port"),} + return template.substitute(values) + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/cloud/gcloud_worker_cloud_init.template b/cloud/gcloud_worker_cloud_init.template new file mode 100644 index 0000000..e46462d --- /dev/null +++ b/cloud/gcloud_worker_cloud_init.template @@ -0,0 +1,35 @@ +#cloud-config + +# This cloud-init template is used for aws workers's startup configuration. +# http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html +# http://cloudinit.readthedocs.io/ + +fqdn: $hostname + +repo_update: true +repo_upgrade: all + +packages: + - python2.7 + - python-pip + - python-twisted + - python-twisted-web + - python-mysqldb + - curl + - s3fs + +runcmd: + - pip install awscli + - AWS_ACCESS_KEY_ID=$access_key AWS_SECRET_ACCESS_KEY=$secret_access_key aws ec2 --region $region create-tags --resources $$(curl http://instance-data/latest/meta-data/instance-id) --tags Key=Name,Value=$hostname + - mkdir -p $mount_point + - chmod a+w $mount_point + - echo $bucket_name:$access_key:$secret_access_key > /etc/passwd-s3fs + - chmod 0640 /etc/passwd-s3fs + - s3fs -o url=https://s3.amazonaws.com,enable_content_md5 $bucket_name $mount_point + - cat $mount_point/$guerilla_render_filename | tar xzf - -C /tmp/ + - mv /tmp/guerillarender/data/usr/local/guerilla /usr/local/bin/ + - rm -rf /tmp/guerillarender + - cat $mount_point/$coalition_filename | tar xzf - -C /tmp/ + - mv /tmp/coalition /usr/local/bin/ + - GUERILLA=/usr/local/bin/guerilla GUERILLA_CLOUD_ROOT=$mount_point /usr/bin/python2.7 /usr/local/bin/coalition/worker.py http://$coalition_server_ip:$coalition_server_port + diff --git a/cloud/gcloud_worker_startup_script.template b/cloud/gcloud_worker_startup_script.template new file mode 100644 index 0000000..802dedf --- /dev/null +++ b/cloud/gcloud_worker_startup_script.template @@ -0,0 +1,48 @@ +#! /bin/bash + +# This template is the google cloud worker startup-script +# https://cloud.google.com/compute/docs/startupscript + +### Set hostname +hostname $hostname + +### Securing instance +systemctl disable ssh +systemctl stop ssh + +### Installing coalition worker requirements +apt-get install -y \ + curl \ + python2.7 \ + python-httplib2 \ + python-configparser \ + python-twisted \ + python-mysqldb \ + python-ldap + +### Saving key-id json file for authentication +cat < /usr/local/share/gcloud-key-id.json +$key_id_json +EOF + +### Installing gcs-fuse +export GCSFUSE_REPO=gcsfuse-`lsb_release -c -s` +echo "deb http://packages.cloud.google.com/apt $$GCSFUSE_REPO main" | tee /etc/apt/sources.list.d/gcsfuse.list +curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +apt-get update +apt-get install -y gcsfuse + +### Mounting bucket +mkdir $mount_point +chmod a+w $mount_point +gcsfuse --implicit-dirs --key-file /usr/local/share/gcloud-key-id.json $bucket_name $mount_point + +### Installing coalition worker +cat $mount_point/$coalition_package | tar xzf - -C $install_dir + +### Installing the main program +cat $mount_point/$main_program_package | tar xzf - -C $install_dir + +### Running coalition worker with main program required environment variables +$main_program_environment /usr/bin/python2.7 /usr/local/bin/coalition/worker.py http://$coalition_server_ip:$coalition_server_port + diff --git a/cloud/qarnot_api.py b/cloud/qarnot_api.py new file mode 100644 index 0000000..4b14bfe --- /dev/null +++ b/cloud/qarnot_api.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +""" +This module provides functions used for qarnot cloud service. +""" + +from cloud import common +import qarnot +import subprocess +import tempfile +from string import Template +from multiprocessing import Process +import os + + +def startInstance(name, config): + """Use qarnot API to start a worker instance. Instances are started by task creation.""" + startup_script_file = tempfile.NamedTemporaryFile(delete=True) + startup_script_file.write(_getStartupScript(name, config)) + startup_script_file.flush() + os.fsync(startup_script_file.fileno()) + startup_script_file.seek(0) + + connection = qarnot.connection.Connection("cloud_qarnot.ini") + # We need internet access and start one instance at time + task = connection.create_task(name, 'docker-network', 1) + task.constants["DOCKER_REPO"] = config.get("worker", "docker_repo") + task.constants["DOCKER_TAG"] = config.get("worker", "docker_tag") + task.constants["DOCKER_HOST"] = common.createWorkerInstanceName(config.get("worker", "nameprefix")) + task.constants["DOCKER_CMD"] = startup_script_file.read() + p = Process(target=task.run) + p.start() + +def stopInstance(name, config): + """Use qarnot API to terminate the instance.""" + + connection = qarnot.connection.Connection("cloud_qarnot.ini") + tasks = connection.tasks() + task = [t for t in tasks if t.name == name][0] + task.delete() + +def _getStartupScript(name, config): + """Build the workers startup script.""" + + with open(config.get("authentication", "keyfile"), 'r') as f: + key_id_data = f.read() + + with open("cloud/qarnot_worker_startup_script.template", 'r') as f: + template = Template(f.read()) + values = { + "key_id_json": key_id_data, + "hostname": name, + "mount_point": config.get("storage", "mountpoint"), + "bucket_name": config.get("storage", "name"), + "install_dir": config.get("worker", "installdir"), + "coalition_package": config.get("storage", "coalitionpackage"), + "main_program_package": config.get("main_program", "package"), + "main_program_environment": config.get("main_program", "environment"), + "coalition_server_ip": config.get("coalition", "ip"), + "coalition_server_port": config.get("coalition", "port"),} + return template.substitute(values) + + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 diff --git a/cloud/qarnot_worker_startup_script.template b/cloud/qarnot_worker_startup_script.template new file mode 100644 index 0000000..fa8d778 --- /dev/null +++ b/cloud/qarnot_worker_startup_script.template @@ -0,0 +1,48 @@ +#! /bin/bash + +# This template is the qarnot cloud worker docker startup-script +# https://computing.qarnot.com/developers/apps/docker + +### Set hostname +hostname $hostname + +### Securing instance +systemctl disable ssh +systemctl stop ssh + +### Installing coalition worker requirements +apt-get install -y \ + curl \ + python2.7 \ + python-httplib2 \ + python-configparser \ + python-twisted \ + python-mysqldb \ + python-ldap + +### Saving key-id json file for authentication +cat < /usr/local/share/gcloud-key-id.json +$key_id_json +EOF + +### Installing gcs-fuse +export GCSFUSE_REPO=gcsfuse-`lsb_release -c -s` +echo "deb http://packages.cloud.google.com/apt $$GCSFUSE_REPO main" | tee /etc/apt/sources.list.d/gcsfuse.list +curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - +apt-get update +apt-get install -y gcsfuse + +### Mounting bucket +mkdir $mount_point +chmod a+w $mount_point +gcsfuse --implicit-dirs --key-file /usr/local/share/gcloud-key-id.json $bucket_name $mount_point + +### Installing coalition worker +cat $mount_point/$coalition_package | tar xzf - -C $install_dir + +### Installing the main program +cat $mount_point/$main_program_package | tar xzf - -C $install_dir + +### Running coalition worker with main program required environment variables +$main_program_environment /usr/bin/python2.7 /usr/local/bin/coalition/worker.py http://$coalition_server_ip:$coalition_server_port + diff --git a/coalition.ini b/coalition.ini deleted file mode 100644 index ae12f58..0000000 --- a/coalition.ini +++ /dev/null @@ -1,123 +0,0 @@ - -[server] -# Server configuration - -# Server port (default is 19211) -#port=19211 - -# Worker time out in seconds, time lapse after a worker missing heartbeats is considered out (default is 10) -#timeout=10 - -# Run the server as service (Windows only) -#service=1 - -# Display verbose logs -#verbose=0 - -# Database autosave timing in seconds -#savetime=300 - -# Database backup timing in seconds -#backuptime=3600 - -# Maximum database backup files -#backupmax=24 - -# Notify the user after the N first children jobs have been finished. 0 disables this notification. -#notifyafter=10 - -# Decrease the priority of a parent job after N errors. -#decreasepriorityafter=10 - -# SMTP server hostname, emails disabled if empty -#smtphost= - -# SMTP server port -#smtpport=587 - -# SMTP use a TLS connection -#smtptls=1 - -# SMTP sender email -#smtpsender= - -# SMTP server login, no authentification if empty -#smtplogin= - -# SMTP server password, no authentification if empty -#smtppasswd= - -# LDAP server to use for authentication -# If not empty, coalition will requier a login and a password at every requests -#ldaphost= - -# LDAP template used to validate the user, like uid=__login__,ou=people,dc=exemple,dc=com -# __login__ will be replaced by the user login -#ldaptemplate= - -# In LDAP mode, list if the trusted users who don t need a password -#trustedusers=trustedUser1 -# trustedUser2 -# trustedUser3 - -# Command while list. The command white list can be global or per user. -#commandwhitelist=global command1 regexp -# global command2 regexp -# @user1 -# user1 command1 regexp -# user1 command2 regexp -# @user2 -# user2 command1 regexp -# user2 command2 regexp - -# Command while list. The command white list can be global or per user. -#commandwhitelist=global command1 regexp - -[worker] -# Worker configuration - -# Server URL, like http://serverhost:19211, let it blank to use autodetection by broadcasting. -#serverUrl= - -# Number of simultaneous workers on this system (default is 1) -#workers=1 - -# Worker name (default is host name) -#name=MyWorker - -# Sleep time between two heartbeats in seconds (default is 5) -#sleep=5 - -# Maximum number of cpus per worker, will override the number of workers when defined (Windows only) -#cpus=None - -# Command to execute at worker startup -#startup= - -# Display verbose logs -#verbose=0 - -# Customize the command used to run the job. -# -# The command set in runcommand is responsible for : -# * changing the user -# * changing the working directory -# * run the job command with the current environment -# -# The following pattern will be replaced: -# __user__ : the job user name -# __dir__ : the job directory -# __cmd__ : the job command -# -# If runcommand is blank, the worker set the working directory to the job directory -# and run the command using the worker permissions. - -# Default run command -#runcommand= - -# Run the jobs using sudo -#runcommand=sudo -u __user__ -E -- sh -c 'cd __dir__; __cmd__' - -# Workers log file -#logfile=./worker.log - diff --git a/coalition.version b/coalition.version deleted file mode 100644 index 5174c53..0000000 --- a/coalition.version +++ /dev/null @@ -1 +0,0 @@ -3.16 diff --git a/control.py b/control.py index 1d82848..d7b4155 100644 --- a/control.py +++ b/control.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import sys, getopt, urllib, httplib, re global cmd, serverUrl, dir, title, action,id @@ -47,10 +50,10 @@ def usage(): usage() sys.exit(2) serverUrl = args[0] - while serverUrl[-1] == '/': - serverUrl = serverUrl[:-1] + while serverUrl[-1] == '/': + serverUrl = serverUrl[:-1] action = args[1] -except getopt.GetoptError, err: +except getopt.GetoptError as err: # print help information and exit: print str(err) # will print something like "option -a not recognized" usage() @@ -139,3 +142,6 @@ def output (str): conn.close() else: print("I don't know what to do with myself. Use another action") + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/db.py b/db.py new file mode 100644 index 0000000..5f30887 --- /dev/null +++ b/db.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- + +import time + + +class DB(object): + def __init__(self): + self.IntoWith = False + + '''Enter a transaction block''' + def __enter__(self): + + self.Jobs = {} + self.Worker = {} + + # Those map are the edits done on every objects to commit at the end of the transaction + self.JobsToUpdate = {} + self.WorkersToUpdate = {} + + self.IntoWith = True + + '''Leave a transaction block''' + def __exit__ (self, type, value, traceback): + self.IntoWith = False + if not isinstance(value, TypeError): + self.editJobs(self.JobsToUpdate) + self.editWorkers(self.WorkersToUpdate) + + def getRoot (self): + return Job (self, 0, 0, "Root", "", "", "", "", "", 0, 0, 0, 0, 0, 0, 0, "", "", 0, 0, 0, 0, 0, 0, 0, "", "", "") + + +class Worker(object): + ''' + The database proxy object for a worker + + This object is readonly outside a transaction block. + ''' + def __init__ (self, db, values): + self.db = db + self.name = values['name'] + self.Data = values + # Should not exist in the cache + assert (db.Workers.get (self.name) == None) + # Cache it + db.Workers[self.name] = self + + def __setattr__(self, attr, value): + # Backup the value for delayed writting + db = super (object, self).__getattr__ ('db') + name = super (object, self).__getattr__ ('name') + data = super (object, self).__getattr__ ('data') + if not db.IntoWith: + raise Exception + w = db.WorkerToUpdate.get (name) + if not w: + w = {} + db.WorkersToUpdate[name] = w + w[attr] = value + data[attr] = value + + def __getattr__(self, attr): + data = super (object, self).__getattr__ ('data') + return data[attr] + + +class Job(object): + ''' + The database proxy object for a job + + This object is readonly outside a transaction block. + ''' + def __init__ (self, db, values): + self.db = db + self.id = values['id'] + self.Data = values + # Should not exist in the cache + assert (db.Jobs.get (self.id) == None) + # Cache it + db.Jobs[self.id] = self + + def __setattr__(self, attr, value): + # Backup the value for delayed writting + db = super (object, self).__getattr__ ('db') + id = super (object, self).__getattr__ ('id') + data = super (object, self).__getattr__ ('data') + if not db.IntoWith: + raise Exception + w = db.WorkerToUpdate.get (id) + if not w: + w = {} + db.WorkersToUpdate[id] = w + w[attr] = value + data[attr] = value + + def __getattr__(self, attr): + data = super (object, self).__getattr__ ('data') + return data[attr] + + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/db_mysql.py b/db_mysql.py new file mode 100644 index 0000000..1b24640 --- /dev/null +++ b/db_mysql.py @@ -0,0 +1,38 @@ +import sys +import MySQLdb +from db_sql import DBSQL + +class DBMySQL(DBSQL): + + # The Context class allows using context capsules for the sql transactions + # Note: this behaviour has disappeared from MySQLdb as of 2018 + class Context: + def __init__ (self, db, conn): + self.DB = db + self.Conn = conn + self.Conn.ping(True) + self.Conn.autocommit = False + + def __enter__(self): + pass + + def __exit__ (self, type, value, traceback): + if type is None: + self.Conn.commit () + else: + if self.DB.Verbose: + sys.stdout.flush () + sys.stdout.write ("[SQL] Warning: db context exited with an exception, rollback!\n") + sys.stdout.flush () + self.Conn.rollback () + + def cursor (self): + return self.Conn.cursor () + + + def __init__ (self, host, user, password, database, **kwargs): + self.config = kwargs["config"] + self.cloudconfig = kwargs["cloudconfig"] + self.Conn = self.Context (self, MySQLdb.connect(host, user, password, database)) + # super is called *after* because DBSQL inits stuffs in the DB + super(DBMySQL, self).__init__() diff --git a/db_sql.py b/db_sql.py new file mode 100644 index 0000000..c88aa56 --- /dev/null +++ b/db_sql.py @@ -0,0 +1,1300 @@ +# -*- coding: utf-8 -*- +import unittest, time, re, sys +from db import DB +from importlib import import_module +import cloud +from cloud.common import createWorkerInstanceName +from textwrap import dedent +import os + + +def convdata (d): + return isinstance(d, str) and repr (d) or (isinstance(d, bool) and (d and '1' or '0') or (isinstance(d, unicode) and repr(str(d)) or str(d))) + + +class LdapError(Exception): + """Error class for LDAP exceptions.""" + def __init__(self, value): + self.value = value + def __str__(self): + return "[LDAP] Error: {value}".format(value=self.value) + + +class DBSQL(DB): + + def __init__ (self): + self.StartTime = time.time () + self.lastworkerinstancestarttime = 0 + self.LastUpdate = 0 + self.EnterTime = 0 + self.RunTime = 0.0 + self.HeartBeats = 0 + self.PickJobs = 0 + self.Verbose = False + self.NotifyFinished = None + self.NotifyError = None + self.Workers = dict() + self.AffinityBitsToName = dict() + + tables = self._getDatabaseTables() + + if (("Workers",) in tables) or (("workers",) in tables): + self._populateWorkersCache() + + if (("Affinities",) in tables) or (("affinities",) in tables): + self._populateAffinitiesTable() + + def __enter__(self): + self.EnterTime = time.time () + self.Conn.__enter__ () + + def __exit__ (self, type, value, traceback): + self.RunTime = time.time ()-self.EnterTime + if not isinstance(value, TypeError): + self._update () + self.Conn.__exit__ (type, value, traceback) + + def _execute (self, cur, req, data=None): + now = time.time () + if data: + cur.execute (req, data) + else: + cur.execute (req) + after = time.time () + if self.Verbose: + sys.stdout.flush () + sys.stdout.write ("[SQL] (%f/%f) %s\n" % (now-self.StartTime, after-now, req)) + sys.stdout.flush () + + def _rowAsDict (self, cur, row): + if row: + result = {} + for idx, col in enumerate (cur.description): + result[col[0]] = row[idx] + return result + else: + return None + + def _populateWorkersCache(self): + """Populate cache with pre-existent data in Workers table.""" + cur = self.Conn.cursor () + self._execute (cur, "SELECT name FROM Workers") + for worker in cur: + info = {} + info['ping_time'] = int (time.time ()) + info['cpu'] = '' + info['free_memory'] = 0 + info['total_memory'] = 0 + info['ip'] = '' + info['timeout'] = False + self.Workers[worker[0]] = info + + def _populateAffinitiesTable(self): + """Populate Affinities table with pre-existent data having an id < 64.""" + cur = self.Conn.cursor () + with self.Conn: + affinities = {} + self._execute (cur, "SELECT id, name FROM Affinities") + for row in cur: + affinities[int (row[0])] = row[1] + for i in range (1, 64): + if not i in affinities: + self._execute (cur, "INSERT INTO Affinities (id, name) VALUES (%d,'')" % i) + + def _getLdapPermission(self, action): + """Check ldap permissions. + + If ldap is not configured, ldap_user is unset, return "". + If ldap user is allowed to do the action, return the additional sql filter + or "" if the user has global permissions. + If ldap user is not allowed, return False.""" + + if not hasattr(self, "ldap_user") or not self.ldap_user: # LDAP is not set up in configuration or ldapunsafeapi is set to True + return "" + if action == "addjob": + if self.permissions["ldaptemplateaddjobglobal"]: + return "" + elif self.permissions["ldaptemplateaddjob"]: + return "AND user='{user}'".format(user=self.ldap_user) + else: + raise LdapError("Action '{action}' is not permitted for user '{user}'".format(action=action, user=self.ldap_user)) + return False + elif action == "viewjob": + if self.permissions["ldaptemplateviewjobglobal"]: + return "" + elif self.permissions["ldaptemplateviewjob"]: + return "AND user='{user}'".format(user=self.ldap_user) + else: + raise LdapError("Action '{action}' is not permitted for user '{user}'".format(action=action, user=self.ldap_user)) + return False + elif action == "editjob": + if self.permissions["ldaptemplateeditjobglobal"]: + return "" + elif self.permissions["ldaptemplateeditjob"]: + return "AND user='{user}'".format(user=self.ldap_user) + else: + raise LdapError("Action '{action}' is not permitted for user '{user}'".format(action=action, user=self.ldap_user)) + return False + elif action == "deletejob": + if self.permissions["ldaptemplatedeletejobglobal"]: + return "" + elif self.permissions["ldaptemplatedeletejob"]: + return "AND user='{user}'".format(user=self.ldap_user) + else: + raise LdapError("Action '{action}' is not permitted for user '{user}'".format(action=action, user=self.ldap_user)) + return False + else: + raise LdapError("Action '{action}' is not defined for user '{user}'".format(action=action, user=self.ldap_user)) + return False + + def listJobs (self): + cur = self.Conn.cursor () + self._execute (cur, "SELECT * FROM Jobs") + for row in cur: + print (self._rowAsDict (cur, row)) + + def listUnpausedWaitingJobs(self): + """Get jobs currently waiting for a worker.""" + cur = self.Conn.cursor () + req = "SELECT * FROM Jobs WHERE state = 'WAITING' and paused = 0" + self._execute (cur, req) + return [self._rowAsDict (cur, row) for row in cur] + + def listWorkers (self): + cur = self.Conn.cursor () + self._execute (cur, "SELECT * FROM Workers") + for row in cur: + print (row) + + def listWorkersByStates(self, state, *argv): + cur = self.Conn.cursor () + req = "SELECT * FROM Workers WHERE state = '%s'" % state + if argv: + for arg in argv: + req += " OR state = '%s'" % arg + self._execute (cur, req) + return [self._rowAsDict (cur, row) for row in cur] + + def listAffinities (self): + cur = self.Conn.cursor () + self._execute (cur, "SELECT id, name FROM Affinities") + aff = {} + for row in cur: + if row[1] != "" and row[0] >= 1 and row[0] <= 63: + aff[row[1]] = (1L << (row[0]-1)) + return aff + + def getAffinities (self): + cur = self.Conn.cursor () + self._execute (cur, "SELECT id, name FROM Affinities") + aff = {} + for row in cur: + if row[0] >= 1 and row[0] <= 63: + aff[row[0]] = row[1] + return aff + + def setAffinities (self, affinities): + # reset affinities cache + self.AffinityBitsToName = {} + cur = self.Conn.cursor () + for id, affinity in affinities.iteritems (): + self._execute (cur, "UPDATE Affinities SET name = '%s' WHERE id = %d" % (affinity, int (id))) + + def getAffinityMask (self, affinities): + if affinities == "": + return 0 + aff = self.listAffinities () + mask = 0L + cur = self.Conn.cursor () + for affinity in affinities.split (","): + if affinity != "": + m = re.match(r"^#(\d+)$", affinity) + if m: + bit = (int(m.group (1))-1) + mask = mask | (1L << bit) + else: + mask = mask | aff[affinity] + return mask + + def getAffinityString (self, affinity_bits): + if affinity_bits == 0: + return "" + if affinity_bits in self.AffinityBitsToName: + return self.AffinityBitsToName[affinity_bits] + names = [] + aff = self.getAffinities() + for id, name in aff.iteritems (): + bit = (1L << (id-1)) + if affinity_bits & bit != 0: + if name != '': + names.append (name) + else: + names.append ("#"+ str (id)) + names.sort () + result = ",".join (names) + self.AffinityBitsToName[affinity_bits] = result + return result + + def newJob(self, parent, title, command, dir, environment, state, paused, timeout, + priority, affinity, user, url, progress_pattern, dependencies = None): + ldap_perm = self._getLdapPermission("addjob") + if ldap_perm is False: + return None + if ldap_perm != "": # User can add job owned by himself, force user value + user = self.ldap_user + cur = self.Conn.cursor() + self._execute(cur, + "SELECT h_depth, h_affinity, h_priority, h_paused, command " + "FROM Jobs " + "WHERE id = {parent} {ldap_perm}".format(parent=parent, ldap_perm=ldap_perm)) + data = cur.fetchone() + paused = 0 + if state == "PAUSED": + paused = 1; + if data is None: + data = [-1, 0, 0, 0, ''] + if data[4] != '': + print("Error: can't add job, parent {parent} is not a group".format(parent=parent)) + return None + # one depth below + h_depth = data[0]+1 + # merge parent affinities with child affinities + parent_affinities = data[1] + child_affinities = self.getAffinityMask(affinity) + h_affinity = parent_affinities | child_affinities + # merge priority + priority = max(0, min(255, int(priority))) + h_priority = data[2] + (priority << (56-h_depth*8)) + h_paused = data[3] or paused + + self._execute (cur, + "INSERT INTO Jobs (" + "parent, title, command, dir, environment, timeout," + "priority, affinity, affinity_bits, user, url," + "progress_pattern, paused, state, worker, h_depth," + "h_affinity, h_priority, h_paused" + ") VALUES (" + "{parent}, {title}, {command}, {directory}, {environment}, {timeout}," + "{priority}, {affinity}, {child_affinities}, {user}, {url}," + "{progress_pattern}, {paused}, {state}, {worker}, {h_depth}," + "{h_affinity}, {h_priority}, {h_paused})".format(parent=parent, title=repr(title), command=repr(command), + directory=repr(dir), environment=repr(environment), timeout=timeout, + priority=priority, affinity=repr(affinity), child_affinities=child_affinities, + user=repr(user), url=repr(url), progress_pattern=repr(progress_pattern), + paused="'"+str(paused)+"'", state="'WAITING'", worker="''", h_depth=int(h_depth), + h_affinity=h_affinity, h_priority=int(h_priority), h_paused="'"+str(h_paused)+"'")) + + data = cur.fetchone () + job = self.getJob (cur.lastrowid) + if job is not None and dependencies is not None: + self.setJobDependencies (job['id'], dependencies) + self._updateJobCounters (parent) + job['dependencies'] = dependencies + return job + + def getJob(self, id): + ldap_perm = self._getLdapPermission("viewjob") + if ldap_perm is False: + return None + print("LDAP_PERM", ldap_perm) + cur = self.Conn.cursor() + self._execute(cur, + "SELECT * FROM Jobs " + "WHERE id = {id} {ldap_perm}".format(id=id, ldap_perm=ldap_perm)) + result = self._rowAsDict(cur, cur.fetchone()) + if result is not None: + if result['paused']: + result['state'] = str("PAUSED") + if result['state'] == "WORKING" and result['total'] == 0: + current_time = int(time.time ()) + result['duration'] = current_time - result['start_time'] + result['affinity'] = self.getAffinityString(result['affinity_bits']) + # get dependencies + result['dependencies'] = [] + self._execute(cur, + "SELECT job.id FROM Jobs AS job " + "INNER JOIN Dependencies AS dep " + "ON job.id = dep.dependency " + "WHERE dep.job_id = {id}".format(id=id)) + for row in cur: + result['dependencies'].append(row[0]) + return result + + def getJobChildren(self, id, data): + ldap_perm = self._getLdapPermission("viewjob") + if ldap_perm is False: + return None + cur = self.Conn.cursor() + self._execute(cur, + "SELECT * FROM Jobs " + "WHERE parent = {id} {ldap_perm}".format(id=id, ldap_perm=ldap_perm)) + jobs = [] + for row in cur: + result = self._rowAsDict (cur, row) + if result and result['paused']: + result['state'] = str ("PAUSED") + if result['state'] == "WORKING" and result['total'] == 0: + current_time = int (time.time ()) + result['duration'] = current_time - result['start_time'] + result['affinity'] = self.getAffinityString (result['affinity_bits']) + jobs.append (result) + return jobs + + def getJobDependencies(self, id): + ldap_perm = self._getLdapPermission("viewjob") + if ldap_perm is False: + return None + cur = self.Conn.cursor() + self._execute (cur, + "SELECT job.* FROM Jobs AS job " + "INNER JOIN Dependencies AS dep " + "ON job.id = dep.dependency " + "WHERE dep.job_id = {id} {ldap_perm}".format(id=id, ldap_perm=ldap_perm)) + rows = cur.fetchall() + return [self._rowAsDict (cur, row) for row in rows] + + def getCountJobsWhere(self, where_clause=''): + """Get the number of matching jobs.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT COUNT(*) FROM Jobs WHERE {}".format(where_clause[0])) + return cur.fetchone()[0] + + def getJobsWhere(self, where_clause='', index_min=0, index_max=1): + """Get Jobs via a readonly SQL request.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT * FROM Jobs WHERE {} LIMIT {},{}".format(where_clause, index_min, index_max)) + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getJobsUsers(self): + """Get users.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT DISTINCT user FROM Jobs ORDER BY user") + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getJobsStates(self): + """Get States.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT DISTINCT state FROM Jobs") + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getJobsWorkers(self): + """Get States.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT DISTINCT worker FROM Jobs") + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getJobsPriorities(self): + """Get States.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT DISTINCT priority FROM Jobs") + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getJobsAffinities(self): + """Get States.""" + cur = self.Conn.cursor() + self._execute(cur, "SELECT DISTINCT affinity FROM Jobs") + return [self._rowAsDict (cur, row) for row in cur.fetchall()] + + def getChildrenDependencyIds (self, id): + cur = self.Conn.cursor () + self._execute (cur, "SELECT job.id AS id, dep.dependency AS dependency FROM Dependencies AS dep " + "INNER JOIN Jobs AS job ON job.id = dep.job_id " + " WHERE job.parent = %d" % id) + + def getChildrenDependencyIds(self, id): + cur = self.Conn.cursor() + self._execute(cur, """ + SELECT job.id AS id, dep.dependency AS dependency + FROM Dependencies AS dep + INNER JOIN Jobs AS job + ON job.id = dep.job_id + WHERE job.parent = {id} + """.format(id=id)) + rows = cur.fetchall() + return [self._rowAsDict(cur, row) for row in rows] + + def getWorker (self, hostname): + cur = self.Conn.cursor () + self._execute (cur, "SELECT * FROM Workers WHERE name = '%s'" % hostname) + worker = self._rowAsDict (cur, cur.fetchone ()) + try: + info = self.Workers[hostname] + worker['ping_time'] = info['ping_time'] + worker['cpu'] = info['cpu'] + worker['free_memory'] = info['free_memory'] + worker['total_memory'] = info['total_memory'] + except: + pass + + self._execute (cur, "SELECT affinity FROM WorkerAffinities WHERE worker_name = '%s'" % ( hostname ) ) + affinities = [] + + data = cur.fetchone() + + if data is None: + worker['affinity'] = "" + return worker + + for data in cur: + affinities.append( self.getAffinityString( data[0] ) ) + + worker['affinity'] = "\n".join( affinities ) + return worker + + def getWorkerStartTime(self, name): + """Get the number of seconds since epoch.""" + cur = self.Conn.cursor () + self._execute(cur, "SELECT start_time FROM Workers WHERE name = '%s'" % name) + db_type = self._getDatabaseType() + if db_type == "mysql": + start_time = cur.fetchone()[0].timetuple() + else: + start_time = time.strptime(cur.fetchone()[0], '%Y-%m-%d %H:%M:%S') + return time.mktime(start_time) + + def getWorkers (self): + cur = self.Conn.cursor () + self._execute (cur, "SELECT * FROM Workers") + workers = [] + for row in cur: + worker = self._rowAsDict (cur, row) + try: + info = self.Workers[worker['name']] + worker['ping_time'] = info['ping_time'] + worker['cpu'] = info['cpu'] + worker['free_memory'] = info['free_memory'] + worker['total_memory'] = info['total_memory'] + except: + pass + + req = self.Conn.cursor() + self._execute( req, "SELECT affinity FROM WorkerAffinities WHERE worker_name = '%s'" % ( worker['name'] ) ) + affinities = [] + + for d in req: + + affinities.append( self.getAffinityString( d[0] ) ) + + worker['affinity'] = "\n".join( affinities ) + worker['start_time'] = self.getWorkerStartTime(worker['name']) + workers.append (worker) + return workers + + def getEvents (self, job, worker, howlong): + cur = self.Conn.cursor() + req = "SELECT * FROM Events WHERE start > %d" % (int(time.time())-howlong) + if worker: + req += " AND worker=%s" % convdata (worker) + if job > 0: + req += " AND job_id=%d" % job + self._execute (cur, req); + return [self._rowAsDict (cur, row) for row in cur.fetchall ()] + + def editJobs (self, jobs): + ldap_perm = self._getLdapPermission("editjob") + if ldap_perm is False: + return None + cur = self.Conn.cursor () + for id, attr in jobs.iteritems (): + if attr.has_key("user") and attr["user"].lower() != self.ldap_user.lower() and ldap_perm != "": + # User has no global permission, so he can change his own jobs only + raise LdapError("User '{user}' is not allowed to change job id='{id}' user attribute to '{user_attr}'".format( + user=self.ldap_user, id=id, user_attr=attr["user"])) + break + toUpdate = [k+"="+convdata(v) for k,v in attr.iteritems() + if k != 'dependencies' and k != 'affinity' and k != 'priority' and + k != 'state' and k != 'parent'] + if toUpdate: + req = "UPDATE Jobs SET " + ",".join (toUpdate) + " WHERE id=" + str(id) + self._execute(cur, req) + cur.fetchall() + # Special cases + if attr.get ('paused') is not None: + paused = attr.get ('paused') + if paused: + self.pauseJob (int (id)) + else: + self.startJob (int (id)) + if attr.get ('state'): + state = attr.get ('state') + if state == 'PAUSED': + self.pauseJob (int (id)) + elif state == 'WAITING': + self.startJob (int (id)) + else: + self._setJobState (int (id), state, True) + updateChildren = False + if attr.get ('parent') is not None: + self.moveJob (int (id), int (attr['parent'])) + if attr.get ('affinity') is not None: + self.setJobAffinity (int (id), attr['affinity']) + if attr.get ('priority'): + self.setJobPriority (int (id), attr['priority']) + if attr.get ('parent') is not None or attr.get ('affinity') is not None or attr.get ('priority') is not None or attr.get ('paused') is not None: + self._updateChildren (int (id)) + if attr.get ('dependencies'): + dependencies = attr['dependencies'] + if type(dependencies) is str: + # Parse the dependencies string + dependencies = re.findall ('(\d+)', dependencies) + ids = [] + for i, dep in enumerate (dependencies) : + try: + ids.append (int (dep)) + except: + pass + self.setJobDependencies (int (id), ids) + self._setJobState (int (id), None, True) + + def editWorkers (self, workers): + cur = self.Conn.cursor () + for name, attr in workers.iteritems (): + hasField = False + req = "UPDATE Workers SET" + for k, v in attr.iteritems(): + if k != 'affinity': + hasField = True + req += " " + k + " = " + convdata (v) + req += " WHERE name = '" + name + "'" + if hasField: + self._execute(cur, req) + cur.fetchall() + if attr.get ('affinity') is not None: + self.setWorkerAffinity (str (name), attr['affinity']) + + def setJobProgress (self, jobId, progress): + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Jobs SET progress = %f WHERE id = %d" % (progress, jobId)) + + def _getDatabaseType(self): + """Get the database type.""" + return self.config.get("server", "db_type") + + def _getDatabaseTables(self): + """Return list of database tables.""" + cur = self.Conn.cursor() + db_type = self._getDatabaseType() + if db_type == "mysql": + req = "SHOW TABLES;" + else: + req = "SELECT name FROM sqlite_master WHERE type = 'table';" + self._execute(cur, req) + return cur.fetchall() + + def _getDatabaseVersion(self): + """Return database version.""" + cur = self.Conn.cursor() + tables = self._getDatabaseTables() + if (not ("Migrations",) in tables) and (not ("migrations",) in tables): + current_version = [("0000",)] + else: + req = "SELECT database_version FROM Migrations;" + self._execute(cur, req) + current_version = cur.fetchall() + return int(current_version[0][0]) + + def _getMigrationVersion(self): + """Return latest migration version.""" + return int(max([re.sub(r'_.*$', '', f) for f in + os.walk("migrations").next()[2]])) + + def _getDatabaseDataCount(self): + datacount = 0 + cur = self.Conn.cursor() + for table in self._getDatabaseTables(): + req = "SELECT COUNT(*) FROM {}".format(table[0]) + self._execute(cur, req) + fetched = cur.fetchone() + if fetched: + datacount += fetched[0] + return datacount + + def initDatabase(self): + """Initialize the database.""" + if len(self._getDatabaseTables()): + if self._getDatabaseDataCount() != 0: + print("The database is not empty, it will not be initialized.") + return False + print("Initializing database.") + return self.migrateDatabase(init=True) + + def migrateDatabase(self, init=False): + """Migrate the database.""" + db_type = self._getDatabaseType() + current = self._getDatabaseVersion() + target = self._getMigrationVersion() + if init: + # Init with the '0000' migration + current -= 1 + cur = self.Conn.cursor() + print("The database version is {current} and the migration target is {target}. Migrating.".format(current=current, target=target)) + while current < target: + current += 1 + migration_module_name = "{current:04d}_db_{db_type}".format(current=current, db_type=db_type) + migration_module = import_module("migrations.{}".format(migration_module_name)) + with self.Conn: + for step in migration_module.steps: + self._execute(cur, step.strip()) + if init: + with self.Conn: + for i in range(1, 64): + self._execute(cur, dedent(""" + INSERT INTO Affinities (id, name) + VALUES ('{}', '')""".format(i))) + return True + + def moveJob (self, jobId, parent): + cur = self.Conn.cursor () + self._execute (cur, "SELECT parent FROM Jobs WHERE id = %d" % jobId) + previous = cur.fetchone () + self._execute (cur, "UPDATE Jobs SET parent = %d WHERE id = %d" % (parent, jobId)) + self._updateJobCounters (previous[0]) + self._updateJobCounters (parent) + + def setJobAffinity (self, id, affinity): + cur = self.Conn.cursor () + affinities = self.getAffinityMask (affinity) + self._execute (cur, "UPDATE Jobs SET affinity = '%s', affinity_bits = %d WHERE id = %d" % (affinity, affinities, id)) + + def setJobPriority (self, id, priority): + cur = self.Conn.cursor () + priority = max (0, min (255, int (priority))) + self._execute (cur, "UPDATE Jobs SET priority = %d WHERE id = %d" % (priority, id)) + + def setJobDependencies (self, id, dependencies): + cur = self.Conn.cursor () + self._execute (cur, "DELETE FROM Dependencies WHERE job_id = %d" % int (id)) + for dep in dependencies: + self._execute (cur, "INSERT INTO Dependencies (job_id,dependency) " + "VALUES (%d,%d)" % (int (id), int (dep))) + self._setJobState (int (id), None, True) + + def resetJob (self, id, updateChildren = True): + ldap_perm = self._getLdapPermission("editjob") + if ldap_perm is False: + return None + if ldap_perm != "": # Not a global permission + cur = self.Conn.cursor () + self._execute(cur, "SELECT user FROM Jobs WHERE id={id}".format(id=id)) + data = cur.fetchone() + if data and data[0].lower() != self.ldap_user.lower(): + raise LdapError("User '{user}' is not allowed to reset job id='{id}'".format(user=user,id=id)) + return None + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Jobs SET start_time = 0 WHERE id = %d" % id) + self._setJobState (id, "WAITING", False) + self._execute (cur, "SELECT id FROM Jobs WHERE parent = %d" % id) + for row in cur: + self.resetJob (row[0], False) + if updateChildren: + self._resetJobCounters (id) + + def resetErrorJob (self, id, updateChildren = True): + ldap_perm = self._getLdapPermission("editjob") + if ldap_perm is False: + return None + if ldap_perm != "": # Not a global permission + cur = self.Conn.cursor () + self._execute(cur, "SELECT user FROM Jobs WHERE id={id}".format(id=id)) + data = cur.fetchone() + if data and data[0].lower() != self.ldap_user.lower(): + raise LdapError("User '{user}' is not allowed to reset error job id='{id}'".format(user=user,id=id)) + return None + cur = self.Conn.cursor () + self._execute (cur, "SELECT state FROM Jobs WHERE id = %d" % id) + data = cur.fetchone () + if data is not None and data[0] == "ERROR": + self._execute (cur, "UPDATE Jobs SET start_time = 0 WHERE id = %d" % id) + self._setJobState (id, "WAITING", False) + self._execute (cur, "SELECT id FROM Jobs WHERE parent = %d" % id) + for row in cur: + self.resetErrorJob (row[0], False) + if updateChildren: + self._resetJobCounters (id) + + def startJob (self, id): + ldap_perm = self._getLdapPermission("editjob") + if ldap_perm is False: + return None + if ldap_perm != "": # Not a global permission + cur = self.Conn.cursor () + self._execute(cur, "SELECT user FROM Jobs WHERE id={id}".format(id=id)) + data = cur.fetchone() + if data and data[0].lower() != self.ldap_user.lower(): + raise LdapError("User '{user}' is not allowed to start job id='{id}'".format(user=user,id=id)) + return None + + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Jobs SET paused = 0 WHERE id = %d" % id) + self._setJobState (id, "WAITING", False) + self._updateChildren (id) + self._updateJobCounters (id) + + def pauseJob (self, id): + ldap_perm = self._getLdapPermission("editjob") + if ldap_perm is False: + return None + if ldap_perm != "": # Not a global permission + cur = self.Conn.cursor () + self._execute(cur, "SELECT user FROM Jobs WHERE id={id}".format(id=id)) + data = cur.fetchone() + if data and data[0].lower() != self.ldap_user.lower(): + raise LdapError("User '{user}' is not allowed to pause job id='{id}'".format(user=user,id=id)) + return None + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Jobs SET paused = 1 WHERE id = %d" % id) + self._setJobState (id, "PAUSED", False) + self._updateChildren (id) + self._updateJobCounters (id) + + def deleteJob (self, id, deletedJobs = [], updateCounters = True): + ldap_perm = self._getLdapPermission("deletejob") + if ldap_perm is False: + return None + if ldap_perm != "": # Not a global permission + cur = self.Conn.cursor () + self._execute(cur, "SELECT user FROM Jobs WHERE id={id}".format(id=id)) + data = cur.fetchone() + if data and data[0].lower() != self.ldap_user.lower(): + raise LdapError("User '{user}' is not allowed to delete job id='{id}'".format(user=user,id=id)) + return None + + cur = self.Conn.cursor () + self._execute (cur, "SELECT id FROM Jobs WHERE parent = %d" % id) + for row in cur: + self.deleteJob (row[0], deletedJobs, False) + parent = None + if updateCounters: + self._execute (cur, "SELECT parent FROM Jobs WHERE id = %d" % id) + parent = cur.fetchone () + self._execute (cur, "DELETE FROM Jobs WHERE id = %d" % id) + # clean up Events? + #self._execute (cur, "DELETE FROM Events WHERE job_id = %d" % id) + deletedJobs.append (id) + if parent is not None: + self._updateJobCounters (parent[0]) + + def newWorker (self, name): + cur = self.Conn.cursor () + self._execute (cur, "INSERT INTO Workers (name,ip,affinity, state,finished," + "error,last_job,current_event,cpu,free_memory,total_memory,active) " + "VALUES ('%s','','','WAITING',0,0,-1,-1,'[0]',0,0,1)" % name) + + def setWorkerAffinity (self, name, affinity): + cur = self.Conn.cursor () + # Delete all the worker's affinities + self._execute( cur, "DELETE FROM WorkerAffinities WHERE worker_name = '%s'" % ( name ) ) + + if len( affinity ) > 0: + + affinities = affinity.split( "\n" ) + + for index, aff in enumerate( affinities ): + + query = "INSERT INTO WorkerAffinities ( worker_name, affinity, ordering ) VALUES( '%s', %d, %d )" % ( name, self.getAffinityMask( aff ), index+1 ) + self._execute( cur, query ) + + def stopWorker (self, name): + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Workers SET active = 0 WHERE name = '%s'" % name) + self._execute (cur, "SELECT job.id FROM Jobs AS job " + "INNER JOIN Workers AS worker ON " + "worker.last_job = job.id AND worker.name = job.worker " + "WHERE worker.name = '%s' AND job.state = 'WORKING'" % name) + row = cur.fetchone () + if row is not None: + self._setJobState (row[0], "WAITING", True) + + def startWorker (self, name): + cur = self.Conn.cursor () + self._execute (cur, "UPDATE Workers SET active = 1 WHERE name = '%s'" % name) + + def deleteWorker (self, name): + cur = self.Conn.cursor () + self._execute (cur, "DELETE FROM Workers WHERE name = '%s'" % name) + try: + del self.Workers[name] + except: + pass + + def _updateWorkerInfo (self, hostname, cpu, free_memory, total_memory, ip): + try: + info = self.Workers[hostname] + except: + info = {} + self.Workers[hostname] = info + info['ping_time'] = int (time.time ()) + info['cpu'] = cpu + info['free_memory'] = free_memory + info['total_memory'] = total_memory + info['ip'] = ip + info['timeout'] = False + return info + + # Worker heartbeats while running a job + # Lookup for worker and job + # update worker and job + def heartbeat (self, hostname, jobId, cpu, free_memory, total_memory, ip): + self.HeartBeats += 1 + current_time = int(time.time()) + cur = self.Conn.cursor () + + self._updateWorkerInfo (hostname, cpu, free_memory, total_memory, ip) + + _query = ("SELECT w.active, w.state, j.state FROM Workers as w " + "INNER JOIN Jobs AS j ON " + "j.worker = w.name AND j.id = %d AND w.last_job = %d AND " + "w.state = 'WORKING' AND j.state = 'WORKING' and j.h_paused = 0 " + "WHERE w.name = '%s'" % (jobId, jobId, hostname)) + self._execute (cur, _query) + data = cur.fetchone () + + if data: + return True + + # slow path here + # either worker doesn't exist or job is not assigned to the worker or job was pause + # get the worker active and state + self._execute (cur, "SELECT active, state FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone () + if worker is None: + # create worker if needed + self.newWorker (hostname) + self._execute (cur, "SELECT active, state FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone () + + # by default we're suspicious and we flag the worker as waiting + state = "WAITING" + job = None + if worker[0] == True: + self._execute (cur, "SELECT state, h_paused FROM Jobs WHERE id = %d AND worker = '%s'" % (jobId, hostname)) + job = cur.fetchone () + if job is not None and job[0] == "WORKING" and not job[1]: + # if the worker is active and is running the job, it's all good + # we just lost track of the worker (deleteWorker) and we just need + # to update them + self._setWorkerState (hostname, "WORKING") + return True + + # something is not right! + # reset the worker to WAITING + self._setWorkerState (hostname, "WAITING") + # and if the job exists, reset it to WAITING as well + if job is not None: + self._setJobState (jobId, "WAITING", True) + return False + + def pickJob (self, hostname, cpu, free_memory, total_memory, ip): + self.PickJobs += 1 + current_time = int(time.time()) + cur = self.Conn.cursor() + + self._updateWorkerInfo(hostname, cpu, free_memory, total_memory, ip) + + # get the worker active and state + self._execute(cur, "SELECT active, state, last_job FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone() + if worker is None: + self.newWorker(hostname) + self._execute(cur, "SELECT active, state, last_job FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone() + + # check the worker is not already working + # this can happen if the worker crashed and restarted before + # timeout is detected + if worker[1] == "WORKING": + # reset all working jobs assigned to this worker + self._execute(cur, "SELECT id FROM Jobs WHERE state = 'WORKING' and worker = '%s'" % hostname) + for job in cur: + self._setJobState(job[0], "WAITING", True) + + # worker is not active, drop now + if not worker[0]: + return -1,"","","",None + + # Here, we have an INNER JOIN query + # Fetch the FIRST job whose affinity match the worker's first affinity in the list (stored in WorkerAffinities) + self._execute(cur, dedent(""" + SELECT J.id, J.title, J.command, J.dir, J.user, J.environment + FROM Jobs AS J + INNER JOIN WorkerAffinities AS W + ON (( J.h_affinity & W.affinity = J.h_affinity ) & ( J.h_affinity != 0 )) + WHERE W.worker_name = '{}' + AND J.state = 'WAITING' + AND NOT J.h_paused + AND J.command != '' + ORDER BY W.ordering ASC, J.h_priority DESC, J.id ASC LIMIT 1""".format(hostname))) + + job = cur.fetchone() # This instruction is redundant because there is a LIMIT 1 in the query + + # At this point, the job will be set to None IF : + # * There is no Worker whose affinity match any Job affinity + # * A job has no affinity + # The former case is EXPECTED, but not the latter one + # Therefore, we need to add a query that take the first Job that has no affinity WHEN Workers are not doing anything + if job is None: + self._execute(cur, dedent(""" + SELECT id, title, command, dir, user, environment + FROM Jobs + WHERE state = 'WAITING' + AND NOT h_paused + AND affinity = '' + AND command != '' + ORDER BY h_priority DESC, id ASC LIMIT 1""")) + + job = cur.fetchone () + + # Finally, return nothing if there is no job. + if job is None: + # Update worker state + self._execute (cur, "UPDATE Workers SET state = 'WAITING' WHERE name = '{}'".format(hostname)) + return -1, "", "", "", None + + # update the job and worker + id = job[0] + + # create a new event + self._execute (cur, "INSERT INTO Events (worker, job_id, job_title, state, start, duration) " + "VALUES (%s, %d, %s, 'WORKING', %d, %d)" % + (convdata (hostname), job[0], convdata (job[1]), + current_time, 0)) + cur.fetchone () + eventid = cur.lastrowid + + self._execute (cur, "UPDATE Jobs SET worker = '%s', start_time = %d, duration = 0, progress = 0.0 " + "WHERE id = %d" % (hostname, current_time, id)) + self._execute (cur, "UPDATE Workers SET last_job = %d, state = 'WORKING', current_event = %d " + "WHERE name = '%s'" % (id, eventid, hostname)) + + self._setJobState (id, "WORKING", True) + + if job[4] != None and job[4] != "": + return job[0], job[2], job[3], job[4], job[5] + else: + return job[0], job[2], job[3], "", job[5] + + def endJob (self, hostname, jobId, errorCode, ip): + current_time = int(time.time()) + cur = self.Conn.cursor () + self._execute (cur, "SELECT active, current_event FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone () + if worker is None: + self.newWorker (hostname) + self._execute (cur, "SELECT active, current_event FROM Workers WHERE name = '%s'" % hostname) + worker = cur.fetchone () + + self._execute (cur, "SELECT state, start_time FROM Jobs WHERE id = %d AND worker = '%s' AND state = 'WORKING'" % (jobId, hostname)) + job = cur.fetchone () + if job is not None: + state = (errorCode != 0) and "ERROR" or "FINISHED" + # update event + start_time = job[1] + self._execute (cur, "UPDATE Events SET state = %s, duration = %d WHERE id = %d" % + (convdata (state), current_time-start_time, worker[1])) + self._setJobState (jobId, state, True) + self._setWorkerState (hostname, state) + + def _isJobPending (self, id): + cur = self.Conn.cursor () + self._execute (cur, "SELECT COUNT(job.id) FROM Jobs AS job " + "INNER JOIN Dependencies AS dep ON job.id = dep.dependency " + "WHERE dep.job_id = %d AND job.state != 'FINISHED'" % id) + result = cur.fetchone () + return (result[0] > 0) + + def _updateDependentJobsState (self, id): + cur = self.Conn.cursor () + self._execute (cur, "SELECT job.id FROM Jobs AS job " + "INNER JOIN Dependencies AS dep ON job.id = dep.job_id " + "WHERE dep.dependency = %d" % id) + for dependent in cur: + self._setJobState (dependent[0], None, True) + + # update the job state + # also check dependencies, mark pending in this case + # if None is passed as state, assumes previous state + def _setJobState (self, id, state, updateCounters): + current_time = int(time.time()) + cur = self.Conn.cursor () + self._execute (cur, "SELECT state, parent, user, title, id FROM Jobs WHERE id = %d" % id) + job = cur.fetchone () + if job is not None: + jobdict = self._rowAsDict (cur, job) + # passed None, use previous state + if state is None: + state = job[0] + # job set to waiting/pending, check dependencies first + if state == "WAITING" or state == "PENDING": + state = self._isJobPending (id) and "PENDING" or "WAITING" + # changing status? + if state != job[0]: + if state == "FINISHED" and self.NotifyFinished: + self.NotifyFinished (jobdict) + elif state == "ERROR" and self.NotifyError: + self.NotifyError (jobdict) + _set = "state = '%s'" % state + if state == "FINISHED" or state == "ERROR": + _set += ", duration = %d-start_time" % current_time + _set += ", run_done = run_done+1" + self._execute (cur, "UPDATE Jobs SET "+_set+" WHERE id = %d" % id) + self._updateDependentJobsState (id) + self._updateChildren (id) + if updateCounters: + self._updateJobCounters (job[1]) + + # recompute the whole job hierarchy counters + def _resetJobCounters (self, id, updateParent = True): + if id != 0: + cur = self.Conn.cursor () + self._execute (cur, "SELECT id FROM Jobs WHERE parent = %d" % id) + for child in cur: + self._resetJobCounters (child[0], False) + self._updateJobCounters (id, updateParent) + + # update this job and its parent counters + def _updateJobCounters (self, id, updateParent = True): + if id != 0: + current_time = int(time.time()) + cur = self.Conn.cursor () + total = 0 + working = 0 + errors = 0 + finished = 0 + total_working = 0 + total_errors = 0 + total_finished = 0 + start_time = 0 + duration = 0 + self._execute (cur, "SELECT state, total_working, total_errors, total_finished, total, start_time, duration FROM Jobs WHERE parent = %d" % id) + for job in cur: + state = job[0] + if job[4] == 0: + total += 1 + if state == 'WORKING': + working += 1 + elif state == 'ERROR': + errors += 1 + elif state == 'FINISHED': + finished += 1 + total_working += job[1] + total_errors += job[2] + total_finished += job[3] + total += job[4] + if job[5] != 0: + if start_time == 0: + start_time = job[5] + else: + start_time = min (start_time, job[5]) + if state == 'ERROR' or state == 'FINISHED': + duration += job[6] + elif state == 'WORKING': + duration += (current_time - job[5]) + total_working += working + total_errors += errors + total_finished += finished + # update job counters! + # note that we also update the start_time as the minimum of + # all children start times + _set = ("working = %d, errors = %d, finished = %d, " + "total_working = %d, total_errors = %d, total_finished = %d, " + "total = %d" % (working, errors, finished, total_working, + total_errors, total_finished, total)) + if total > 0: + _set += ", start_time = %d, duration = %d" % (start_time, duration) + self._execute (cur, "UPDATE Jobs SET " + _set + (" WHERE id = %d" % id)) + if total > 0: + self._execute (cur, "SELECT state, parent, user, title, id, progress FROM Jobs WHERE id = %d" % id) + oldState = cur.fetchone () + jobdict = self._rowAsDict (cur, oldState) + newState = "WAITING" + if total_errors > 0: + newState = "ERROR" + elif total_finished == total: + newState = "FINISHED" + elif total_working > 0: + newState = "WORKING" + if newState != oldState[0]: + # parent job is finished! + # update the duration now! + if newState == "WAITING" or newState == "PENDING": + newState = self._isJobPending (id) and "PENDING" or "WAITING" + self._execute (cur, "UPDATE Jobs SET state = '%s' WHERE id = %d" % (newState, id)) + # and send notification + if newState == "FINISHED" and self.NotifyFinished: + self.NotifyFinished (jobdict) + elif newState == "ERROR" and self.NotifyError: + self.NotifyError (jobdict) + # no longer pending, unpause children + if newState == "WAITING" and oldState[0] == "PENDING": + self._updateChildren (id) + # finished job, update dependent jobs + if newState == "FINISHED": + self._updateDependentJobsState (id) + progress = float (total_finished) / total + if progress != oldState[5]: + self._execute (cur, "UPDATE Jobs SET progress = %f WHERE id = %d" % (progress, id)) + + if updateParent: + self._execute (cur, "SELECT parent FROM Jobs WHERE id = %d" % id) + parent = cur.fetchone () + if parent is not None: + self._updateJobCounters (parent[0]) + + # update the worker state + # if passing an error state, increase counters + def _setWorkerState (self, hostname, state): + cur = self.Conn.cursor () + self._execute (cur, "SELECT state FROM Workers AS worker WHERE name = '%s'" % hostname) + worker = cur.fetchone () + if worker is not None and worker[0] != state: + if state == "ERROR": + self._execute (cur, "UPDATE Workers SET state = 'WAITING', error = error+1 WHERE name = '%s'" % hostname) + elif state == "TIMEOUT": + self._execute (cur, "UPDATE Workers SET state = 'TIMEOUT', error = error+1 WHERE name = '%s'" % hostname) + elif state == "FINISHED": + self._execute (cur, "UPDATE Workers SET state = 'WAITING', finished = finished+1 WHERE name = '%s'" % hostname) + else: + self._execute (cur, "UPDATE Workers SET state = '%s' WHERE name = '%s'" % (state, hostname)) + + # update children hierarchical values, such as h_priority, h_affinity, h_paused + def _updateChildren (self, id, parenth = None): + cur = self.Conn.cursor () + self._execute (cur, "SELECT parent, affinity_bits, priority, paused, state FROM Jobs WHERE id = %d" % id) + job = cur.fetchone () + if job: + if not parenth: + self._execute (cur, "SELECT h_depth, h_affinity, h_priority, h_paused FROM Jobs WHERE id = %d" % job[0]) + parenth = cur.fetchone () or (-1, 0, 0, False) + h_depth = parenth[0]+1 + h_affinity = parenth[1] | job[1] + h_priority = parenth[2] + (job[2] << (56-h_depth*8)) + if parenth[3] or job[3] or job[4] == "PENDING": + h_paused = 1 + else: + h_paused = 0 + self._execute (cur, "UPDATE Jobs SET h_depth = %d, h_affinity = %d, h_priority = %d, h_paused = %d " + "WHERE id = %d" % (h_depth, h_affinity, h_priority, h_paused, id)) + self._execute (cur, "SELECT id FROM Jobs WHERE parent = %d" % id) + jobh = [h_depth,h_affinity,h_priority,h_paused] + for child in cur: + self._updateChildren (child[0], jobh) + + def _update (self): + current_time = int(time.time()) + # update timeout jobs no more than every 10 seconds + if current_time - self.LastUpdate >= 10: + load = self.RunTime / (current_time - self.LastUpdate) + if self.Verbose: + print ("[STAT] %d heartbeats, %d pickjobs, load %f" % (self.HeartBeats, self.PickJobs, load)) + self.HeartBeats = 0 + self.PickJobs = 0 + self.LastUpdate = current_time + self.RunTime = 0 + cur = self.Conn.cursor () + timeout = 60 + + # find all working jobs that are running out of time *or* + # all working jobs which worker is timing out + self._execute (cur, "SELECT id, worker FROM Jobs " + "WHERE state = 'WORKING' AND command != '' AND " + "(timeout != 0 AND %d-start_time > timeout)" % + current_time) + for job in cur: + print ("Job %d timeout!" % job[0]) + self._setJobState (job[0], "ERROR", True) + self._setWorkerState (job[1], "TIMEOUT") + + for worker in self.Workers: + info = self.Workers[worker] + if current_time - info['ping_time'] > timeout and not info['timeout']: + # worker timeout! + info['timeout'] = True + self._execute (cur, "SELECT last_job FROM Workers WHERE name = '%s' AND state = 'WORKING'" % worker) + data = cur.fetchone () + if data is not None: + self._setJobState (data[0], "WAITING", True) + if self.getWorker(worker)['state'] == "TERMINATED": + # State TERMINATED is more explicit than TIMEOUT for terminated instances + pass + else: + self._setWorkerState (worker, "TIMEOUT") + + # If cloud mode has been set via "servermode" option + if self.cloudconfig: + cloudprovider = self.config.get('server', 'servermode') + # Dynamic module loading for configured provider + self.cloudmanager = import_module('cloud.{}'.format(cloudprovider)) + waitingjobs = self.listUnpausedWaitingJobs() + if len(waitingjobs): + self._manageWorkerInstanceStart(current_time, + waitingjobs) + else: + self._manageWorkerInstanceTerminate(current_time) + + def _manageWorkerInstanceStart(self, current_time, waitingjobs): + """ + Manage worker starting. A new worker is started if the start + delay is reached, if there are more waiting jobs than available + workers and the maximum number of instances has not been reached. + Create an instance via the cloud provider module, create a + worker reference in the coalition DB and update the delay + timestamp. + """ + if current_time - self.lastworkerinstancestarttime < int( + self.cloudconfig.get("coalition", "workerinstancestartdelay")): + return + availableworkers = self.listWorkersByStates("STARTING", "WORKING", "WAITING") + if len(waitingjobs) > len(availableworkers) and len(availableworkers) < int( + self.cloudconfig.get("coalition", "workerinstancemax")): + name = createWorkerInstanceName( + self.cloudconfig.get("worker", "nameprefix")) + self.cloudmanager.startInstance(name, self.cloudconfig) + self.newWorker(name) + self._setWorkerState(name, 'STARTING') + self.lastworkerinstancestarttime = current_time + if self.Verbose: + print("[CLOUD] Starting new instance %s" % name) + + def _manageWorkerInstanceTerminate(self, current_time): + """ + Manage worker termination. Worker instances are terminated if + they are not working and they have been living for at least the + number of second defined by "workerinstancestopdelay". Terminate + via the cloud provider module, update the coalition DB reference. + """ + uselessworkers = self.listWorkersByStates( + "STARTING", "WAITING", "TIMEOUT") + if len(uselessworkers): + for worker in uselessworkers: + name = worker["name"] + lastworkerstarttime = self.getWorkerStartTime(name) + if lastworkerstarttime and ( + current_time - lastworkerstarttime > int( + self.cloudconfig.get("coalition", + "workerinstanceminimumlifetime"))): + self._setWorkerState(name, "TERMINATED") + self.cloudmanager.stopInstance(name, self.cloudconfig) + if self.Verbose: + print("[CLOUD] Terminating instance %s" % name) + + def requiresMigration(self): + """ + Check if database requires migration. + Returns a boolean. + """ + return self._getDatabaseVersion() < self._getMigrationVersion() + + def reset (self): + cur = self.Conn.cursor () + self._execute (cur, "DELETE FROM Jobs"); + self._execute (cur, "DELETE FROM Workers"); + self._execute (cur, "DELETE FROM Dependencies"); + self._execute (cur, "DELETE FROM Events"); + self._execute (cur, "DELETE FROM Affinities"); + self._execute (cur, "DELETE FROM WorkerAffinities"); + print("[SQL] Database has been reset.") + exit(0) + + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/db_sqlite.py b/db_sqlite.py new file mode 100644 index 0000000..7c47a49 --- /dev/null +++ b/db_sqlite.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +import sqlite3 +from db_sql import DBSQL + + +class DBSQLite(DBSQL): + def __init__(self, database, **kwargs): + self.config = kwargs["config"] + self.cloudconfig = kwargs["cloudconfig"] + self.Conn = sqlite3.connect(database) + + super(DBSQLite, self).__init__() + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/releasenote.txt b/doc/CHANGELOG.txt similarity index 90% rename from releasenote.txt rename to doc/CHANGELOG.txt index 3fda056..e8836cc 100644 --- a/releasenote.txt +++ b/doc/CHANGELOG.txt @@ -1,75 +1,82 @@ -3.17 -Added : A trusted user list in LDAP mode -Added : Global command white list : restrict the job commands for all the users -Added : User command white list : restrict the job commands by users -Changed : User can't change the username of a job in LDAP mode anymore - -3.16 -Changed : usesudo and usesu have been replaced by a custom user run command. See the runcommand option in coalition.ini for details. - -3.15 -Fixed : hierarchical affinities are working - -3.14 -Added : worker log file path in coalition.ini -Changed : reset job clear the job logs -Changed : better worker logs -Added : workers IP address visible in the web interface - -3.13 -Fixed : Optional environment would replace the worker environment, loosing previous values -Added : Now use sudo instead of su -Fixed : Recursive environment variables were not properly expanded - -3.12 -Added : Optional environment to override the worker environment - -3.11 -Added : umask 022 for the worker - -3.10 -Fixed : Patch for the darwin plateform (by Peter Postma) -Changed : Background saving of the database, no more freeze during the save -Changed : The file format has been changed to support background saving -Added : Reset error jobs command - -3.9 -Added : Better log progress parsing -Fixed : All job attributes available in control.py - -3.8 -Added : Emails sent to the user -Added : activities list -Fixed : it is now possible to add a jobs in any parent job from the ui -Fixed : parent job state is now correct -Fixed : sort by progress works -Added : sum of jobs -Fixed : month in date was wrong -Added : show the average duration -Changed : sequentiel mode (no more random) -Added : cut and paste jobs -Added : URL link per job - -3.7 -Fixed : bad json code generation with big numbers - -3.6 -Added : command line worker on windows -Added : the database is backuped -Added : working column -Added : start time column -Added : CSV export of the jobs -Changed : if the database is corrupted, the server quits with an error -Fixed : variable timeout was not properly read from the config file - -3.2 -Changed : No psutil anymore, was crashing at service start - -3.1 -Fixed : Install MS dll - -3.0 -Changed : Major rework of the JS GUI -Added : Progression of jobs is shown -Changed : Best multi-cores monitoring -Added : Memory monitoring +4.0 +First version using a SQL database as storage. +Uses SQLite by default, and provides a mysql driver (see coalition.ini). +The server provides now a REST api. +A phyton module is also available in /api to warp the REST api in some sugar. + + +3.17 +Added : A trusted user list in LDAP mode +Added : Global command white list : restrict the job commands for all the users +Added : User command white list : restrict the job commands by users +Changed : User can't change the username of a job in LDAP mode anymore + +3.16 +Changed : usesudo and usesu have been replaced by a custom user run command. See the runcommand option in coalition.ini for details. + +3.15 +Fixed : hierarchical affinities are working + +3.14 +Added : worker log file path in coalition.ini +Changed : reset job clear the job logs +Changed : better worker logs +Added : workers IP address visible in the web interface + +3.13 +Fixed : Optional environment would replace the worker environment, loosing previous values +Added : Now use sudo instead of su +Fixed : Recursive environment variables were not properly expanded + +3.12 +Added : Optional environment to override the worker environment + +3.11 +Added : umask 022 for the worker + +3.10 +Fixed : Patch for the darwin plateform (by Peter Postma) +Changed : Background saving of the database, no more freeze during the save +Changed : The file format has been changed to support background saving +Added : Reset error jobs command + +3.9 +Added : Better log progress parsing +Fixed : All job attributes available in control.py + +3.8 +Added : Emails sent to the user +Added : activities list +Fixed : it is now possible to add a jobs in any parent job from the ui +Fixed : parent job state is now correct +Fixed : sort by progress works +Added : sum of jobs +Fixed : month in date was wrong +Added : show the average duration +Changed : sequentiel mode (no more random) +Added : cut and paste jobs +Added : URL link per job + +3.7 +Fixed : bad json code generation with big numbers + +3.6 +Added : command line worker on windows +Added : the database is backuped +Added : working column +Added : start time column +Added : CSV export of the jobs +Changed : if the database is corrupted, the server quits with an error +Fixed : variable timeout was not properly read from the config file + +3.2 +Changed : No psutil anymore, was crashing at service start + +3.1 +Fixed : Install MS dll + +3.0 +Changed : Major rework of the JS GUI +Added : Progression of jobs is shown +Changed : Best multi-cores monitoring +Added : Memory monitoring diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..6f1a1f3 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,89 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Coalition.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Coalition.qhc" + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/build.sh b/doc/build.sh new file mode 100755 index 0000000..8bb781c --- /dev/null +++ b/doc/build.sh @@ -0,0 +1,5 @@ +#/usr/bash + +# Can't set the environment directly in the makefile under windows. +export PYTHONPATH=../api +make html \ No newline at end of file diff --git a/doc/source/cloud_mode.rst b/doc/source/cloud_mode.rst new file mode 100644 index 0000000..3f67070 --- /dev/null +++ b/doc/source/cloud_mode.rst @@ -0,0 +1,221 @@ +========== +Cloud mode +========== + +In this setup, the coalition server is allowed to start and delete instances so that all the jobs get done with minimum costs. Here, we install the coalition server on a dedicated cloud instance (instead of locahost). This way we simplify the network setup as we don't need a VPN or VLAN. + +Configuration +============= + +Amazon Cloud +------------ + +First, an initial setup is required on the cloud provider side. We provide here a minimal working setup. It can of course be enriched by your specificid needs and policy. + +1. **Amazon account** + + To be allowed to manage cloud instances (ie. starting and terminating), the coalition server needs authentication. + + - from you amazon cloud account, visit the section *Manage security credentials* + - get an access Keys (ID and Secret key) as text file + + You might prefer to create a dedicated user instead of your global user account. + +2. **Virtual Private Cloud (VPC)** + + For the workers and the server to communicate securely, we use a common VPC: + + - create a new VPC + +3. **Security Groups** + + The coalition's server and workers communication port defaults to 19211. The server should be accessible by the user (to interact with the API and/or web frontend) and from workers. The server can be hosted in the office or in the cloud, according to your network policy. Here, the server is instanciated in the cloud, belongs to the security group *sg-coalition* and the workers belong to the security group *sg-worker*. The workers should be accessible from the server only. So, the security groups and inbound rules are like those: + + - create a security group *sg-coalition* + + - Inbound Rules: TCP 19211 sg-worker + - Inbound Rules: TCP 19211 office-public-IP + + - create security group *sg- worker* + + - Inbound Rules: TCP 19211 sg-coalition + +4. **Setup Coalition server as a cloud instance** + +Now that the cloud provider has been set up, the coalition server has to be configured accordingly. + + - install a coalition server on a cloud instance as explained in *Installation* documentation page + - edit the file **coalition.ini** in the **[Server]** section and set:: + + servermode = aws + + - copy the file **_cloud_aws.ini** to **cloud_aws.ini** + - edit the file **cloud_aws.ini** + +The configuration file **cloud_aws.ini** is self-explanatory. Set the options with your own amazon parameters: + +.. include:: ../../_cloud_aws.ini + :literal: + +5. **Bucket** + + As workers are instanciated on demand, they need to fetch startup configuration files somewhere. Besides, as the workers might produce some data files (for example in a renderfarm usecase), those files must be saved in a filer. We create a bucket for that: + + - create a bucket + - prepare the startup configuration files in the bucket + + - create a directory **srv** + - copy the coalition source code into the **srv** directory: + + - download `coalition source code `_ as a zip file (or use the git source you got while installing the server) + - unzip the file + - copy **_coalition.ini** into **coalition.ini** and edit the **[worker]** section + - recompress and pack it as a tar compressed file + - copy **coalition.tar.gz** to the bucket: **srv/coalition.tar.gz** + + - in this setup, we build a `guerilla render `_ cloud renderfarm, so the worker needs the guerilla render binary: + + - copy **guerilla_render_2.0.0a13_linux64.tar.gz** to **srv/guerilla_render_2.0.0a13_linux64.tar.gz** + +Google cloud +------------ + +1. **Google cloud account** + + - login on `google cloud console ` + - create a new project eg. **guerilla-cloud** + - get the json key file for the service account (menu IAM & Admin > Service accounts > Options > Create keys) + +2. **Networking** + + We want to be able to visit the coalition server web frontend, so we need to allow remote connection from our office. + + - add a firewall rule allowing office IP on port tcp:19211 + +3. **Setup Coalition server as a google cloud instance** + +Now that the cloud provider has been set up, the coalition server has to be configured accordingly. + + - install a coalition server in a compute cloud instance as explained in *Installation* documentation page + + - as the server will create and delete cloud instances, set the instance **access scope** to **Allow full acees to all Cloud APIs** + - use a dedicated IP instead of an ephemeral one for permanent reachability + - ssh access for copying coalition files can be done via google credentials:: + + ssh -i ~/.ssh/google_compute_engine + + - edit the file **coalition.ini** in the **[Server]** section and set:: + + servermode = gcloud + + - copy the file **_cloud_gcloud.ini** to **cloud_gcloud.ini** + - edit the file **cloud_gcloud.ini** + +The configuration file **cloud_gcloud.ini** is self-explanatory. Set the options with your own google parameters: + +.. include:: ../../_cloud_gcloud.ini + :literal: + +4. **Storage** + + As workers are instanciated on demand, they need to fetch startup configuration files somewhere. Besides, as the workers might produce some data files (for example in a renderfarm usecase), those files must be saved in a filer. We create a bucket for that: + + - create a bucket + - prepare the startup configuration files in the bucket + + - create a directory **srv** + - copy the coalition source code into the **srv** directory: + + - download `coalition source code `_ as a zip file (or use the git source you got while installing the server) + - unzip the file + - copy the service user json key file into the coalition directory + - copy **_coalition.ini** into **coalition.ini** and edit the **[worker]** section + - recompress and pack it as a tar compressed file + - copy **coalition.tar.gz** to the bucket: **srv/coalition.tar.gz** + + - in this setup, we build a `guerilla render `_ cloud renderfarm, so the worker needs the guerilla render binary: + + - copy **guerilla_render_2.0.0a13_linux64.tar.gz** to **srv/guerilla_render_2.0.0a13_linux64.tar.gz** + +Running coalition +================= +The coalition server is now ready to manage workers in the cloud: + + - start the server + - visit the web interface **http://:19211** + - add affinities + - add some jobs + +Workers will automagically be instanciated, getting jobs, working and terminated according to the configuration until there are no more jobs in waiting state on the server. + +Changing coalition server or worker configuration while running +--------------------------------------------------------------- +On the server instance, edit the concerned configuration files **coalition.py** and **cloud_.py** and restart the server. + +As the configuration for workers is set up in the **bucket**, edit the configuration file **coalition.py** and re-upload the **coalition.tar.gz** to the bucket. Newly started instances will immediately use the new configuration. You might want to manually terminate previous instances. The coalition server does not need restarting in this case since the file names in the bucket are unchanged. + +Monitoring the cloud deployment +=============================== +The coalition server limits the number of simultaneous instances to the configuration parameter **workerinstancemax** in **coalition.ini**. But if there is a configuration problem (for instance in the workers starting scripts located in the bucket), coalition server might not be reached by the workers. In this case, coalition server will keep starting instances. So, as long as the configuration is not confirmed, you are advised to check in your cloud provider console the the effective number of starting instances. Some limits can also be setup directly in the cloud provider preventing any excessive cloud usage. + +On the web frontend, in the workers tab, clicking the button **Terminate** destroys the selected instances after confirmation. + +Additional documentation for programmers +======================================== + +python cloud module +------------------- + +cloud.common +'''''''''''' + +.. automodule:: cloud.common + :members: + +cloud.aws +''''''''' + +.. automodule:: cloud.aws + :members: + +Amazon specific templates +------------------------- + +cloud/aws_worker_cloud_init.template +'''''''''''''''''''''''''''''''''''' + +.. include:: ../../cloud/aws_worker_cloud_init.template + :literal: + +cloud/aws_worker_spot_launchspecification.json.template +''''''''''''''''''''''''''''''''''''''''''''''''''''''' + +.. include:: ../../cloud/aws_worker_spot_launchspecification.json.template + :literal: + +.. + Google cloud specific templates + ------------------------------- + + cloud/gcloud_worker_cloud_init.template + ''''''''''''''''''''''''''''''''''''''' + + .. include:: ../../cloud/glcoud_worker_cloud_init.template + + + Qarnot specific templates + ------------------------- + + cloud/qarnot_worker_cloud_init.template + ''''''''''''''''''''''''''''''''''''''' + + .. include:: ../../cloud/qarnot_worker_cloud_init.template + + + IBM cloud specific templates + ---------------------------- + cloud/ibm_worker_cloud_init.template + '''''''''''''''''''''''''''''''''''' + + .. include:: ../../cloud/ibm_worker_cloud_init.template + diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..8c48236 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# +# Coalition documentation build configuration file, created by +# sphinx-quickstart on Mon Jan 18 10:12:25 2016. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.append(os.path.abspath('../../..')) +sys.path.append(os.path.abspath('../..')) + +# -- General configuration ----------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinxcontrib.httpdomain'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Coalition' +copyright = u'2016, Mercenaries Engineering SARL' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '4.0' +# The full version, including alpha/beta/rc tags. +release = '4.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. Major themes that come with +# Sphinx are currently 'default' and 'sphinxdoc'. +#html_theme = 'default' +html_theme = 'sphinx_rtd_theme' + + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'coalition' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'Coalition.tex', u'Coalition Documentation', + u'Mercenaries Engineering SARL', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/doc/source/contribute.rst b/doc/source/contribute.rst new file mode 100644 index 0000000..95f537d --- /dev/null +++ b/doc/source/contribute.rst @@ -0,0 +1,31 @@ +Contribute +========== + +Development platform +-------------------- + +Coalition is `free software LGPL licensed `_ and `hosted on github `_. Feel free to participate via a github account. + +Running tests +------------- + +The test suite requires a database. To prevent a database overwriting, you should **first backup your current database or change the database reference in the coalition.ini** configuration file. + +Run:: + + # Intialize a fresh database + python server.py --init + + # Run the tests + python tests/main_tests.py + +The status of the tests must show no errors. + +A **.travis.yml** file is provided for automated testing via `travis testing platform `_. + +Build documentation +------------------- + +Go to the **coalition/doc** directory and run:: + + ./build.sh diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..1b634de --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,20 @@ +Coalition documentation +======================= + +Welcome to *"Coalition, a small but beautiful task manager"* documentation page. We hope you will find here all the information you need. If it's not the case, or if you want to contribute, you may contact the project's maintainers on the `the project development platform `_. + +.. toctree:: + :maxdepth: 3 + + Introduction + Installation + Cloud mode + Web interface + Python API + REST API + Usage + Contribute + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/source/installation.rst b/doc/source/installation.rst new file mode 100644 index 0000000..4e80ed2 --- /dev/null +++ b/doc/source/installation.rst @@ -0,0 +1,139 @@ +============ +Installation +============ + +Installing the server +===================== +.. _installing a server: + +The *Coalition server* can be installed on a localhost or on a remote host. Please remember that communication between server and workers is not encrypted, so if the server is installed on localhost and workers on remote machines, using a VPN or a VLAN is a good idea. + +*Coalition* works with python2.7. + +Debian like, Ubuntu, etc. via system packages +--------------------------------------------- + +Logged as a priviledged user, in a shell prompt, run:: + + apt-get install -y \ + python2.7 \ + python-httplib2 \ + python-configparser \ + python-twisted \ + python-mysqldb \ + python-ldap \ + python-sphinx \ + python-sphinxcontrib-httpdomain + + cd /usr/local/bin + git clone https://github.com/MercenariesEngineering/coalition.git + cd coalition + cp _coalition.ini coalition.ini + +Edit the section *[server]* in the file *coalition.ini* according to your needs. + +You may want to fine tune the installation using: + + - a dedicated system user and group to isolate the process and file ownership; + - a `systemd service definition file `_; + - any system service monitoring daemon. + +Via pip, the python package manager +----------------------------------- + +Using a `python virtual environment `_ is advised in this case, although not mandatory. + +Logged as a priviledged user, in a shell prompt, run:: + + cd /usr/local/bin + git clone https://github.com/MercenariesEngineering/coalition.git + cd coalition + pip install -r requirements.txt + cp _coalition.ini coalition.ini + +Edit the section *[server]* in the file *coalition.ini* according to your needs. + +You may want to fine tune the installation using: + + - a dedicated system user and group to isolate the process and file ownership; + - a `systemd service definition file `_; + - any system service monitoring daemon. + +coalition.ini configuration file +-------------------------------- +This configuration file contains two sections: **[server]** that will be used in server mode, and **[worker]** that will be used while running in worker mode. + +.. include:: ../../_coalition.ini + :literal: + +Cloud mode +---------- + +A coalition server must be installed and the cloud provider needs configuration. See *cloud mode documentation page* for details. + +Database +-------- + +A database must be setup for the coalition server. To initialize it the firest time, run:: + + python server.py --verbose --init + +The database can be reset on demand. All data are lost:: + + python server.py --verbose --reset + +See also the next section about migrations. + +Update coalition to a new release +--------------------------------- + +If you update the coalition source code with a more recent coalition release, the new coalition features may need a database schema update. If it's the case, you will be informed by a message while trying to run the server:: + + python server.py --verbose --init + # ... + # The database requires migration + +In this case, you should use the *--migrate* option to explicitely reconfigure the database:: + + python server.py --verbose --init + # ... + # Migration was sucessful + +Running the server +------------------ +When all has been set up, run:: + + python server.py + +To see available command line arguments, run:: + + python server.py --help + +On windows, use one those options:: + + python server.py --console + python server.py --service + + +Installing a worker +=================== + +The same procedure than above in `installing a server`_ applies, except for configuration and running. + +Configuration +------------- +Edit the section *[worker]* of the configuration file *coalition.ini* according to your needs. + +You may want to fine tune the installation using: + + - a dedicated system user and group to isolate the process and file ownership; + - a `systemd service definition file `_; + - any system service monitoring daemon. + +Running a worker +---------------- +Run:: + + python worker.py --verbose + + diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst new file mode 100644 index 0000000..32a3d2a --- /dev/null +++ b/doc/source/introduction.rst @@ -0,0 +1,5 @@ +Introduction +============ + +.. include:: ../../README.rst + diff --git a/doc/source/python_api.rst b/doc/source/python_api.rst new file mode 100644 index 0000000..28e1847 --- /dev/null +++ b/doc/source/python_api.rst @@ -0,0 +1,6 @@ +Python API +========== + +.. automodule:: coalition + :members: + :show-inheritance: diff --git a/doc/source/rest_api.rst b/doc/source/rest_api.rst new file mode 100644 index 0000000..4687316 --- /dev/null +++ b/doc/source/rest_api.rst @@ -0,0 +1,701 @@ +REST API +======== + +The coalition server provides a REST API using json data. + +Jobs +**** + +.. http:get:: /api/jobs + + Returns the root jobs (with parent=0). + + **Example request**: + + .. sourcecode:: http + + GET /api/jobs HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "id": 123, + "parent": 0, + "title": "Job 123", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + }, + { + "id": 124, + "parent": 0, + "title": "Job 124", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + } + ] + + :statuscode 200: no error + :statuscode 500: error + +.. http:get:: /api/jobs/(int:id) + + Returns the job (`id`) object. + + **Example request**: + + .. sourcecode:: http + + GET /api/jobs/123 HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id": 123, + "parent": 0, + "title": "Job 123", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + } + + :statuscode 200: no error + :statuscode 500: error + + +.. http:put:: /api/jobs + + Create a job. Returns the new job id. + + **Example request**: + + .. sourcecode:: http + + PUT /api/jobs HTTP/1.1 + Host: localhost + + { + "parent": 0, + "title": "Job 1", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "url": "http://localhost/image.png", + "progress_pattern": "#%percent" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 123 + + :statuscode 200: no error + :statuscode 500: error + + +.. http:post:: /api/jobs + + Modify the jobs properties. + + **Example request**: + + .. sourcecode:: http + + POST /api/jobs HTTP/1.1 + Host: localhost + + { + 123: + { + "title": "Job renamed 123", + "command": "echo renamed", + }, + 124: + { + "title": "Job renamed 124", + "command": "echo renamed", + } + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + :statuscode 200: no error + :statuscode 500: error + + +.. http:get:: /api/jobs/(int:id)/children + + Returns the job (`id`) children objects. + + **Example request**: + + .. sourcecode:: http + + GET /api/jobs/123/children HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "id": 124, + "parent": 123, + "title": "Job 124", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + }, + { + "id": 125, + "parent": 123, + "title": "Job 125", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + }, + ] + +.. http:get:: /api/jobs/(int:id)/dependencies + + Returns the job objects on which the job (`id`) depends. + + **Example request**: + + .. sourcecode:: http + + GET /api/jobs/123/dependencies HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "id": 124, + "parent": 0, + "title": "Job 124", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + }, + { + "id": 125, + "parent": 0, + "title": "Job 125", + "command": "echo test", + "dir": ".", + "environment": "", + "state": "WAITING", + "worker": "worker-1", + "start_time": 123456, + "duration": 12, + "ping_time": 123456, + "run_done": 1, + "retry": 10, + "timeout": 10000, + "priority": 1000, + "affinity": "LINUX", + "user": "render", + "finished": 0, + "errors": 0, + "working": 0, + "total": 10, + "total_finished": 0, + "total_errors": 0, + "total_working": 0, + "url": "http://localhost/image.png", + "progress": 0.5, + "progress_pattern": "#%percent" + }, + ] + +.. http:post:: /api/jobs/(int:id)/dependencies + + Set the job (`id`) dependencies. + + **Example request**: + + .. sourcecode:: http + + POST /api/jobs/123/dependencies HTTP/1.1 + Host: localhost + + [124,125] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + +.. http:get:: /api/jobs/(int:id)/log + + Returns the job (`id`) log file. + + **Example request**: + + .. sourcecode:: http + + GET /api/jobs/123/log HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + "Job 123 done" + + :statuscode 200: no error + :statuscode 500: error + + +.. http:delete:: /api/jobs + + Delete the jobs. + + **Example request**: + + .. sourcecode:: http + + DELETE /api/jobs HTTP/1.1 + Host: localhost + + [123,124,125] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +.. http:post:: /api/resetjobs + + Reset the jobs. The job status is set to 'WAITING', all the job counters are set to 0. + + **Example request**: + + .. sourcecode:: http + + POST /api/resetjobs HTTP/1.1 + Host: localhost + + [123,124,125] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +.. http:post:: /api/startjobs + + Start the jobs. The job status is set to 'WAITING'. + + **Example request**: + + .. sourcecode:: http + + POST /api/startjobs HTTP/1.1 + Host: localhost + + [123,124,125] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +.. http:post:: /api/pausejobs + + Pause the jobs. The job status is set to 'PAUSED'. + + **Example request**: + + .. sourcecode:: http + + POST /api/pausejobs HTTP/1.1 + Host: localhost + + [123,124,125] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +Workers +******* + +.. http:get:: /api/workers + + Returns the workers. + + **Example request**: + + .. sourcecode:: http + + GET /api/workers HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "name": "worker-1", + "ip": "127.0.0.1", + "affinity": "LINUX,WINDOWS", + "state": "WAITING", + "ping_time": 123456, + "finished": 123, + "error": 21, + "last_job": 1234, + "current_event": 1234, + "cpu": "[0,0,0,0]", + "free_memory": 123456, + "total_memory": 1000000, + "active": 1 + } + + :statuscode 200: no error + :statuscode 500: error + + +.. http:post:: /api/workers + + Modify the workers properties. + + **Example request**: + + .. sourcecode:: http + + POST /api/workers HTTP/1.1 + Host: localhost + + { + "worker-1": + { + "affinity": "LINUX", + "active": 0, + }, + "worker-2": + { + "affinity": "LINUX", + "active": 0, + } + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + :statuscode 200: no error + :statuscode 500: error + + +.. http:delete:: /api/workers + + Delete the workers. + + **Example request**: + + .. sourcecode:: http + + DELETE /api/workers HTTP/1.1 + Host: localhost + + ["worker-1","worker-2"] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +.. http:post:: /api/stopworkers + + Stop the workers. + + **Example request**: + + .. sourcecode:: http + + POST /api/stopworkers HTTP/1.1 + Host: localhost + + ["worker-1","worker-2"] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +.. http:post:: /api/startworkers + + Start the workers. + + **Example request**: + + .. sourcecode:: http + + POST /api/startworkers HTTP/1.1 + Host: localhost + + ["worker-1","worker-2"] + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + 1 + + +Events +****** + +.. http:get:: /api/events + + Returns some events. + + :param job: returns the events for this job. + :param worker: returns the events for this worker. + :param howlong: returns all the events in the last `howlong` seconds. + + **Example request**: + + .. sourcecode:: http + + GET /api/events?job=123&worker=worker-1&howlong=60 HTTP/1.1 + Host: localhost + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + { + "id": 123, + "worker": "worker-1", + "job_id": 123, + "job_title": "Job 123", + "state": "ERROR", + "start": 123456, + "duration": 10 + } + + :statuscode 200: no error + :statuscode 500: error + + diff --git a/doc/source/usage.rst b/doc/source/usage.rst new file mode 100644 index 0000000..1500771 --- /dev/null +++ b/doc/source/usage.rst @@ -0,0 +1,143 @@ +Jobs +==== + +A job is a simple command to run on one of the workers. Each job has an ID chosen by the server. + +A job can be submitted to the server using control.py or a HTTP request. +Job attributes + +A job may have different attributes. For example, the job attribute cmd is the command to execute. + +The different job attributes used to initialise a job are: + + - **cmd**: the command to run + - **title**: the title to display in the user interface + - **dir**: the job's working directory + - **env**: an environment for the command + - **parent**: the parent job ID + - **priority**: the job's priority + - **dependencies**: the job's dependencies on other jobs + - **timeout**: the job's timeout in seconds + - **retry**: how many time to retry this job + - **affinity**: the job affinities to match + - **url**: the url to open with the Open link + - **user**: the user name/email of the owner of this job + - **globalprogress**: the job progression pattern + - **localprogress**: the second job progression pattern + +Job execution +------------- + +A new job is in the **WAITING** state. + +When a worker run a job, it set the current working directory to dir and run the command cmd. The job is then in the **WORKING** state. If the command returns with the exit code 0, the job is put in the state **FINISHED**. If not, the job is put in the state **ERROR**. + +If the job is in the **ERROR** state, the server will retry to run this job up to retry times. + +If the job duration exceeds timeout, the server will kill this job and set it in the **ERROR** state. + +Job display +----------- + +The title attribute is displayed in the user interface. + +The url attribute is the url to open with the Open link in the user interface. + +By default, the web browser blocks the URLs on local files. + +On Firefox, `it is possible to override this behavior `. + +Job environment +--------------- + +An environment can be provided with a job using the **env** attribute. An environment is a string containing all the variables and their values. The separator is the string **"\n"** (a '\' character followed be a 'n' character, not an end of line character). + +Here is a string you can use with the **env** attribute:: + + "USER=mylogin\nPATH=mypath" + +Job hierarchy +------------- + +The hierarchy is useful to organize and schedule the different jobs. + +A job can be the parent of some children jobs. In this case, the parent job won't run any command. Even if the attribute **cmd** has been provided. + +The parent attribute can be specified to create a job into a previously created parent job. + +The parent attribute can be the parent job ID (an integer), a job title (a string) or a path of job titles. Exemples:: + + parent=12345 : add the new job to the job #12345 + parent="departments" : add the new job to the job named "departments" + parent="departments|render" : add the new job to the job named "render" inside the job named "departments" + +Job dependencies +---------------- + +The dependencies attributes is a job ID list of the different jobs to finish before to run this job. + +A list can be provided like this : "1,3,5". + +Job affinities +-------------- + +Affinities are used to associate some jobs to a subset of workers. + +The affinity attribute is a list of strings, separated by comas. + +If a job has an affinity attribute, only the workers with the affinities matching all the job's affinities will be able to run this job. + +For example, let's say a job has the following affinity : "LINUX,24GB". Here is a summary of which worker affinities configuration match the job's one:: + + | Job affinites | Worker affinities | Match | |:------------------|:----------------------|:----------| | "LINUX,24GB" | "LINUX" | NO | | "LINUX,24GB" | "24GB" | NO | | "LINUX,24GB" | "LINUX,24GB" | YES | | "LINUX,24GB" | "LINUX,24GB,GL" | YES | + +Job owner +--------- + +The user attributes is the user name of the owner of the job. If the emails are activated, the emails regarding this job will be sent at user. + +If LDAP is configured, the job will be executed with the user rights. + +Job log +------- + +The job's log is the output of the command's stdout and stderr streams. The log is sent by the worker to the server. + +The server stores the log in the logs/ directory, in a file named ID.log with ID the ID of the job. + +Job progression +--------------- + +The globalprogress attribute is a pattern that is used to extract the job progression out of the job's logs. Here are some examples of logs and patterns:: + + | Log | Pattern | Progression | |:--------|:------------|:----------------| | 25 | %percent | 25% | | (50) | (%percent) | 50% | | 0.75 | %one | 75% | | P:1 | P:%one | 100% | + +The localprogress attribute can be used with globalprogress to specify a second level of the job progression. + +Server +====== + +The coalition server collects the jobs and distributes them to the differents workers. + +Run the server + +Add a job +--------- + +It is possible to add a job to the server using control.py or a HTTP request. + +If the job is added, the new job ID is returned. +Using control.py + +You can use control.py to add jobs to the server:: + + python control.py --cmd="echo toto" --priority=1000 --affinity="linux" --retry=10 http://127.0.0.1:19211 add + +Using a HTTP request +-------------------- + +To add a job using the HTTP interface, simply GET or POST the url http://host:port/json/addjob with any job attributes. + +Example:: + + http://127.0.0.1:19211/json/addjob?title=job&cmd=echo toto&priority=1000&affinity=linux&retry=10 diff --git a/doc/source/web_frontend.rst b/doc/source/web_frontend.rst new file mode 100644 index 0000000..9c985a2 --- /dev/null +++ b/doc/source/web_frontend.rst @@ -0,0 +1,18 @@ +Web interface +============= + +With a web browser visit:: + + http://:19211 + +If LDAP is configured in **coalition.ini**, you will be asked for a username and a password. Once logged in, some permissions are granted to you according to the LDAP policies. + +By defaut no LDAP is required and any action are authorized. + +You can select multiple lines while pressing or key and mouse clic. + +A double clic on a job will get you to the job log page. + +The job page will remember your last jobs filtering criteria via localstorage so that you can refresh the page without loosing your customisations. Clear you browser's localstorage or click the button "reset filter" to revert to the default display. + +In cloud mode, you can manually terminate worker instances with the "terminate" button in the workers tab. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fdd3e6e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +coalition: + build: . + ports: + - 19211:19211 + volumes: + - ./coalition.ini:/usr/src/app/coalition.ini:z + - ./data:/usr/src/app/data:z + restart: always diff --git a/error.py b/error.py deleted file mode 100644 index 9b5739b..0000000 --- a/error.py +++ /dev/null @@ -1,6 +0,0 @@ -# Simulate a job ending in error -import time - -print ("ERROR") - -os.exit (1) diff --git a/host_cpu.py b/host_cpu.py index bfa4782..4164689 100644 --- a/host_cpu.py +++ b/host_cpu.py @@ -1,101 +1,107 @@ -import sys,os,re - -if sys.platform=="win32": - import win32pdh - import win32pdhquery - import win32pdhutil - import _winreg - -# Parse the registry to find the localized perf counter name -def pdhTranslateEnglishCounter (counter): - key = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009") - strings = _winreg.QueryValueEx (key, 'Counter')[0] - for i in range(0,len(strings),2): - if counter == strings[i+1]: - return win32pdh.LookupPerfNameByIndex (None, int(strings[i])) - return counter - -def cpuCount(): - """Returns the number of CPUs in the system""" - num = 1 - if sys.platform == 'win32': - try: - num = int(os.environ['NUMBER_OF_PROCESSORS']) - except (ValueError, KeyError): - pass - elif sys.platform == 'darwin': - try: - num = int(os.popen('sysctl -n hw.ncpu').read()) - except ValueError: - pass - else: - try: - num = os.sysconf('SC_NPROCESSORS_ONLN') - except (ValueError, OSError, AttributeError): - pass - - return num - -gUser = 0 -gNice = 0 -gSystem = 0 -gIdle = 0 - -class HostCPU: - """This class returns the per CPU""" -# def __init__(self): -# if sys.platform=="win32": -# self.base = win32pdh.OpenQuery() -# self.Counters = [] -# cpucount = cpuCount() -# for cpuid in range(0,cpucount): -# self.Counters.append (win32pdh.AddCounter(self.base, win32pdh.MakeCounterPath((None, pdhTranslateEnglishCounter ("Processor"),str(cpuid),None, -1, pdhTranslateEnglishCounter ("% Processor Time"))))) -# #self.Counters.append (win32pdh.AddCounter(self.base, win32pdh.MakeCounterPath((None, "Processor",str(cpuid),None, -1, "% Processor Time")))) -# win32pdh.CollectQueryData(self.base) - - def getUsage(self): - ''' Return a list with the usage of each CPU ''' -# if sys.platform=="win32": -# result = [] -# win32pdh.CollectQueryData(self.base) -# for counter in self.Counters: -# try: -# load = win32pdh.GetFormattedCounterValue(counter,win32pdh.PDH_FMT_DOUBLE)[1] -# except: -# load = 0 -# pass -# result.append (load) -# return result -# else: -# result = [] -# for cpuid in range(0,cpucount): -# result.append (0) - if sys.platform!="win32" and sys.platform!="darwin": - global gUser - global gNice - global gSystem - global gIdle - user = 0 - nice = 0 - system = 0 - idle = 0 - file = open ("/proc/stat", "r") - for line in file: - words = re.split ('\W+', line) - if len(words) >= 5: - if words[0] == 'cpu': - user = int(words[1]) - nice = int(words[2]) - system = int(words[3]) - idle = int(words[4]) - usage = (user-gUser)+(nice-gNice)+(system-gSystem) - total = usage+(idle-gIdle) - gUser = user - gNice = nice - gSystem = system - gIdle = idle - if total > 0: - return [100*usage/total] - return [0] - - return [0] +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys,os,re + +if sys.platform=="win32": + import win32pdh + import win32pdhquery + import win32pdhutil + import _winreg + +# Parse the registry to find the localized perf counter name +def pdhTranslateEnglishCounter (counter): + key = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib\009") + strings = _winreg.QueryValueEx (key, 'Counter')[0] + for i in range(0,len(strings),2): + if counter == strings[i+1]: + return win32pdh.LookupPerfNameByIndex (None, int(strings[i])) + return counter + +def cpuCount(): + """Returns the number of CPUs in the system""" + num = 1 + if sys.platform == 'win32': + try: + num = int(os.environ['NUMBER_OF_PROCESSORS']) + except (ValueError, KeyError): + pass + elif sys.platform == 'darwin': + try: + num = int(os.popen('sysctl -n hw.ncpu').read()) + except ValueError: + pass + else: + try: + num = os.sysconf('SC_NPROCESSORS_ONLN') + except (ValueError, OSError, AttributeError): + pass + + return num + +gUser = 0 +gNice = 0 +gSystem = 0 +gIdle = 0 + +class HostCPU: + """This class returns the per CPU""" +# def __init__(self): +# if sys.platform=="win32": +# self.base = win32pdh.OpenQuery() +# self.Counters = [] +# cpucount = cpuCount() +# for cpuid in range(0,cpucount): +# self.Counters.append (win32pdh.AddCounter(self.base, win32pdh.MakeCounterPath((None, pdhTranslateEnglishCounter ("Processor"),str(cpuid),None, -1, pdhTranslateEnglishCounter ("% Processor Time"))))) +# #self.Counters.append (win32pdh.AddCounter(self.base, win32pdh.MakeCounterPath((None, "Processor",str(cpuid),None, -1, "% Processor Time")))) +# win32pdh.CollectQueryData(self.base) + + def getUsage(self): + ''' Return a list with the usage of each CPU ''' +# if sys.platform=="win32": +# result = [] +# win32pdh.CollectQueryData(self.base) +# for counter in self.Counters: +# try: +# load = win32pdh.GetFormattedCounterValue(counter,win32pdh.PDH_FMT_DOUBLE)[1] +# except: +# load = 0 +# pass +# result.append (load) +# return result +# else: +# result = [] +# for cpuid in range(0,cpucount): +# result.append (0) + if sys.platform!="win32" and sys.platform!="darwin": + global gUser + global gNice + global gSystem + global gIdle + user = 0 + nice = 0 + system = 0 + idle = 0 + file = open ("/proc/stat", "r") + for line in file: + words = re.split ('\W+', line) + if len(words) >= 5: + if words[0] == 'cpu': + user = int(words[1]) + nice = int(words[2]) + system = int(words[3]) + idle = int(words[4]) + usage = (user-gUser)+(nice-gNice)+(system-gSystem) + total = usage+(idle-gIdle) + gUser = user + gNice = nice + gSystem = system + gIdle = idle + if total > 0: + return [100*usage/total] + return [0] + + return [0] + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/host_mem.py b/host_mem.py index 400cc5d..98136d5 100644 --- a/host_mem.py +++ b/host_mem.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import sys,re,os @@ -66,3 +69,5 @@ def getAvailableMem (): total, free = parseMemInfo () return free * 1024 +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/install/linux/build_installer.sh b/install/linux/build_installer.sh old mode 100755 new mode 100644 diff --git a/install/win32/build_installer.py b/install/win32/build_installer.py index 90a7660..61d7714 100644 --- a/install/win32/build_installer.py +++ b/install/win32/build_installer.py @@ -1,91 +1,96 @@ -import _winreg, os, re, os.path - -compile = True -buildNsis = True - -# under windows, uses the registry setup by the installer -hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\NSIS", 0, _winreg.KEY_READ) -NSISDir, type = _winreg.QueryValueEx (hKey, "") -print ("NSIS found here : " + NSISDir) - -# Stop the services -os.system ("net stop CoalitionServer") - -# Compile the services -# os.chdir ("../..") -if compile: - os.system ("python server.py remove") - os.system ("python setup_py2exe.py install") - os.system ("python setup_py2exe.py py2exe") - -if buildNsis: - # Get the version number - f = open ("coalition.version", "r") - version = f.read () - version = re.sub ("\n", "", version) - version = re.sub ("\r", "", version) - f.close () - - # Generates the NSIS script - f = open ("install/win32/coalition.nsi", "r") - script = f.read () - f.close () - - installFiles = "" - removeFiles = "" - currentDir = "" - currentPath = "" - - def setOutPath (path, goin): - global installFiles, removeFiles, currentDir, currentPath - currentPath = path - currentDir = path == "" and "$INSTDIR" or ("$INSTDIR\\" + path) - installFiles = installFiles + "\tSetOutPath \"" + currentDir + "\"\n" - if goin: - removeFiles = "\tRMDir \"" + currentDir + "\"\n" + removeFiles - - def addFile (localpath): - global installFiles, removeFiles, currentDir - currentFile = currentDir + "\\" + os.path.basename (localpath) - installFiles = installFiles + "\tFile \"" + localpath + "\"\n" - removeFiles = "\tDelete \"" + currentFile + "\"\n" + removeFiles - - def addFiles (localpath, rec): - global currentPath - for file in os.listdir(localpath): - filename = localpath + "\\" + file - if os.path.isdir (filename): - if rec and file != ".svn": - oldpath = currentPath - setOutPath (currentPath + "\\" + file, True) - addFiles (filename, rec) - setOutPath (oldpath, False) - else: - addFile (filename) - - setOutPath ("", True) - addFile ("coalition.ini") - addFile ("images\coalition.ico") - addFile ("images\server_start.ico") - addFile ("images\server_stop.ico") - addFile ("images\worker_start.ico") - addFile ("images\worker_stop.ico") - addFile ("vcredist_x86.exe") - addFiles ("dist", True) - setOutPath ("public_html", True) - addFiles ("public_html", True) - - installFiles = re.sub ("\\\\", "\\\\\\\\", installFiles) - script = re.sub ("__INSTALL_FILES__", installFiles, script) - removeFiles = re.sub ("\\\\", "\\\\\\\\", removeFiles) - script = re.sub ("__REMOVE_FILES__", removeFiles, script) - - script = re.sub ("__VERSION__", version, script) - - f = open ("_coalition.nsi", "w") - f.write (script) - f.close () - - # Run NSIS - os.system ("\"" + NSISDir + "/makensis.exe\" _coalition.nsi") - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import _winreg, os, re, os.path + +compile = True +buildNsis = True + +# under windows, uses the registry setup by the installer +hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\NSIS", 0, _winreg.KEY_READ) +NSISDir, type = _winreg.QueryValueEx (hKey, "") +print ("NSIS found here : " + NSISDir) + +# Stop the services +os.system ("net stop CoalitionServer") + +# Compile the services +# os.chdir ("../..") +if compile: + os.system ("python server.py remove") + os.system ("python setup_py2exe.py install") + os.system ("python setup_py2exe.py py2exe") + +if buildNsis: + # Get the version number + f = open ("coalition.version", "r") + version = f.read () + version = re.sub ("\n", "", version) + version = re.sub ("\r", "", version) + f.close () + + # Generates the NSIS script + f = open ("install/win32/coalition.nsi", "r") + script = f.read () + f.close () + + installFiles = "" + removeFiles = "" + currentDir = "" + currentPath = "" + + def setOutPath (path, goin): + global installFiles, removeFiles, currentDir, currentPath + currentPath = path + currentDir = path == "" and "$INSTDIR" or ("$INSTDIR\\" + path) + installFiles = installFiles + "\tSetOutPath \"" + currentDir + "\"\n" + if goin: + removeFiles = "\tRMDir \"" + currentDir + "\"\n" + removeFiles + + def addFile (localpath): + global installFiles, removeFiles, currentDir + currentFile = currentDir + "\\" + os.path.basename (localpath) + installFiles = installFiles + "\tFile \"" + localpath + "\"\n" + removeFiles = "\tDelete \"" + currentFile + "\"\n" + removeFiles + + def addFiles (localpath, rec): + global currentPath + for file in os.listdir(localpath): + filename = localpath + "\\" + file + if os.path.isdir (filename): + if rec and file != ".svn": + oldpath = currentPath + setOutPath (currentPath + "\\" + file, True) + addFiles (filename, rec) + setOutPath (oldpath, False) + else: + addFile (filename) + + setOutPath ("", True) + addFile ("coalition.ini") + addFile ("images\coalition.ico") + addFile ("images\server_start.ico") + addFile ("images\server_stop.ico") + addFile ("images\worker_start.ico") + addFile ("images\worker_stop.ico") + addFile ("vcredist_x86.exe") + addFiles ("dist", True) + setOutPath ("public_html", True) + addFiles ("public_html", True) + + installFiles = re.sub ("\\\\", "\\\\\\\\", installFiles) + script = re.sub ("__INSTALL_FILES__", installFiles, script) + removeFiles = re.sub ("\\\\", "\\\\\\\\", removeFiles) + script = re.sub ("__REMOVE_FILES__", removeFiles, script) + + script = re.sub ("__VERSION__", version, script) + + f = open ("_coalition.nsi", "w") + f.write (script) + f.close () + + # Run NSIS + os.system ("\"" + NSISDir + "/makensis.exe\" _coalition.nsi") + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/install/win32/coalition.nsi b/install/win32/coalition.nsi index 223585c..b3b9d31 100644 --- a/install/win32/coalition.nsi +++ b/install/win32/coalition.nsi @@ -1,135 +1,135 @@ -!include "Sections.nsh" - - -Icon "images\coalition.ico" -UninstallIcon "${NSISDIR}\contrib\graphics\icons\classic-uninstall.ico" - -InstallDir $PROGRAMFILES\Coalition - -Page components -Page directory - -Page instfiles - -Section "Common Files" -SectionIn RO - SetShellVarContext all - - ReadRegStr $R1 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "UninstallString" - StrCmp $R1 "" UninstallMSI_nomsi - IfSilent noUninstallWarning - MessageBox MB_YESNOCANCEL|MB_ICONQUESTION "A previous version of Coalition was found. It is recommended that you uninstall it first.$\n$\nDo you want to do that now?" IDNO UninstallMSI_nomsi IDYES UninstallMSI_yesmsi - Quit -noUninstallWarning: - UninstallMSI_yesmsi: - ExecWait '$R1 /S _?=$INSTDIR' - UninstallMSI_nomsi: - - CreateDirectory "$INSTDIR" - CreateDirectory "$SMPROGRAMS\Coalition" - CreateDirectory "$APPDATA\Coalition" - AccessControl::GrantOnFile "$APPDATA\Coalition" "(BU)" "FullAccess" - - ExecWait 'net stop CoalitionServer' - ExecWait 'net stop CoalitionWorker' - - ; Write the registry - WriteRegStr HKLM "Software\Mercenaries Engineering\Coalition" "Installdir" $INSTDIR - WriteRegStr HKLM "Software\Mercenaries Engineering\Coalition" "Datadir" "$APPDATA\Coalition" - - ; Write the uninstall keys for Windows - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "DisplayName" "Coalition" - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "DisplayIcon" '"$INSTDIR\coalition.ico"' - WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "UninstallString" '"$INSTDIR\uninstall.exe"' - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "NoModify" 1 - WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "NoRepair" 1 - - ; Set output path to the installation directory. - WriteUninstaller "uninstall.exe" - - CreateShortCut "$SMPROGRAMS\Coalition\Configuration File.lnk" "$INSTDIR\coalition.ini" "" "" - - ; Set output path to the installation directory. -__INSTALL_FILES__ - - ; Install msvc redist - ExecWait '"$INSTDIR\vcredist_x86.exe /q"' - Delete $INSTDIR\vcredist_x86.exe - -SectionEnd - -Section /o "Server (the master computer)" - SetShellVarContext all - - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Monitor.lnk" "http://localhost:19211" "" "$INSTDIR\coalition.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Start.lnk" "net" "start CoalitionServer" "$INSTDIR\server_start.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Stop.lnk" "net" "stop CoalitionServer" "$INSTDIR\server_stop.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" - CreateShortCut "$DESKTOP\Coalition Server Monitor.lnk" "http://localhost:19211" "" "$INSTDIR\coalition.ico" - CreateShortCut "$DESKTOP\Coalition Server Start.lnk" "net" "start CoalitionServer" "$INSTDIR\server_start.ico" - CreateShortCut "$DESKTOP\Coalition Server Stop.lnk" "net" "stop CoalitionServer" "$INSTDIR\server_stop.ico" - - ExecWait '"$INSTDIR\server" -remove' - ExecWait '"$INSTDIR\server" -install -auto' - ExecWait 'net start CoalitionServer' -SectionEnd - -Section "Worker Command Line (slave)" - SetShellVarContext all - - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker.lnk" "$INSTDIR\worker.exe" "" "$INSTDIR\worker_start.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 - CreateShortCut "$DESKTOP\Coalition Worker.lnk" "$INSTDIR\worker.exe" "" "$INSTDIR\worker_start.ico" - - ExecWait 'net start CoalitionWorker' -SectionEnd - -Section /o "Worker Service (slave)" - SetShellVarContext all - - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker Service Start.lnk" "net" "start CoalitionWorker" "$INSTDIR\worker_start.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker Service Stop.lnk" "net" "stop CoalitionWorker" "$INSTDIR\worker_stop.ico" - CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 - CreateShortCut "$DESKTOP\Coalition Worker Service Start.lnk" "net" "start CoalitionWorker" "$INSTDIR\worker_start.ico" - CreateShortCut "$DESKTOP\Coalition Worker Service Stop.lnk" "net" "stop CoalitionWorker" "$INSTDIR\worker_stop.ico" - - ExecWait '"$INSTDIR\worker_service" -remove' - ExecWait '"$INSTDIR\worker_service" -install -auto' - ExecWait 'net start CoalitionWorker' -SectionEnd - -;Section /o "Autorun the worker on idle" -; ExecWait 'schtasks /Create /TN "Coalition Worker" /SC ONIDLE /IT /I 1 /TR "\"$INSTDIR\worker.exe\""' -;SectionEnd - -Section "Uninstall" - SetShellVarContext all - - ; ** Ask the user for a confirmation - IfSilent noUninstallWarning - MessageBox MB_YESNO|MB_ICONQUESTION "Do you want to uninstall Coalition from this computer ?" IDYES Uninstall_yes - Quit - Uninstall_yes: -noUninstallWarning: - - ;ExecWait 'schtasks /Delete /TN "Coalition Worker" /F' - ExecWait 'net stop CoalitionServer' - ExecWait '"$INSTDIR\server" -remove' - ExecWait 'net stop CoalitionWorker' - ExecWait '"$INSTDIR\worker_service" -remove' - Delete $INSTDIR\uninstall.exe ; delete self (see explanation below why this works) - RMDir /r "$SMPROGRAMS\Coalition" - Delete "$DESKTOP\Coalition Server Monitor.lnk" - Delete "$DESKTOP\Coalition Server Start.lnk" - Delete "$DESKTOP\Coalition Server Stop.lnk" - Delete "$DESKTOP\Coalition Worker Service Start.lnk" - Delete "$DESKTOP\Coalition Worker Service Stop.lnk" - Delete "$DESKTOP\Coalition Worker.lnk" - - DeleteRegKey HKLM "Software\Mercenaries Engineering\Coalition" - DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" -__REMOVE_FILES__ -Sectionend - -Name "Coalition v__VERSION__" -OutFile "Coalition-Win32-__VERSION__.exe" +!include "Sections.nsh" + + +Icon "images\coalition.ico" +UninstallIcon "${NSISDIR}\contrib\graphics\icons\classic-uninstall.ico" + +InstallDir $PROGRAMFILES\Coalition + +Page components +Page directory + +Page instfiles + +Section "Common Files" +SectionIn RO + SetShellVarContext all + + ReadRegStr $R1 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "UninstallString" + StrCmp $R1 "" UninstallMSI_nomsi + IfSilent noUninstallWarning + MessageBox MB_YESNOCANCEL|MB_ICONQUESTION "A previous version of Coalition was found. It is recommended that you uninstall it first.$\n$\nDo you want to do that now?" IDNO UninstallMSI_nomsi IDYES UninstallMSI_yesmsi + Quit +noUninstallWarning: + UninstallMSI_yesmsi: + ExecWait '$R1 /S _?=$INSTDIR' + UninstallMSI_nomsi: + + CreateDirectory "$INSTDIR" + CreateDirectory "$SMPROGRAMS\Coalition" + CreateDirectory "$APPDATA\Coalition" + AccessControl::GrantOnFile "$APPDATA\Coalition" "(BU)" "FullAccess" + + ExecWait 'net stop CoalitionServer' + ExecWait 'net stop CoalitionWorker' + + ; Write the registry + WriteRegStr HKLM "Software\Mercenaries Engineering\Coalition" "Installdir" $INSTDIR + WriteRegStr HKLM "Software\Mercenaries Engineering\Coalition" "Datadir" "$APPDATA\Coalition" + + ; Write the uninstall keys for Windows + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "DisplayName" "Coalition" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "DisplayIcon" '"$INSTDIR\coalition.ico"' + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" "NoRepair" 1 + + ; Set output path to the installation directory. + WriteUninstaller "uninstall.exe" + + CreateShortCut "$SMPROGRAMS\Coalition\Configuration File.lnk" "$INSTDIR\coalition.ini" "" "" + + ; Set output path to the installation directory. +__INSTALL_FILES__ + + ; Install msvc redist + ExecWait '"$INSTDIR\vcredist_x86.exe /q"' + Delete $INSTDIR\vcredist_x86.exe + +SectionEnd + +Section /o "Server (the master computer)" + SetShellVarContext all + + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Monitor.lnk" "http://localhost:19211" "" "$INSTDIR\coalition.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Start.lnk" "net" "start CoalitionServer" "$INSTDIR\server_start.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Server Stop.lnk" "net" "stop CoalitionServer" "$INSTDIR\server_stop.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" + CreateShortCut "$DESKTOP\Coalition Server Monitor.lnk" "http://localhost:19211" "" "$INSTDIR\coalition.ico" + CreateShortCut "$DESKTOP\Coalition Server Start.lnk" "net" "start CoalitionServer" "$INSTDIR\server_start.ico" + CreateShortCut "$DESKTOP\Coalition Server Stop.lnk" "net" "stop CoalitionServer" "$INSTDIR\server_stop.ico" + + ExecWait '"$INSTDIR\server" -remove' + ExecWait '"$INSTDIR\server" -install -auto' + ExecWait 'net start CoalitionServer' +SectionEnd + +Section "Worker Command Line (slave)" + SetShellVarContext all + + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker.lnk" "$INSTDIR\worker.exe" "" "$INSTDIR\worker_start.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$DESKTOP\Coalition Worker.lnk" "$INSTDIR\worker.exe" "" "$INSTDIR\worker_start.ico" + + ExecWait 'net start CoalitionWorker' +SectionEnd + +Section /o "Worker Service (slave)" + SetShellVarContext all + + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker Service Start.lnk" "net" "start CoalitionWorker" "$INSTDIR\worker_start.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Coalition Worker Service Stop.lnk" "net" "stop CoalitionWorker" "$INSTDIR\worker_stop.ico" + CreateShortCut "$SMPROGRAMS\Coalition\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$DESKTOP\Coalition Worker Service Start.lnk" "net" "start CoalitionWorker" "$INSTDIR\worker_start.ico" + CreateShortCut "$DESKTOP\Coalition Worker Service Stop.lnk" "net" "stop CoalitionWorker" "$INSTDIR\worker_stop.ico" + + ExecWait '"$INSTDIR\worker_service" -remove' + ExecWait '"$INSTDIR\worker_service" -install -auto' + ExecWait 'net start CoalitionWorker' +SectionEnd + +;Section /o "Autorun the worker on idle" +; ExecWait 'schtasks /Create /TN "Coalition Worker" /SC ONIDLE /IT /I 1 /TR "\"$INSTDIR\worker.exe\""' +;SectionEnd + +Section "Uninstall" + SetShellVarContext all + + ; ** Ask the user for a confirmation + IfSilent noUninstallWarning + MessageBox MB_YESNO|MB_ICONQUESTION "Do you want to uninstall Coalition from this computer ?" IDYES Uninstall_yes + Quit + Uninstall_yes: +noUninstallWarning: + + ;ExecWait 'schtasks /Delete /TN "Coalition Worker" /F' + ExecWait 'net stop CoalitionServer' + ExecWait '"$INSTDIR\server" -remove' + ExecWait 'net stop CoalitionWorker' + ExecWait '"$INSTDIR\worker_service" -remove' + Delete $INSTDIR\uninstall.exe ; delete self (see explanation below why this works) + RMDir /r "$SMPROGRAMS\Coalition" + Delete "$DESKTOP\Coalition Server Monitor.lnk" + Delete "$DESKTOP\Coalition Server Start.lnk" + Delete "$DESKTOP\Coalition Server Stop.lnk" + Delete "$DESKTOP\Coalition Worker Service Start.lnk" + Delete "$DESKTOP\Coalition Worker Service Stop.lnk" + Delete "$DESKTOP\Coalition Worker.lnk" + + DeleteRegKey HKLM "Software\Mercenaries Engineering\Coalition" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Coalition" +__REMOVE_FILES__ +Sectionend + +Name "Coalition v__VERSION__" +OutFile "Coalition-Win32-__VERSION__.exe" diff --git a/job.py b/job.py index fe847f0..5818cc6 100644 --- a/job.py +++ b/job.py @@ -1,4 +1,10 @@ -# Simulate a job ending in error +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Simulate a job ending in error +""" + import time, sys for i in range(1000) : @@ -7,3 +13,6 @@ time.sleep (0.01) sys.exit (0) + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/migrations/0000_db_mysql.py b/migrations/0000_db_mysql.py new file mode 100644 index 0000000..4c06768 --- /dev/null +++ b/migrations/0000_db_mysql.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- + +# Initial database setup + +steps = [ +""" +CREATE TABLE IF NOT EXISTS WorkerAffinities( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + worker_name VARCHAR(255), + affinity BIGINT DEFAULT 0, + ordering INT DEFAULT 0) +""", +""" +CREATE TABLE IF NOT EXISTS Jobs( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + parent INT DEFAULT 0, + title TEXT, + command TEXT, + dir TEXT, + environment TEXT, + state TEXT, + paused BOOLEAN DEFAULT 0, + worker TEXT, + start_time INT DEFAULT 0, + duration INT DEFAULT 0, + run_done INT DEFAULT 0, + timeout INT DEFAULT 0, + priority INT UNSIGNED DEFAULT 8, + affinity TEXT, + affinity_bits BIGINT DEFAULT 0, + user TEXT, + finished INT DEFAULT 0, + errors INT DEFAULT 0, + working INT DEFAULT 0, + total INT DEFAULT 0, + total_finished INT DEFAULT 0, + total_errors INT DEFAULT 0, + total_working INT DEFAULT 0, + url TEXT, + progress FLOAT, + progress_pattern TEXT, + h_affinity BIGINT DEFAULT 0, + h_priority BIGINT UNSIGNED DEFAULT 0, + h_paused BOOLEAN DEFAULT 0, + h_depth INT DEFAULT 0) +""", +""" +CREATE TABLE IF NOT EXISTS Dependencies( + job_id Int, dependency INT) +""", +""" +CREATE TABLE IF NOT EXISTS Workers( + name VARCHAR(255), + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ip TEXT, + affinity TEXT, + state TEXT, + finished INT, + error INT, + last_job INT, + current_event INT, + cpu TEXT, + free_memory INT, + total_memory int, + active BOOLEAN) +""", +""" +CREATE TABLE IF NOT EXISTS Events( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + worker VARCHAR(255), + job_id INT, + job_title TEXT, + state TEXT, + start INT, + duration INT) +""", +""" +CREATE TABLE IF NOT EXISTS Affinities( + id INTEGER, + name TEXT) +""", +""" +CREATE INDEX worker_name_index ON WorkerAffinities(worker_name) +""", +""" +CREATE UNIQUE INDEX name_index ON Workers(name) +""", +""" +CREATE INDEX parent_index ON Jobs(parent) +""", +""" +CREATE INDEX state_index ON Jobs(state) +""", +""" +CREATE INDEX job_id_index ON Dependencies(job_id) +""", +""" +CREATE INDEX dependency_index ON Dependencies(dependency) +""", +""" +CREATE INDEX worker_index ON Events(worker) +""", +""" +CREATE INDEX job_id_name ON Events(job_id) +""", +""" +CREATE INDEX start_name ON Events(start) +""", +] + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/migrations/0000_db_sqlite.py b/migrations/0000_db_sqlite.py new file mode 100644 index 0000000..12c9ca0 --- /dev/null +++ b/migrations/0000_db_sqlite.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- + +# Initial database setup + +steps = [ +""" +CREATE TABLE IF NOT EXISTS WorkerAffinities( + id INTEGER PRIMARY KEY AUTOINCREMENT, + worker_name TEXT, + affinity BIGINT DEFAULT 0, + ordering INT DEFAULT 0) +""", +""" +CREATE INDEX IF NOT EXISTS worker_name_index ON WorkerAffinities(worker_name) +""", +""" +CREATE TABLE IF NOT EXISTS Jobs( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent INT DEFAULT 0, + title TEXT DEFAULT "", + command TEXT DEFAULT "", + dir TEXT DEFAULT ".", + environment TEXT DEFAULT "", + state TEXT DEFAULT "WAITING", + paused BOOLEAN DEFAULT 0, + worker TEXT DEFAULT "", + start_time INT DEFAULT 0, + duration INT DEFAULT 0, + run_done INT DEFAULT 0, + timeout INT DEFAULT 0, + priority UNSIGNED INT DEFAULT 8, + affinity TEXT DEFAULT "", + affinity_bits BIGINT DEFAULT 0, + user TEXT DEFAULT "", + finished INT DEFAULT 0, + errors INT DEFAULT 0, + working INT DEFAULT 0, + total INT DEFAULT 0, + total_finished INT DEFAULT 0, + total_errors INT DEFAULT 0, + total_working INT DEFAULT 0, + url TEXT DEFAULT "", + progress FLOAT, + progress_pattern TEXT DEFAULT "", + h_affinity BIGINT DEFAULT 0, + h_priority UNSIGNED BIGINT DEFAULT 0, + h_paused BOOLEAN DEFAULT 0, + h_depth INT DEFAULT 0) +""", +""" +CREATE INDEX IF NOT EXISTS Parent_index ON Jobs(parent) +""", +""" +CREATE TABLE IF NOT EXISTS Dependencies( + job_id Int, + dependency INT) +""", +""" +CREATE INDEX IF NOT EXISTS JobId_index ON Dependencies(job_id) +""", +""" +CREATE INDEX IF NOT EXISTS Dependency_index ON Dependencies(dependency) +""", +""" +CREATE TABLE IF NOT EXISTS Workers( + name TEXT, + start_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + ip TEXT, + affinity TEXT DEFAULT "", + state TEXT, + finished INT, + error INT, + last_job INT, + current_event INT, + cpu TEXT, + free_memory INT, + total_memory int, + active BOOLEAN) +""", +""" +CREATE UNIQUE INDEX IF NOT EXISTS Name_index ON Workers (name) +""", +""" +CREATE TABLE IF NOT EXISTS Events( + id INTEGER PRIMARY KEY AUTOINCREMENT, + worker TEXT, job_id INT, + job_title TEXT, + state TEXT, + start INT, + duration INT) +""", +""" +CREATE INDEX IF NOT EXISTS Worker_index ON Events(worker) +""", +""" +CREATE INDEX IF NOT EXISTS JobID_index ON Events(job_id) +""", +""" +CREATE INDEX IF NOT EXISTS Start_index ON Events(start) +""", +""" +CREATE TABLE IF NOT EXISTS Affinities( + id INTEGER, + name TEXT) +""" +] + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/migrations/0001_db_mysql.py b/migrations/0001_db_mysql.py new file mode 100644 index 0000000..f9a8196 --- /dev/null +++ b/migrations/0001_db_mysql.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +# Add table Migrations +# Set initial database_version + +steps = [ +""" +CREATE TABLE IF NOT EXISTS Migrations( + database_version INT) +""", +""" +INSERT INTO Migrations (database_version) VALUES (1) +""" +] + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 diff --git a/migrations/0001_db_sqlite.py b/migrations/0001_db_sqlite.py new file mode 100644 index 0000000..5e18fe3 --- /dev/null +++ b/migrations/0001_db_sqlite.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +# Add table Migrations +# Set initial database_version + +steps = [ +""" +CREATE TABLE IF NOT EXISTS Migrations( + database_version INT) +""", +""" +INSERT INTO Migrations (database_version) VALUES (1) +""" +] + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/migrations/__init__.py b/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/public_html/coalition.css b/public_html/coalition.css deleted file mode 100644 index f32a222..0000000 --- a/public_html/coalition.css +++ /dev/null @@ -1,464 +0,0 @@ -body -{ - text-decoration: none; - color: black; - font-family: Verdana; - font-size: 10pt; - font-weight: normal; - background-color: #b0b0b0; - background-image: url(bg-gradient.png); - background-repeat: repeat-x; - margin: 0; -} - -input -{ - border: #000000 solid 1px; - font-size: 12px; - color: #000000; -} - -h2 -{ - font-size: 12pt; -} - -#parents -{ - position: fixed; - left: 10px; - top: 92px; - background-color: #b0b0b0; - padding-left: 10; - padding-right: 10; - font-size: 8pt; - border-top:1px solid #000000; - border-left:1px solid #000000; - border-right:1px solid #888888; - border-bottom:1px solid #888888; -} - -#parents a:link -{ - color: #222222; - text-decoration: none; - font-weight: bold; -} - -#jobs, #workers, #logs, #activities -{ - position: fixed; - left: 10px; - right: 10px; - top: 110px; - bottom: 152px; - overflow: auto; - border-top:1px solid #888888; - border-left:1px solid #888888; - border-right:1px solid #000000; - border-bottom:1px solid #000000; -} - -#workers, #logs, #activities -{ - top: 95px; - bottom: 58px; -} - -#logs -{ - color: White; - bottom: 10px; -} - -#workersTab -{ -} - -#jobsTable, #workersTable, #activitiesTable -{ - width:100%; - border-collapse: collapse; - text-align: right; -} - -#jobsTable td, #workersTable td, #activitiesTable td -{ - border-right:1px solid black; -} - -#jobsTable td a:link, #workersTable td a:link, #activitiesTable td a:link -{ - color: #DDDDDD; -} - -#jobsTable th, #workersTable th, #activitiesTable th -{ - text-align: center; - border-right:1px solid black; -} - -.entry, .entry0, .entry1, .title, .entry0Selected, .entry1Selected -{ - font-size: 8pt; - color: #DDDDDD; -} - -.title -{ - background-color: #666666; - color: #EEEEEE; -} - -.title a:link -{ - color: #EEEEEE; - text-decoration: none; -} - -.entry0 -{ - background-color: #222222; -} - -.entry1 -{ - background-color: #303030; -} - -.entry0Selected -{ - background-color: #444466; -} - -.entry1Selected -{ - background-color: #555577; -} - -.FINISHED, .PAUSED, .ERROR, .TIMEOUT, .WAITING, .WORKING, .ACTIVEtrue, .ACTIVEfalse -{ - text-align: center; - color: White; -} - -.FINISHED -{ - background-color: #009000; -} - -.PAUSED -{ - background-color: #000090; -} - -.ERROR, .TIMEOUT -{ - background-color: #900000; -} - -.WORKING -{ - background-color: #009090; -} - -.ACTIVEtrue -{ - background-color: #009000; -} - -.ACTIVEfalse -{ - background-color: #900000; -} - -.logs -{ - font-size: 8pt; -} - -#main -{ - position: fixed; - bottom: 5px; - top: 60px; - border-width:1; - border-style:solid; - border-color:Black; - background: #444444; - left: 5px; - right: 5px; -} - -#header, #footer -{ - width: 100%; -} - -#maintitle -{ - display: inline; - font-family: Verdana; - text-transform: lowercase; - margin-left: 5px; -} - -#maintitle a -{ - color: #444444; - font-size: xx-large; - font-weight: bolder; - display: inline; - text-decoration: none; -} - -#subtitle -{ - color: #444444; - font-size: small; - font-style: italic; - display: inline; -} - -#subtitle a -{ - color: #444444; - font-weight: bold; - text-decoration: none; - display: inline; -} - -#tabs -{ - position: fixed; - top: 35; - height: 21; - display: table-row; - z-index: 1000; - border-spacing: 5; -} - -.activetab, .unactivetab -{ - display: table-cell; - padding-left : 10; - padding-right : 10; - background: #444444; - color: White; - height:21; - vertical-align: middle; - border-top-style: solid; - border-top-color: Black; - border-top-width: 1; - border-left-style: solid; - border-left-color: Black; - border-left-width: 1; - border-right-style: solid; - border-right-color: Black; - border-right-width: 1; - cursor: pointer; -} - -.unactivetab -{ - background: #333333; - border-bottom-style: solid; - border-bottom-color: Black; - border-bottom-width: 1; -} - -.refreshtools -{ - display: inline; - position: fixed; - width: 160; - top: 5px; - right: 5px; -} - -.refreshbutton, .refreshing -{ - display: inline; - background: #DDDDDD; - cursor: pointer; - border: solid 1px Black; - position: fixed; - width: 100; - right: 5px; - text-align: center; - font-weight: bold; - height: 50; - border-style: solid; - border-color: Black; - border-width: 1; -} - -.refreshing -{ - background: #00F000; -} - -#jobtools, #workertools, #logtools, #activitiestools -{ - position: fixed; - left: 10px; - top: 65px; - color: #dddddd; - cursor: pointer; -} - -.button -{ - display: inline; - padding: 4px; - border: solid 1px #444444; -} - -.button img -{ - position: relative; - top: 3; -} - -#b02:hover, #b03:hover, #b04:hover, #b05:hover, #b06:hover, #b07:hover, #b08:hover, #b09:hover, #b10:hover, #b11:hover, #b12:hover -{ - border-top: solid 1px #777777; - border-left: solid 1px #777777; - border-right: solid 1px #222222; - border-bottom: solid 1px #222222; -} - -.buttonSep -{ - display: inline; - width: 10px; -} - -#toolstable -{ - position: fixed; - bottom: 8px; - height: 86px; - left: 8px; - right: 8px; -} - -#workertoolstable -{ - position: fixed; - bottom: 8px; - height: 43px; - left: 8px; - right: 8px; -} - -.ttr1 -{ - width: 10%; - font-size: small; - color: White; - text-align:right; -} -.ttr2 -{ - width: 60%; -} -.ttr3 -{ - width: 10%; - font-size: small; - color: White; - text-align:right; -} -.ttr4 -{ -} -.ttedit -{ - width: 100%; - border: 0; -} - -#toolbuttons -{ - height: 100%; - text-align:center; -} - -.toolbutton -{ - width: 100%; - height: 40; - font-weight: bold; - cursor: pointer; -} - -.headerCell -{ - cursor: pointer; -} - -.loadbar -{ - background: #009000; -} - -.loadlabel -{ - width:100%; - text-align: center; - position: absolute; - top: 1; -} - -.load -{ - width:100%; - position: relative; -} - -.membar -{ - height: 16; - background: #009000; -} - -.memlabel -{ - width:100%; - text-align: center; - position: absolute; - top: 1; -} - -.mem -{ - width:100%; - position: relative; -} - -.lprogressbar -{ - height: 16; - background: #009000; -} - -.gprogressbar -{ - height: 16; - background: #007000; -} - -.progresslabel -{ - width:100%; - text-align: center; - position: absolute; - top: 1; -} - -.progress -{ - width:100%; - position: relative; -} diff --git a/public_html/coalition.js b/public_html/coalition.js deleted file mode 100644 index e95e97e..0000000 --- a/public_html/coalition.js +++ /dev/null @@ -1,1381 +0,0 @@ -var xmlrpc; -var timer; -var page = "jobs"; -var viewJob = 0; -var logId = 0; -var jobs = []; -var selectedJobs = {}; -var cutJobs = {}; -var selectedWorkers = {}; -var selectedActivities = {}; -var workers = []; -var parents = {}; -var activities = []; -var jobsSortKey = "ID"; -var jobsSortKeyToUpper = true; -var workersSortKey = "Name"; -var workersSortKeyToUpper = true; -var activitiesSortKey = "Start"; -var activitiesSortKeyToUpper = false; -var selectionStart = 0; -var showTools = true; - -function setJobKey (id) -{ - // Same key ? - if (jobsSortKey == id) - jobsSortKeyToUpper = !jobsSortKeyToUpper; - else - { - jobsSortKey = id; - jobsSortKeyToUpper = true; - } - renderJobs (); -} - -function setWorkerKey (id) -{ - // Same key ? - if (workersSortKey == id) - workersSortKeyToUpper = !workersSortKeyToUpper; - else - { - workersSortKey = id; - workersSortKeyToUpper = true; - } - renderWorkers (); -} - -function setActivityKey (id) -{ - // Same key ? - if (activitiesSortKey == id) - activitiesSortKeyToUpper = !activitiesSortKeyToUpper; - else - { - activitiesSortKey = id; - activitiesSortKeyToUpper = true; - } - renderActivities (); -} - -function get_cookie ( cookie_name ) -{ - var results = document.cookie.match ( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)' ); - - if ( results ) - return ( unescape ( results[2] ) ); - else - return ""; -} - -$(document).ready(function() -{ - reloadJobs (); - reloadWorkers (); - reloadActivities (); - showJobs (); - timer=setTimeout(timerCB,4000); -}); - -function showHideTools () -{ - showTools = !showTools; - updateTools (); -} - -function updateTools () -{ - if (!showTools) - { - $("#tools").show (); - $("#jobtools").hide (); - $("#workertools").hide (); - $("#showhidetools").show (); - } - else if (page == "jobs") - { - $("#tools").show (); - $("#jobtools").show (); - $("#workertools").hide (); - } - else if (page == "workers") - { - $("#tools").show (); - $("#jobtools").hide (); - $("#workertools").show (); - } - else - { - $("#tools").hide (); - $("#jobtools").hide (); - $("#workertools").hide (); - } -} - -function goToJob (jobId) -{ - viewJob = jobId; - reloadJobs (); -} - -function showLog () -{ - $("#jobsTab").hide (); - $("#workersTab").hide (); - $("#activitiesTab").hide (); - $("#logsTab").show (); - document.getElementById("jobtab").className = "unactivetab"; - document.getElementById("workertab").className = "unactivetab"; - document.getElementById("activitytab").className = "unactivetab"; - document.getElementById("logtab").className = "activetab"; - - page = "logs"; - updateTools (); -} - -function clearLog () -{ - $("#logs").empty (); -} - -function renderLog (jobId) -{ - showLog (); - logId = jobId; - - $.ajax({ type: "GET", url: "/json/getlog", data: "id="+str(jobId), dataType: "json", success: - function (data) - { - $("#logs").empty(); - $("#logs").append("

Logs for job "+jobId+":

"+data+"
"); - - page = "logs"; - updateTools (); - document.getElementById("refreshbutton").className = "refreshbutton"; - } - }); -} - -function clearWorkers () -{ - if (confirm("Do you really want to clear all the workers?")) - { - var _data = ""; - for (j=workers.length-1; j >= 0; j--) - { - var worker = workers[j]; - if (selectedWorkers[worker.Name]) - _data += "id="+str(worker.Name)+"&"; - } - $.ajax({ type: "GET", url: "/json/clearworkers", data: _data, dataType: "json", success: - function () - { - selectedWorkers = {} - reloadWorkers (); - updateWorkerProps (); - } - }); - } -} - -function formatDate (_date) -{ - var date = new Date(_date*1000) - return date.getFullYear() + '/' + (date.getMonth()+1) + '/' + date.getDate() + ' ' + date.getHours () + ':' + date.getMinutes () + ':' + date.getSeconds(); -} - -function formatDuration (secondes) -{ - var days = Math.floor (secondes / (60*60*24)); - var hours = Math.floor ((secondes-days*60*60*24) / (60*60)); - var minutes = Math.floor ((secondes-days*60*60*24-hours*60*60) / 60); - var secondes = Math.floor (secondes-days*60*60*24-hours*60*60-minutes*60); - if (days > 0) - return days + " d " + hours + " h " + minutes + " m " + secondes + " s"; - if (hours > 0) - return hours + " h " + minutes + " m " + secondes + " s"; - if (minutes > 0) - return minutes + " m " + secondes + " s"; - return secondes + " s"; -} - -// Timer callback -function timerCB () -{ - if (document.getElementById("autorefresh").checked) - refresh (); - - // Fire a new time event - timer=setTimeout(timerCB,4000); -} - -function refresh () -{ - document.getElementById("refreshbutton").className = "refreshing"; - if (page == "jobs") - reloadJobs (); - else if (page == "workers") - reloadWorkers (); - else if (page == "activities") - reloadActivities (); - else if (page == "logs") - renderLog (logId); -} - -function compareStrings (a,b,toupper) -{ - if (a < b) - return toupper ? -1 : 1; - if (a == b) - return 0; - return toupper ? 1 : -1; -} - -function compareNumbers (a,b,toupper) -{ - return toupper ? a-b : b-a; -} - -function showJobs () -{ - $("#jobsTab").show (); - $("#workersTab").hide (); - $("#activitiesTab").hide (); - $("#logsTab").hide (); - document.getElementById("jobtab").className = "activetab"; - document.getElementById("workertab").className = "unactivetab"; - document.getElementById("activitytab").className = "unactivetab"; - document.getElementById("logtab").className = "unactivetab"; - - page = "jobs"; - updateTools (); -} - -// Returns the HTML code for a job title column -function addSumEmpty (str) -{ - if (str == undefined) - return ""; - else - return "" + str + ""; -} - -// Returns the HTML code for a job title column -function addSum (inputs, attribute) -{ - var sum = 0; - for (i=0; i < inputs.length; i++) - { - var job = inputs[i]; - sum += job[attribute]; - } - return "" + sum + ""; -} - -// Returns the HTML code for a job title column -function addSumFinished (inputs, attribute) -{ - var sum = 0; - for (i=0; i < inputs.length; i++) - { - var job = inputs[i]; - if (job[attribute] == "FINISHED") - sum ++; - } - return "" + sum + " FINISHED"; -} - -// Average -function addSumAvgDuration (inputs, attribute) -{ - var sum = 0; - var count = 0; - for (i=0; i < inputs.length; i++) - { - var job = inputs[i]; - sum += job[attribute]; - count++; - } - if (count > 0) - return "Avg " + formatDuration (sum/count) + ""; - else - return ""; -} - -// Returns the HTML code for a job title column -function addSumSimple (inputs) -{ - return "" + inputs.length + " jobs"; -} - -// Render the current jobs -function renderJobs () -{ - $("#jobs").empty (); - $("#parents").empty (); - var table = ""; - - function getJobProgress (job) - { - if (job.Total > 0) - { - // A bar div - lProgress = job.TotalFinished / job.Total; - gProgress = job.TotalFinished / job.Total; - } - else - { - gProgress = job.State == "FINISHED" ? - 1.0 : - ( - job.GlobalProgress == null ? - 0.0 : - parseFloat(job.GlobalProgress) - ); - lProgress = job.State == "FINISHED" ? - 1.0 : - ( - job.LocalProgress == null ? - gProgress : - parseFloat(job.LocalProgress) - ); - } - return lProgress, gProgress; - } - - function _sort (a,b) - { - if (jobsSortKey == "Progress") - { - var lProgressA, gProgressA = getJobProgress (a); - var lProgressB, gProgressB = getJobProgress (b); - return compareNumbers (gProgressA, gProgressB, jobsSortKeyToUpper); - } - else - { - var aValue = a[jobsSortKey]; - if (typeof aValue == 'string') - return compareStrings (aValue, b[jobsSortKey], jobsSortKeyToUpper); - else - return compareNumbers (aValue, b[jobsSortKey], jobsSortKeyToUpper); - } - } - - jobs.sort (_sort); - - for (i=0; i < parents.length; i++) - { - var parent = parents[i]; - $("#parents").append((i == 0 ? "" : " > ") + ("" + parent.Title + "")); - } - - // Returns the HTML code for a job title column - function addTitleHTMLEx (attribute, alias) - { - table += ""; - } - - function addTitleHTML (attribute) - { - addTitleHTMLEx (attribute, attribute) - } - - table += ""; - //addTitleHTML ("Order"); - addTitleHTML ("ID"); - addTitleHTML ("Title"); - addTitleHTML ("URL"); - addTitleHTML ("User"); - addTitleHTML ("State"); - addTitleHTML ("Priority"); - addTitleHTMLEx ("TotalFinished", "Ok"); - addTitleHTMLEx ("TotalWorking", "Wrk"); - addTitleHTMLEx ("TotalErrors", "Err"); - addTitleHTML ("Total"); - addTitleHTML ("Progress"); - addTitleHTML ("Affinity"); - addTitleHTML ("TimeOut"); - addTitleHTML ("Worker"); - addTitleHTML ("StartTime"); - addTitleHTML ("Duration"); - addTitleHTML ("Try"); - addTitleHTML ("Command"); - addTitleHTML ("Dir"); - addTitleHTML ("Dependencies"); - table += "\n"; - - for (i=0; i < jobs.length; i++) - { - var job = jobs[i]; - - var mouseDownEvent = "onMouseDown='onClickList(event,"+i+")' onDblClick='onDblClickList(event,"+i+")'"; - - table += ""; - function addTD (attr) - { - table += ""; - } - //addTD (job.Order); - addTD (job.ID); - table += "\n"; - - // URL - if (job.URL != "") - addTD ("Open") - else - addTD ("") - - addTD (job.User); - table += ""; - addTD (job.Priority); - if (job.Total > 0) - { - table += ""; - table += ""; - table += ""; - table += ""; - } - else - { - addTD (""); - addTD (""); - addTD (""); - addTD (""); - } - - // *** Progress bar - var progress = "" - var lProgress, gProgress = getJobProgress (job) - lProgress = Math.floor(lProgress*100.0); - gProgress = Math.floor(gProgress*100.0); - - // A bar div - progress = "
"; - progress += "
"; - progress += "
" + gProgress + "%
"; - progress += "
"; - - addTD (progress); - addTD (job.Affinity); - addTD (job.TimeOut); - addTD (job.Worker); - addTD (formatDate (job.StartTime)); - addTD (formatDuration (job.Duration)); - addTD (job.Try+"/"+job.Retry); - addTD (job.Command); - addTD (job.Dir); - // Compute the dependencies - var deps = ""; - var j; - for (j = 0; j < job.Dependencies.length; j++) - { - deps += job.Dependencies[j] + " "; - } - addTD (deps); - - table += "
\n"; - } - - // Footer - table += ""; - - table += addSumEmpty ("TOTAL"); - table += addSumSimple (jobs); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumFinished (jobs, "State"); - table += addSumEmpty (); - table += addSum (jobs, "TotalFinished"); - table += addSum (jobs, "TotalWorking"); - table += addSum (jobs, "TotalErrors"); - table += addSum (jobs, "Total"); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumAvgDuration (jobs, "Duration"); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumEmpty (); - table += addSumEmpty (); - table += "\n"; - - table += "
"; - var value = jobs[0]; - if (value) - { - table += alias; - if (attribute == jobsSortKey && jobsSortKeyToUpper) - table += " ↓"; - if (attribute == jobsSortKey && !jobsSortKeyToUpper) - table += " ↑"; - } - else - table += attribute; - table += "
" + attr + "" + job.Title + ""+job.State+""+job.TotalFinished+""+job.TotalWorking+""+job.TotalErrors+""+job.Total+"
"; - $("#jobs").append(table); -} - -function logSelection () -{ - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - renderLog (job.ID); - } -} - -// Ask the server for the jobs and render them -function reloadJobs () -{ - var tag = document.getElementById("filterJobs").value; - tag = tag == "NONE" ? "" : tag; - $.ajax({ type: "GET", url: "/json/getjobs", data: "filter="+tag+"&id="+str(viewJob), dataType: "json", success: - function(data) - { - jobs = [] - for (j = 0; j < data.Jobs.length; ++j) - { - var job = data.Jobs[j]; - var nj = {}; - for (i = 0; i < data.Vars.length; ++i) - { - nj[data.Vars[i]] = job[i]; - } - jobs.push (nj); - } - parents = data.Parents; - renderJobs (); - document.getElementById("refreshbutton").className = "refreshbutton"; - }, - error : - function (jqXHR, textStatus, errorThrown) - { - alert("JQuery error : " + textStatus); - } - }); -} - -function startWorkers () -{ - var _data = ""; - for (j=workers.length-1; j >= 0; j--) - { - var worker = workers[j]; - if (selectedWorkers[worker.Name]) - _data += "id="+worker.Name+"&"; - } - $.ajax({ type: "GET", url: "/json/startworkers", data: _data, dataType: "json", success: - function () - { - reloadWorkers (); - } - }); -} - -function stopWorkers () -{ - var _data = ""; - for (j=workers.length-1; j >= 0; j--) - { - var worker = workers[j]; - if (selectedWorkers[worker.Name]) - _data += "id="+worker.Name+"&"; - } - $.ajax({ type: "GET", url: "/json/stopworkers", data: _data, dataType: "json", success: - function () - { - reloadWorkers (); - } - }); -} - -function workerActivity () -{ - var _data = ""; - for (j=workers.length-1; j >= 0; j--) - { - var worker = workers[j]; - if (selectedWorkers[worker.Name]) - { - title:$('#activityWorker').attr("value", worker.Name) - title:$('#activityJob').attr("value", "") - break; - } - } - - reloadActivities () - page = "activities" - showActivities () -} - -function jobActivity () -{ - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - { - title:$('#activityWorker').attr("value", "") - title:$('#activityJob').attr("value", job.ID) - break; - } - } - - reloadActivities () - page = "activities" - showActivities () -} - -var MultipleSelection = {} -function checkSelectionProperties (list, props, selectedList, idName) -{ - var values = [] - - for (i = 0; i < list.length; i++) - { - var item = list[i]; - if (selectedList[item[idName]]) - { - for (j = 0; j < props.length; ++j) - { - var value = item[props[j][0]]; - if (values[j] != null && values[j] != value) - values[j] = MultipleSelection; - else - values[j] = value; - } - } - } - - for (i = 0; i < props.length; ++i) - { - if (values[i] == MultipleSelection) - { - // different values - $('#'+props[i][1]).css("background-color", "orange"); - $('#'+props[i][1]).attr("value", ""); - } - else if (values[i] == null) - { - // default value - $('#'+props[i][1]).css("background-color", "white"); - $('#'+props[i][1]).attr("value", props[i][2]); - } - else - { - // unique values - $('#'+props[i][1]).css("background-color", "white"); - $('#'+props[i][1]).attr("value", values[i]); - } - } - return values; -} - -function updateSelectionProp (values, props, prop) -{ - for (i = 0; i < props.length; ++i) - if (props[i][1] == prop) - { - values[i] = true; - $('#'+props[i][1]).css("background-color", "greenyellow"); - break; - } -} - -function sendSelectionPropChanges (list, idName, values, props, _url, selectedList, func) -{ - var _data = ""; - for (j=list.length-1; j >= 0; j--) - { - var id = list[j][idName]; - if (selectedList[id]) - _data += "id="+str(id)+"&"; - } - - for (i = 0; i < props.length; ++i) - if (values[i] == true) - { - var prop = props[i][0]; - var value = $('#'+props[i][1]).attr("value"); - _data += "prop="+prop+"&value="+value+"&"; - } - - // One single call - $.ajax({ type: "GET", url: _url, data: _data, dataType: "json", success: - function () - { - for (i = 0; i < props.length; ++i) - if (values[i] == true) - { - props[i][2] = value; - } - func (); - } - }); -} - -function setSelectionDefaultProperties (props) -{ - for (i = 0; i < props.length; ++i) - props[i][2] = $('#'+props[i][1]).attr("value"); -} - -var WorkerProps = -[ - [ "Affinity", "waffinity", "" ], -]; -var updatedWorkerProps = {} - -function updateWorkerProps () -{ - updatedWorkerProps = checkSelectionProperties (workers, WorkerProps, selectedWorkers, "Name"); -} - -function onchangeworkerprop (prop) -{ - updateSelectionProp (updatedWorkerProps, WorkerProps, prop); -} - -function updateworkers () -{ - sendSelectionPropChanges (workers, 'Name', updatedWorkerProps, WorkerProps, "/json/updateworkers", selectedWorkers, - function () - { - reloadWorkers (); - } - ); -} - -function reloadWorkers () -{ - $.ajax({ type: "GET", url: "/json/getworkers", dataType: "json", success: - function (data) - { - workers = []; - for (j = 0; j < data.Workers.length; ++j) - { - var worker = data.Workers[j]; - var nw = {}; - for (i = 0; i < data.Vars.length; ++i) - { - nw[data.Vars[i]] = worker[i]; - } - workers.push (nw); - } - renderWorkers (); - document.getElementById("refreshbutton").className = "refreshbutton"; - } - }); -} - -function showWorkers () -{ - $("#jobsTab").hide (); - $("#workersTab").show (); - $("#activitiesTab").hide (); - $("#logsTab").hide (); - document.getElementById("jobtab").className = "unactivetab"; - document.getElementById("workertab").className = "activetab"; - document.getElementById("activitytab").className = "unactivetab"; - document.getElementById("logtab").className = "unactivetab"; - - page = "workers"; - updateTools (); -} - -function renderWorkers () -{ - $("#workers").empty (); - - var table = ""; - table += "\n"; - - // Returns the HTML code for a worker title column - function addTitleHTML (attribute) - { - table += ""; - } - - addTitleHTML ("Name"); - addTitleHTML ("Active"); - addTitleHTML ("State"); - addTitleHTML ("Affinity"); - addTitleHTML ("Load"); - addTitleHTML ("Memory"); - addTitleHTML ("LastJob"); - addTitleHTML ("Finished"); - addTitleHTML ("Error"); - addTitleHTML ("IP"); - - table += "\n"; - - function _sort (a,b) - { - var aValue = a[workersSortKey]; - if (typeof aValue == 'string') - return compareStrings (aValue, b[workersSortKey], workersSortKeyToUpper); - else - return compareNumbers (aValue, b[workersSortKey], workersSortKeyToUpper); - } - - workers.sort (_sort); - - for (i=0; i < workers.length; i++) - { - var worker = workers[i]; - - // *** Build the load tab for this worker - // A global div - var load = "
"; - // Add each CPU load - var loadValue = 0; - for (j=0; j < worker.Load.length; j++) - { - load += "
"; - loadValue += worker.Load[j] - } - - // Add the numerical value of the load - load += "
" + Math.floor(loadValue/worker.Load.length) + "%
"; - load += "
"; - - // *** Build the memory tab for this worker - var memory = "
"; - memory += "
"; - - function formatMem (a) - { - if (a > 1024) - return Math.round(a/1024*100)/100 + " GB"; - else - return str(a) + " Mo"; - } - - memLabel = formatMem (worker.TotalMemory-worker.FreeMemory); - memLabel += " / "; - memLabel += formatMem (worker.TotalMemory); - - // Add the numerical value of the mem - memory += "
" + memLabel + "
"; - memory += "
"; - - table += "
"+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - "\n"; - } - table += "
"; - var value = workers[0]; - if (value && value[attribute] != null) - { - table += attribute; - if (attribute == workersSortKey && workersSortKeyToUpper) - table += " ↓"; - if (attribute == workersSortKey && !workersSortKeyToUpper) - table += " ↑"; - } - else - table += attribute; - table += "
"+worker.Name+""+worker.Active+""+worker.State+""+worker.Affinity+""+load+""+memory+""+worker.LastJob+""+worker.Finished+""+worker.Error+""+worker.IP+"
"; - $("#workers").append(table); - $("#workers").append("
"); -} - -function reloadActivities () -{ - var _data = {} - var job = $('#activityJob').attr("value") - if (job != "") - _data.job = job - var worker = $('#activityWorker').attr("value") - if (worker != "") - _data.worker = worker - _data.howlong = $('#howlong').attr("value") - $.ajax({ type: "GET", url: "/json/getactivities", data: _data, dataType: "json", success: - function (data) - { - activities = []; - for (j = 0; j < data.Activities.length; ++j) - { - var _activities = data.Activities[j]; - var nw = {}; - for (i = 0; i < data.Vars.length; ++i) - { - nw[data.Vars[i]] = _activities[i]; - } - activities.push (nw); - } - renderActivities (); - document.getElementById("refreshbutton").className = "refreshbutton"; - } - }); -} - -function showActivities () -{ - $("#jobsTab").hide (); - $("#workersTab").hide (); - $("#activitiesTab").show (); - $("#logsTab").hide (); - document.getElementById("jobtab").className = "unactivetab"; - document.getElementById("workertab").className = "unactivetab"; - document.getElementById("activitytab").className = "activetab"; - document.getElementById("logtab").className = "unactivetab"; - - page = "activities"; - updateTools (); -} - -function renderActivities () -{ - $("#activities").empty (); - - var table = ""; - table += "\n"; - - // Returns the HTML code for a worker title column - function addTitleHTML (attribute) - { - table += ""; - } - - addTitleHTML ("Start"); - addTitleHTML ("JobID"); - addTitleHTML ("JobTitle"); - addTitleHTML ("State"); - addTitleHTML ("Worker"); - addTitleHTML ("Duration"); - - table += "\n"; - - function _sort (a,b) - { - var aValue = a[activitiesSortKey]; - if (typeof aValue == 'string') - return compareStrings (aValue, b[activitiesSortKey], activitiesSortKeyToUpper); - else - return compareNumbers (aValue, b[activitiesSortKey], activitiesSortKeyToUpper); - } - - activities.sort (_sort); - - for (i=0; i < activities.length; i++) - { - var activity = activities[i]; - - date = formatDate (activity.Start); - dura = formatDuration (activity.Duration); - - var mouseDownEvent = "onMouseDown='onClickList(event,"+i+")' onDblClick='onDblClickList(event,"+i+")'"; - table += ""+ - // table += ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - ""+ - "\n"; - } - - // Footer - table += ""; - table += addSumEmpty ("TOTAL"); - table += addSumSimple (activities); - table += addSumEmpty (); - table += addSumFinished (activities, "State"); - table += addSumEmpty (); - table += addSumAvgDuration (activities, "Duration"); - table += "\n"; - - table += "
"; - var value = activities[0]; - if (value && value[attribute] != null) - { - table += attribute; - if (attribute == activitiesSortKey && activitiesSortKeyToUpper) - table += " ↓"; - if (attribute == activitiesSortKey && !activitiesSortKeyToUpper) - table += " ↑"; - } - else - table += attribute; - table += "
"+date+""+activity.JobID+""+activity.JobTitle+""+activity.State+""+activity.Worker+""+dura+"
"; - $("#activities").append(table); - $("#activities").append("
"); -} - -var JobProps = -[ - [ "Title", "title", "" ], - [ "Command", "cmd", "" ], - [ "Dir", "dir", "." ], - [ "Priority", "priority", "1000" ], - [ "Affinity", "affinity", "" ], - [ "TimeOut", "timeout", "0" ], - [ "Dependencies", "dependencies", "" ], - [ "Retry", "retry", "10" ], - [ "User", "user", "" ], - [ "URL", "url", "" ], - [ "Environment", "env", "" ] -]; -var updatedJobProps = {} - -function onchangejobprop (prop) -{ - updateSelectionProp (updatedJobProps, JobProps, prop); -} - -function updatejobs () -{ - sendSelectionPropChanges (jobs, 'ID', updatedJobProps, JobProps, "/json/updatejobs", selectedJobs, - function () - { - reloadJobs (); - updateJobProps (); - } - ); -} - -function addjob () -{ - var _data = { - title:$('#title').attr("value"), - cmd:$('#cmd').attr("value"), - dir:$('#dir').attr("value"), - env:$('#env').attr("value"), - priority:$('#priority').attr("value"), - retry:$('#retry').attr("value"), - timeout:$('#timeout').attr("value"), - affinity:$('#affinity').attr("value"), - dependencies:$('#dependencies').attr("value"), - user:$('#user').attr("value"), - url:$('#url').attr("value"), - parent:viewJob - }; - $.ajax({ type: "GET", url: "/xmlrpc/addjob", data: _data, dataType: "json", success: - function () - { - setSelectionDefaultProperties (JobProps); - reloadJobs (); - } - }); -} - -function selectJobs () -{ - var tag = document.getElementById("selectJobs").value; - if (tag == "CUSTOM") - ; - else if (tag == "NONE") - selectAll (false); - else if (tag == "ALL") - selectAll (true); - else - selectAll (true, tag); -} - -function onDblClickList (e, i) -{ - if (page == "activities") - { - var activity = activities[i]; - renderLog (activity.JobID); - } - else - { - var job = jobs[i]; - job.Command != "" ? renderLog (job.ID) : goToJob (job.ID); - } -} - -// List selection handler -function onClickList (e, i) -{ - if (!e) var e = window.event - - document.getElementById("selectJobs").value = "CUSTOM"; - - // Unselect if not ctrl keys - if (!e.ctrlKey) - { - if (page == "jobs") - { - selectedJobs = {}; - } - else if (page == "workers") - selectedWorkers = {}; - else if (page == "activities") - selectedActivities = {}; - } - - var thelist; - var selectedList; - var idName; - var tableId; - if (page == "jobs") - { - thelist = jobs; - selectedList = selectedJobs; - idName = "ID"; - tableId = "jobtable"; - } - else if (page == "workers") - { - thelist = workers; - selectedList = selectedWorkers; - idName = "Name"; - tableId = "workertable"; - } - else if (page == "activities") - { - thelist = activities; - selectedList = selectedActivities; - idName = "ID"; - tableId = "activitytable"; - } - else - return; - - // Unselect if not ctrl keys - if (!e.ctrlKey) - { - for (j=0; j < thelist.length; j++) - document.getElementById(tableId+j).className = "entry"+(j%2); - } - - var begin = e.shiftKey ? Math.min (selectionStart, i) : i - var end = e.shiftKey ? Math.max (selectionStart, i) : i - - selectionStart = e.shiftKey ? selectionStart : i; - - for (j = begin; j <= end; j++) - { - var item = thelist[j]; - if (item) - { - var selected = e.ctrlKey ? !selectedList[item[idName]] : true; - selectedList[item[idName]] = selected; - document.getElementById(tableId+j).className = "entry"+(j%2)+(selected?"Selected":""); - } - } - - if (page == "jobs") { updateJobProps (); } - else if (page == "workers") { updateWorkerProps (); } - - // Remove selection - window.getSelection ().removeAllRanges(); -} - -function selectAll (state, filter) -{ - var thelist; - var selectedList; - var idName; - var tableId; - if (page == "jobs") - { - thelist = jobs; - selectedJobs = {}; - selectedList = selectedJobs; - idName = "ID"; - tableId = "jobtable"; - } - else if (page == "workers") - { - thelist = workers; - selectedWorkers = {}; - selectedList = selectedWorkers; - idName = "Name"; - tableId = "workertable"; - } - else - return; - - if (!state) - { - for (j=0; j < thelist.length; j++) - document.getElementById(tableId+j).className = "entry"+(j%2); - } - else - { - for (j=0; j < thelist.length; j++) - { - var item = thelist[j]; - if (filter == null || item.State == filter) - { - selectedList[item[idName]] = true; - document.getElementById(tableId+j).className = "entry"+(j%2)+"Selected"; - } - else - { - selectedList[item[idName]] = false; - document.getElementById(tableId+j).className = "entry"+(j%2); - } - } - } - - if (page == "jobs") { updateJobProps (); } - else if (page == "workers") { updateWorkerProps (); } -} - -function removeSelection () -{ - if (confirm("Do you really want to remove the selected jobs ?")) - { - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - _data += "id="+str(job.ID)+"&"; - } - $.ajax({ type: "GET", url: "/json/clearjobs", data: _data, dataType: "json", success: - function () - { - selectedJobs = {}; - reloadJobs (); - updateJobProps (); - } - }); - } -} - -function startSelection () -{ - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - _data += "id="+str(job.ID)+"&"; - } - $.ajax({ type: "GET", url: "/json/startjobs", data: _data, dataType: "json", success: - function () - { - reloadJobs (); - } - }); -} - -function viewSelection() -{ - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID] && job.URL) - window.open(job.URL); - } -} - -function resetSelection () -{ - if (confirm("Do you really want to reset the selected jobs and all their children jobs ?")) - { - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - _data += "id="+str(job.ID)+"&"; - } - $.ajax({ type: "GET", url: "/json/resetjobs", data: _data, dataType: "json", success: - function () - { - reloadJobs (); - } - }); - } -} - -function resetErrorSelection () -{ - if (confirm("Do you really want to reset the selected jobs and all their children jobs tagged in ERROR ?")) - { - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - _data += "id="+str(job.ID)+"&"; - } - $.ajax({ type: "GET", url: "/json/reseterrorjobs", data: _data, dataType: "json", success: - function () - { - reloadJobs (); - } - }); - } -} - -function pauseSelection () -{ - var _data = ""; - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - _data += "id="+str(job.ID)+"&"; - } - $.ajax({ type: "GET", url: "/json/pausejobs", data: _data, dataType: "json", success: - function () - { - reloadJobs (); - } - }); -} - -function updateJobProps () -{ - updatedJobProps = checkSelectionProperties (jobs, JobProps, selectedJobs, "ID"); -} - -function exportCSV() -{ - window.open('csv.html?id=' + viewJob); -} - -function cutSelection () -{ - cutJobs = {} - for (j=jobs.length-1; j >= 0; j--) - { - var job = jobs[j]; - if (selectedJobs[job.ID]) - { - cutJobs[job.ID] = true - } - } - selectAll (false) -} - -function pasteSelection () -{ - var _data = "dest="+str(viewJob)+"&"; - var count = 0; - for (var id in cutJobs) - { - _data += "id="+str(id)+"&"; - } - - $.ajax({ type: "GET", url: "/json/movejobs", data: _data, dataType: "json", success: - function () - { - reloadJobs (); - } - }); -} diff --git a/public_html/css/coalition.css b/public_html/css/coalition.css new file mode 100644 index 0000000..ed4b54e --- /dev/null +++ b/public_html/css/coalition.css @@ -0,0 +1,850 @@ +html, body { + font-size: 90%; + height: 100%; + width: 100%; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +*, *:before, *:after { + box-sizing: inherit; +} + +body { + text-decoration: none; + color: ghostwhite; + font-family: Arial, Helvetica, sans-serif; + font-weight: normal; + background-color: #b0b0b0; +} + +input, select, textarea { + width: 100%; + border: 0; + font-size: 0.7rem; + background-color: #555; + color: ghostwhite; +} + +.button select { + width: auto; +} + +#parents { + left: 1rem; + background-color: #b0b0b0; + padding: 0 1rem; + justify-content: flex-start; + color: #444; + font-size: 0.9rem; + border-top:1px solid #000000; + border-left:1px solid #000000; + border-right:1px solid #888888; + border-bottom:1px solid #888888; +} + +#parents a:link { + color: #222222; + text-decoration: none; + font-weight: bold; + margin: 0 0.5rem; +} + +#jobs, #workers, #activities, #logs, #affinities +{ + flex-grow: 1; + height: 1rem; + /*border-top:1px solid #888888;*/ + /*border-left:1px solid #888888;*/ + /*border-right:1px solid #000000;*/ + /*border-bottom:1px solid #000000;*/ +} + +#workers, #logs, #activities +{ + /*top: 95px;*/ + /*bottom: 58px;*/ +} + +#affinities +{ + /*position: fixed;*/ + /*left: 10px;*/ + /*right: 10px;*/ + /*top: 65px;*/ + /*bottom: 58px;*/ + /*overflow: auto;*/ + /*border-top:1px solid #888888;*/ + /*border-left:1px solid #888888;*/ + /*border-right:1px solid #000000;*/ + /*border-bottom:1px solid #000000;*/ +} + +#logs { + color: white; +} + +#jobsTable, +#workersTable, +#activitiesTable, +#affinitiesTable +{ + width:100%; + border-collapse: collapse; + text-align: left; +} + +#config-jobs-table { + display: flex; +} + +#jobsTable td a:link, #workersTable td a:link, #activitiesTable td a:link { + color: #DDDDDD; +} + +#jobsTable thead { + min-height: 5rem; +} + +#jobsTable thead label { + cursor: pointer; +} + +#jobsTable thead .headerCell .flex-row { + justify-content: center; +} + +table { + height: 100%; + display: flex; + flex-direction: column; +} + +th, tr { + display: flex; + flex-direction: row; +} + +th { + border-right: 1px dotted gray; +} + +thead { + text-align: center; + overflow-y: scroll; +} + +tfoot { + overflow-y: scroll; + min-height: 1.5rem; +} + +tbody { + height: 100%; + overflow-y: scroll; + word-break: keep-all; +} + +tbody tr { + justify-content: flex-start; +} + +tbody td { + border-right: 1px dotted #444; + height: 1.2rem; + overflow: hidden; + white-space: nowrap; +} + + +/* id */ +#jobsTable th:nth-child(1), #jobsTable td:nth-child(1) { + width: 2%; +} + +/* title */ +#jobsTable th:nth-child(2), #jobsTable td:nth-child(2) { + width: 10%; +} + +/* url */ +#jobsTable th:nth-child(3), #jobsTable td:nth-child(3) { + width: 4%; +} + +/* user */ +#jobsTable th:nth-child(4), #jobsTable td:nth-child(4) { + width: 5%; +} + +/* state */ + +#jobsTable th:nth-child(5), #jobsTable td:nth-child(5) { + width: 6%; +} + +/* priority */ +#jobsTable th:nth-child(6), #jobsTable td:nth-child(6) { + width: 4%; +} + +/* ok */ +#jobsTable th:nth-child(7), #jobsTable td:nth-child(7) { + width: 2%; +} + +/* wkr */ +#jobsTable th:nth-child(8), #jobsTable td:nth-child(8) { + width: 2%; +} + +/* err */ +#jobsTable th:nth-child(9), #jobsTable td:nth-child(9) { + width: 3%; +} + +/* total */ +#jobsTable th:nth-child(10), #jobsTable td:nth-child(10) { + width: 3%; +} + +/* progress */ +#jobsTable th:nth-child(11), #jobsTable td:nth-child(11) { + width: 5%; +} + +/* affinity */ +#jobsTable th:nth-child(12), #jobsTable td:nth-child(12) { + width: 6%; +} + +/* timeout */ +#jobsTable th:nth-child(13), #jobsTable td:nth-child(13) { + width: 4%; +} + +/* worker */ +#jobsTable th:nth-child(14), #jobsTable td:nth-child(14) { + width: 7%; +} + +/* start_time */ +#jobsTable th:nth-child(15), #jobsTable td:nth-child(15) { + width: 6%; +} + +/* duration */ +#jobsTable th:nth-child(16), #jobsTable td:nth-child(16) { + width: 5%; +} + +/* run */ +#jobsTable th:nth-child(17), #jobsTable td:nth-child(17) { + width: 2%; +} + +/* command */ +#jobsTable th:nth-child(18), #jobsTable td:nth-child(18) { + width: 13%; +} + +/* dir */ +#jobsTable th:nth-child(19), #jobsTable td:nth-child(19) { + width: 3%; +} + +/* dependencies */ +#jobsTable th:nth-child(20), #jobsTable td:nth-child(20) { + width: 8%; +} + +/* name */ +#workersTable th:nth-child(1), #workersTable td:nth-child(1) { + width: 20%; +} + +/* active */ +#workersTable th:nth-child(2), #workersTable td:nth-child(2) { + width: 4%; +} + +/* state */ +#workersTable th:nth-child(3), #workersTable td:nth-child(3) { + width: 8%; +} + +/* affinity */ +#workersTable th:nth-child(4), #workersTable td:nth-child(4) { + width: 10%; +} + +/* ping_time */ +#workersTable th:nth-child(5), #workersTable td:nth-child(5) { + width: 10%; +} + +/* cpu */ +#workersTable th:nth-child(6), #workersTable td:nth-child(6) { + width: 11%; +} + +/* memory */ +#workersTable th:nth-child(7), #workersTable td:nth-child(7) { + width: 11%; +} + +/* last_jobs */ +#workersTable th:nth-child(8), #workersTable td:nth-child(8) { + width: 5%; +} + +/* finished */ +#workersTable th:nth-child(9), #workersTable td:nth-child(9) { + width: 5%; +} + +/* error */ +#workersTable th:nth-child(10), #workersTable td:nth-child(10) { + width: 5%; +} + +/* ip */ +#workersTable th:nth-child(11), #workersTable td:nth-child(11) { + width: 11%; +} + +/* start */ +#activitiesTable th:nth-child(1), #activitiesTable td:nth-child(1) { + width: 12%; +} + +/* job_id */ +#activitiesTable th:nth-child(2), #activitiesTable td:nth-child(2) { + width: 12%; +} + +/* jobs_title */ +#activitiesTable th:nth-child(3), #activitiesTable td:nth-child(3) { + width: 12%; +} + +/* state */ +#activitiesTable th:nth-child(4), #activitiesTable td:nth-child(4) { + width: 12%; +} + +/* worker */ +#activitiesTable th:nth-child(5), #activitiesTable td:nth-child(5) { + width: 12%; +} + +/* duration */ +#activitiesTable th:nth-child(6), #activitiesTable td:nth-child(6) { + width: 12%; +} + +/* id */ +#affinitiesTable th:nth-child(1), #affinitiesTable td:nth-child(1) { + width: 5%; +} + +/* name */ +#affinitiesTable th:nth-child(2), #affinitiesTable td:nth-child(2) { + width: 25%; +} + +#affinitiesTable +{ + border-collapse: collapse; + text-align: right; +} + +#affinitiesTable td +{ + /*font-size: 8pt; */ + font-size: 0.9rem; + text-align: center; + color: #FFFFFF; +} + +.entry, .entry0, .entry1, .title, .entry0Selected, .entry1Selected +{ + /*font-size: 8pt; */ + font-size: 0.9rem; + color: #DDDDDD; +} + +.title +{ + background-color: #666666; + color: #EEEEEE; + text-transform: capitalize; +} + +.title a:link +{ + color: #EEEEEE; + text-decoration: none; +} + +.entry0 +{ + background-color: #222222; +} + +.entry1 +{ + background-color: #303030; +} + +.entry0Selected +{ + background-color: #444466; +} + +.entry1Selected +{ + background-color: #555577; +} + +.FINISHED, .PAUSED, .ERROR, .TIMEOUT, .WAITING, .WORKING, .PENDING, .STARTING, .TERMINATED, .ACTIVEtrue, .ACTIVEfalse, .ACTIVE1, .ACTIVE0 +{ + text-align: center; + color: White; +} + +.FINISHED, .TERMINATED +{ + background-color: #009000; +} + +.PAUSED +{ + background-color: #000090; +} + +.PENDING, .STARTING +{ + background-color: #303060; +} + +.ERROR, .TIMEOUT +{ + background-color: #900000; +} + +.WORKING +{ + background-color: #009090; +} + +.ACTIVEtrue, .ACTIVE1 +{ + background-color: #009000; +} + +.ACTIVEfalse, .ACTIVE0 +{ + background-color: #900000; +} + +.logs +{ + font-size: 0.9rem; + /*font-size: 8pt; */ +} + +#main +{ + width: 100%; + /*overflow: auto;*/ + /*position: fixed;*/ + /*bottom: 5px;*/ + /*top: 60px;*/ + /*left: 5px;*/ + /*right: 5px;*/ + background-color: #444444; +} + +#header > div { + margin: 0 1rem; +} + +#maintitle +{ + /*display: inline;*/ + /*font-family: Verdana;*/ + text-transform: lowercase; + /*margin-left: 5px;*/ +} + +#maintitle a +{ + color: #444444; + font-size: 2rem; + font-weight: bolder; + text-decoration: none; +} + +#subtitle { + color: #444444; + font-size: small; + font-style: italic; +} + +#subtitle a { + color: #444444; + font-weight: bold; + text-decoration: none; +} + +#subtitle p { + margin: 0.1rem; +} + +#tabs +{ + justify-content: flex-end; + /*position: fixed;*/ + /*top: 35;*/ + /*height: 21;*/ + /*display: table-row;*/ + /*z-index: 1000;*/ + /*border-spacing: 5;*/ +} + +#tabs > .flex-row { + justify-content: center; +} + +#tabs li { + list-style: none; + text-decoration: none; +} + +#tabs li a { + text-decoration: none; +} + +#tabs button { + margin: 0 0.3rem; + border-radius: 0.4rem 0.4rem 0 0; + border-top: 1px solid #777777; + border-right: 1px solid #222222; + border-bottom: 1px solid transparent; + border-left: 1px solid #777777; + background-color: #333; +} + +#tabs button:hover { + border-top: 1px solid #777777; + border-right: 1px solid #222222; + border-bottom: 1px solid transparent; + border-left: 1px solid #777777; + background-color: #666; + color: white; +} + + +#tabs button.activetab +{ + /*display: table-cell;*/ + /*padding-left : 10;*/ + /*padding-right : 10;*/ + /*padding: 0.5rem 1rem;*/ + background-color: #444; + /*color: White;*/ + /*height:21;*/ + /*vertical-align: middle;*/ + /*border-top-style: 1px solid black;*/ + /*border-top-style: solid;*/ + /*border-top-color: Black;*/ + /*border-top-width: 1;*/ + /*border-left-style: solid;*/ + /*border-left-color: Black;*/ + /*border-left-width: 1;*/ + /*border-right-style: solid;*/ + /*border-right-color: Black;*/ + /*border-right-width: 1;*/ + /*cursor: pointer;*/ +} + +#tags button.unactivetab +{ + background-color: #333333; + border-top: 1px solid black; + border-right: 1px solid black; + border-left: 1px solid black; +} + +#refreshtools label +{ + color: #444; +} + +.refreshbutton, .refreshing +{ + /*display: inline;*/ + /*background-color: #DDDDDD;*/ + /*cursor: pointer;*/ + /*border: 1px solid black;*/ + /*padding: 0.4rem 1rem;*/ + /*margin: 0.2rem;*/ + /*position: fixed;*/ + /*width: 100;*/ + /*right: 5px;*/ + /*text-align: center;*/ + /*font-weight: bold;*/ + /*height: 50;*/ +} + +.refreshing +{ + background-color: #00F000; +} + +#jobtools .button, +#jobtools .button label, +#jobtools .button select, +#workertools .button, +#logtools .button, +#activitiestools .button, +#affinitiestools .button +{ + cursor: pointer; +} + +#jobtools, +#workertools, +#logtools, +#activitiestools, +#affinitiestools +{ + color: #dddddd; + justify-content: flex-end; +} + +.button +{ + color: ghostwhite; + padding: 0.1rem 1rem; + margin: 0.2rem 0.3rem; + background-color: #444; + cursor: pointer; + border-radius: 0.4rem; + border-top: 1px solid #777777; + border-right: 1px solid #222222; + border-bottom: 1px solid #222222; + border-left: 1px solid #777777; + box-shadow: 1px 1px 1px #333; +} + +.button img { + margin: 0 0.3rem; +} + +button:hover +{ + background-color: #666; + color: white; +} + +.buttonSep +{ + /*display: inline;*/ + /*width: 10px;*/ +} + +/* Selection button */ +#b01 { + padding: 0.1rem 0.2rem 0.2rem 1rem; +} + +#toolstable input, +#workertoolstable textarea, +tbody input { + color: white; + background-color: #222; +} + +#toolstable input:hover, +#workertoolstable textarea:hover, +tbody input:hover { + background-color: #333; +} + +#toolstable .flex-column { + align-items: flex-end; + flex-grow: 1; +} + +#toolstable label { + flex-wrap: nowrap; + width: 100%; +} + +#toolstable label span { + margin: 0 0.5rem; +} + +#workertoolstable, #affinitytoolstable { + justify-content: flex-end; +} + +.ttr1 +{ + width: 10%; + font-size: small; + color: White; + text-align: right; +} +.ttr2 +{ + width: 60%; +} +.ttr3 +{ + width: 10%; + font-size: small; + color: White; + text-align: right; +} +.ttr4 +{ +} +.ttedit +{ +} + +#toolbuttons +{ +} + +.toolbutton +{ +} + +.headerCell .flex-column { + justify-content: flex-start; + flex-grow: 1; +} + +.loadbar +{ + background-color: #009000; +} + +.loadlabel +{ + /*width: 100%;*/ + text-align: center; + /*position: absolute;*/ + /*top: 1;*/ +} + +.load +{ + /*width: 100%;*/ + /*position: relative;*/ +} + +.membar +{ + height: inherit; + background-color: #009000; +} + +.memlabel +{ + height: inherit; + text-align: center; + position: relative; + top: -100%; +} + +.mem +{ + height: inherit; +} + +.progress +{ + height: inherit; +} + +.lprogressbar +{ + height: inherit; + background-color: #009000; +} + +.gprogressbar +{ + background-color: #007000; +} + +.progresslabel +{ + height: inherit; + text-align: center; + position: relative; + top: -100%; +} + +.worker_affinities +{ + white-space: pre; +} + +#pagination { + /*display:inline;*/ +} + +.flex-column { + display: flex; + flex-direction: column; + justify-content: space-around; +} + +.flex-row { + display: flex; + flex-direction: row; + justify-content: space-between; + flex-wrap: wrap; +} + +.flex-grow { + flex-grow: 1; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.tab-content { + width: inherit; +} + +.job-sql-search-field { + display: flex; + flex-direction: row; +} + +.job-sql-search-field input:hover, +.job-sql-search-field select:hover { + background-color: #333; +} + +#activitiestools label { + margin: 0 1rem; +} + +#logout-button { + position: relative; + left: -100%; +} + +#logout-button input { + color: white; +} diff --git a/public_html/css/slick-default-theme.css b/public_html/css/slick-default-theme.css new file mode 100644 index 0000000..27ebf74 --- /dev/null +++ b/public_html/css/slick-default-theme.css @@ -0,0 +1,123 @@ +/* +IMPORTANT: +In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes. +No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS +classes should alter those! +*/ + +.slick-header-columns { + background: url('../img/header-columns-bg.gif') repeat-x center bottom; + border-bottom: 1px solid silver; +} + +.slick-header-column { + background: url('../img/header-columns-bg.gif') repeat-x center bottom; + border-right: 1px solid silver; +} + +.slick-header-column:hover, .slick-header-column-active { + background: white url('../img/header-columns-over-bg.gif') repeat-x center bottom; +} + +.slick-headerrow { + background: #fafafa; +} + +.slick-headerrow-column { + background: #fafafa; + border-bottom: 0; + height: 100%; +} + +.slick-row.ui-state-active { + background: #F5F7D7; +} + +.slick-row { + position: absolute; + background: white; + border: 0px; + line-height: 20px; +} + +.slick-row.selected { + z-index: 10; + background: #DFE8F6; +} + +.slick-cell { + padding-left: 4px; + padding-right: 4px; +} + +.slick-group { + border-bottom: 2px solid silver; +} + +.slick-group-toggle { + width: 9px; + height: 9px; + margin-right: 5px; +} + +.slick-group-toggle.expanded { + background: url(../img/collapse.gif) no-repeat center center; +} + +.slick-group-toggle.collapsed { + background: url(../img/expand.gif) no-repeat center center; +} + +.slick-group-totals { + color: gray; + background: white; +} + +.slick-cell.selected { + background-color: #444466; +} + +.slick-cell.active { + border-color: gray; + border-style: solid; +} + +.slick-sortable-placeholder { + background: silver !important; +} + +.slick-row.odd { + background: #222222; +} + +.slick-row.even { + background: #3d3941; +} + + +.slick-row.ui-state-active { + background: #F5F7D7; +} + +.slick-row.loading { + opacity: 0.5; + filter: opacity(50%); +} + +.slick-cell.invalid { + border-color: red; + -moz-animation-duration: 0.2s; + -webkit-animation-duration: 0.2s; + -moz-animation-name: slickgrid-invalid-hilite; + -webkit-animation-name: slickgrid-invalid-hilite; +} + +@-moz-keyframes slickgrid-invalid-hilite { + from { box-shadow: 0 0 6px red; } + to { box-shadow: none; } +} + +@-webkit-keyframes slickgrid-invalid-hilite { + from { box-shadow: 0 0 6px red; } + to { box-shadow: none; } +} diff --git a/public_html/css/slick.grid.css b/public_html/css/slick.grid.css new file mode 100644 index 0000000..c48e28d --- /dev/null +++ b/public_html/css/slick.grid.css @@ -0,0 +1,226 @@ +/* +IMPORTANT: +In order to preserve the uniform grid appearance, all cell styles need to have padding, margin and border sizes. +No built-in (selected, editable, highlight, flashing, invalid, loading, :focus) or user-specified CSS +classes should alter those! +*/ + + +.slick-header.ui-state-default, .slick-headerrow.ui-state-default, .slick-footerrow.ui-state-default, .slick-group-header.ui-state-default { + width: 100%; + overflow: hidden; + border-left: 0px; +} + +.slick-header-columns, .slick-headerrow-columns, .slick-footerrow-columns, .slick-group-header-columns { + position: relative; + white-space: nowrap; + cursor: default; + overflow: hidden; +} + +.slick-header-column.ui-state-default, .slick-group-header-column.ui-state-default { + position: relative; + display: inline-block; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + height: 16px; + line-height: 16px; + margin: 0; + padding: 4px; + border-right: 1px solid silver; + border-left: 0px; + border-top: 0px; + border-bottom: 0px; + float: left; +font-size: 9pt; +} + +.slick-footerrow-column.ui-state-default { + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + margin: 0; + padding: 4px; + border-right: 1px solid silver; + border-left: 0px; + border-top: 0px; + border-bottom: 0px; + float: left; + line-height: 20px; + vertical-align: middle; +height: 100%; +font-size: 9pt; +text-align: right; + +} + +.slick-headerrow-column.ui-state-default, .slick-footerrow-column.ui-state-default { + padding: 4px; +} + +.slick-header-column-sorted { + font-style: italic; +} + +.slick-sort-indicator { + display: inline-block; + width: 8px; + height: 5px; + margin-left: 4px; + margin-top: 6px; + float: left; +} + +.slick-sort-indicator-desc { + background: url(../img/sort-desc.gif); +} + +.slick-sort-indicator-asc { + background: url(../img/sort-asc.gif); +} + +.slick-resizable-handle { + position: absolute; + font-size: 0.1px; + display: block; + cursor: col-resize; + width: 4px; + right: 0px; + top: 0; + height: 100%; +} + +.slick-resizable-handle-hover { + background-color: #ccc; +} + +.slick-sortable-placeholder { + background: silver; +} + +.grid-canvas { + position: relative; + outline: 0; + overflow: hidden; +} + +.slick-row.ui-widget-content, .slick-row.ui-state-active { + position: absolute; + border: 0px; + width: 100%; +} + +.slick-cell, .slick-headerrow-column, .slick-footerrow-column { + position: absolute; + border: 1px solid transparent; + border-right: 1px solid #666; + border-bottom-color: #888; + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + vertical-align: middle; + z-index: 1; + padding: 1px 2px 2px 1px; + margin: 0; + white-space: nowrap; + cursor: default; +font-size: 8pt; + +} + +.slick-right-align { +text-align: right; +padding-right: 4px; +} + + +.slick-group { +} + +.slick-group-toggle { + display: inline-block; +} + +.slick-cell.highlighted { + background: lightskyblue; + background: rgba(0, 0, 255, 0.2); + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + transition: all 0.5s; +} + +.slick-cell.flashing { + border: 1px solid red !important; +} + +.slick-cell.editable { + z-index: 11; + overflow: visible; + background: white; + border-color: black; + border-style: solid; +} + +.slick-cell:focus { + outline: none; +} + +.slick-reorder-proxy { + display: inline-block; + background: blue; + opacity: 0.15; + filter: alpha(opacity=15); + cursor: move; +} + +.slick-reorder-guide { + display: inline-block; + height: 2px; + background: blue; + opacity: 0.7; + filter: alpha(opacity=70); +} + +.slick-selection { + z-index: 10; + position: absolute; + border: 2px dashed black; +} + +.slick-pane { + position: absolute; + outline: 0; + overflow: hidden; + width: 100%; +} + +.slick-pane-header { + display: block; +} + +.slick-header { + overflow: hidden; + position: relative; +} + +.slick-headerrow { + overflow: hidden; + position: relative; +} + +.slick-top-panel-scroller { + overflow: hidden; + position: relative; +} + +.slick-top-panel { + width: 10000px +} + +.slick-viewport { + position: relative; + outline: 0; + width: 100%; +} diff --git a/public_html/floatlayer.js b/public_html/floatlayer.js deleted file mode 100644 index 65081d9..0000000 --- a/public_html/floatlayer.js +++ /dev/null @@ -1,117 +0,0 @@ -///////////////////////////////////////////////////////////////////// - -var FloatLayers = new Array(); -var FloatLayersByName = new Array(); - -function addFloatLayer(n,offX,offY,spd){new FloatLayer(n,offX,offY,spd);} -function getFloatLayer(n){return FloatLayersByName[n];} -function alignFloatLayers(){for(var i=0;i=0) ? leftFloater : rightFloater; - this.alignVertical =(offY>=0) ? topFloater : bottomFloater; - this.ifloatX = Math.abs(offX); - this.ifloatY = Math.abs(offY); -} - -///////////////////////////////////////////////////////////////////// - -function defineFloater(){ - this.layer = document.getElementById(this.name); - this.width = this.layer.offsetWidth; - this.height = this.layer.offsetHeight; - this.prevX = this.layer.offsetLeft; - this.prevY = this.layer.offsetTop; -} - -function adjustFloater() { - this.tm=null; - if(this.layer.style.position!='absolute')return; - - var dx = Math.abs(this.floatX-this.prevX); - var dy = Math.abs(this.floatY-this.prevY); - - if (dx < this.steps/2) - cx = (dx>=1) ? 1 : 0; - else - cx = Math.round(dx/this.steps); - - if (dy < this.steps/2) - cy = (dy>=1) ? 1 : 0; - else - cy = Math.round(dy/this.steps); - - if (this.floatX > this.prevX) - this.prevX += cx; - else if (this.floatX < this.prevX) - this.prevX -= cx; - - if (this.floatY > this.prevY) - this.prevY += cy; - else if (this.floatY < this.prevY) - this.prevY -= cy; - - this.layer.style.left = this.prevX; - this.layer.style.top = this.prevY; - - if (cx!=0||cy!=0){ - if(this.tm==null)this.tm=setTimeout('FloatLayers['+this.index+'].adjust()',50); - }else - alignFloatLayers(); -} - -function setLeftFloater(){this.alignHorizontal=leftFloater;} -function setRightFloater(){this.alignHorizontal=rightFloater;} -function setTopFloater(){this.alignVertical=topFloater;} -function setBottomFloater(){this.alignVertical=bottomFloater;} - -function leftFloater(){this.floatX = document.body.scrollLeft + this.ifloatX;} -function topFloater(){this.floatY = document.body.scrollTop + this.ifloatY;} -function rightFloater(){this.floatX = document.body.scrollLeft + document.body.clientWidth - this.ifloatX - this.width;} -function bottomFloater(){this.floatY = document.body.scrollTop + document.body.clientHeight - this.ifloatY - this.height;} - -function alignFloater(){ - if(this.layer==null)this.initialize(); - this.alignHorizontal(); - this.alignVertical(); - if(this.prevX!=this.floatX || this.prevY!=this.floatY){ - if(this.tm==null)this.tm=setTimeout('FloatLayers['+this.index+'].adjust()',50); - } -} \ No newline at end of file diff --git a/public_html/img/actions.gif b/public_html/img/actions.gif new file mode 100644 index 0000000..026dd10 Binary files /dev/null and b/public_html/img/actions.gif differ diff --git a/public_html/img/ajax-loader-small.gif b/public_html/img/ajax-loader-small.gif new file mode 100644 index 0000000..5b33f7e Binary files /dev/null and b/public_html/img/ajax-loader-small.gif differ diff --git a/public_html/img/arrow_redo.png b/public_html/img/arrow_redo.png new file mode 100644 index 0000000..4f7f55d Binary files /dev/null and b/public_html/img/arrow_redo.png differ diff --git a/public_html/img/arrow_right_peppermint.png b/public_html/img/arrow_right_peppermint.png new file mode 100644 index 0000000..8722567 Binary files /dev/null and b/public_html/img/arrow_right_peppermint.png differ diff --git a/public_html/img/arrow_right_spearmint.png b/public_html/img/arrow_right_spearmint.png new file mode 100644 index 0000000..277ddde Binary files /dev/null and b/public_html/img/arrow_right_spearmint.png differ diff --git a/public_html/img/arrow_undo.png b/public_html/img/arrow_undo.png new file mode 100644 index 0000000..bc9924a Binary files /dev/null and b/public_html/img/arrow_undo.png differ diff --git a/public_html/bg-gradient.png b/public_html/img/bg-gradient.png similarity index 100% rename from public_html/bg-gradient.png rename to public_html/img/bg-gradient.png diff --git a/public_html/img/bullet_blue.png b/public_html/img/bullet_blue.png new file mode 100644 index 0000000..79d978c Binary files /dev/null and b/public_html/img/bullet_blue.png differ diff --git a/public_html/img/bullet_star.png b/public_html/img/bullet_star.png new file mode 100644 index 0000000..142ea48 Binary files /dev/null and b/public_html/img/bullet_star.png differ diff --git a/public_html/img/bullet_toggle_minus.png b/public_html/img/bullet_toggle_minus.png new file mode 100644 index 0000000..f5aa045 Binary files /dev/null and b/public_html/img/bullet_toggle_minus.png differ diff --git a/public_html/img/bullet_toggle_plus.png b/public_html/img/bullet_toggle_plus.png new file mode 100644 index 0000000..a965053 Binary files /dev/null and b/public_html/img/bullet_toggle_plus.png differ diff --git a/public_html/img/calendar.gif b/public_html/img/calendar.gif new file mode 100644 index 0000000..90fd2e1 Binary files /dev/null and b/public_html/img/calendar.gif differ diff --git a/public_html/img/collapse.gif b/public_html/img/collapse.gif new file mode 100644 index 0000000..01e6914 Binary files /dev/null and b/public_html/img/collapse.gif differ diff --git a/public_html/img/comment_yellow.gif b/public_html/img/comment_yellow.gif new file mode 100644 index 0000000..df7158a Binary files /dev/null and b/public_html/img/comment_yellow.gif differ diff --git a/public_html/img/down.gif b/public_html/img/down.gif new file mode 100644 index 0000000..9bd9447 Binary files /dev/null and b/public_html/img/down.gif differ diff --git a/public_html/img/drag-handle.png b/public_html/img/drag-handle.png new file mode 100644 index 0000000..ad7531c Binary files /dev/null and b/public_html/img/drag-handle.png differ diff --git a/public_html/img/editor-helper-bg.gif b/public_html/img/editor-helper-bg.gif new file mode 100644 index 0000000..2daa973 Binary files /dev/null and b/public_html/img/editor-helper-bg.gif differ diff --git a/public_html/img/expand.gif b/public_html/img/expand.gif new file mode 100644 index 0000000..1b24ef1 Binary files /dev/null and b/public_html/img/expand.gif differ diff --git a/public_html/img/header-bg.gif b/public_html/img/header-bg.gif new file mode 100644 index 0000000..fe7dd1c Binary files /dev/null and b/public_html/img/header-bg.gif differ diff --git a/public_html/img/header-columns-bg.gif b/public_html/img/header-columns-bg.gif new file mode 100644 index 0000000..8d459a3 Binary files /dev/null and b/public_html/img/header-columns-bg.gif differ diff --git a/public_html/img/header-columns-over-bg.gif b/public_html/img/header-columns-over-bg.gif new file mode 100644 index 0000000..f9c07af Binary files /dev/null and b/public_html/img/header-columns-over-bg.gif differ diff --git a/public_html/img/help.png b/public_html/img/help.png new file mode 100644 index 0000000..85eca09 Binary files /dev/null and b/public_html/img/help.png differ diff --git a/public_html/icon_activity.png b/public_html/img/icon_activity.png similarity index 100% rename from public_html/icon_activity.png rename to public_html/img/icon_activity.png diff --git a/public_html/icon_cut.png b/public_html/img/icon_cut.png similarity index 100% rename from public_html/icon_cut.png rename to public_html/img/icon_cut.png diff --git a/public_html/icon_logs.png b/public_html/img/icon_logs.png similarity index 100% rename from public_html/icon_logs.png rename to public_html/img/icon_logs.png diff --git a/public_html/icon_none.png b/public_html/img/icon_none.png similarity index 100% rename from public_html/icon_none.png rename to public_html/img/icon_none.png diff --git a/public_html/icon_paste.png b/public_html/img/icon_paste.png similarity index 100% rename from public_html/icon_paste.png rename to public_html/img/icon_paste.png diff --git a/public_html/icon_pause.png b/public_html/img/icon_pause.png similarity index 100% rename from public_html/icon_pause.png rename to public_html/img/icon_pause.png diff --git a/public_html/icon_play.png b/public_html/img/icon_play.png similarity index 100% rename from public_html/icon_play.png rename to public_html/img/icon_play.png diff --git a/public_html/icon_refresh.png b/public_html/img/icon_refresh.png similarity index 100% rename from public_html/icon_refresh.png rename to public_html/img/icon_refresh.png diff --git a/public_html/icon_remove.png b/public_html/img/icon_remove.png similarity index 100% rename from public_html/icon_remove.png rename to public_html/img/icon_remove.png diff --git a/public_html/icon_select.png b/public_html/img/icon_select.png similarity index 100% rename from public_html/icon_select.png rename to public_html/img/icon_select.png diff --git a/public_html/icon_tabler.png b/public_html/img/icon_tabler.png similarity index 100% rename from public_html/icon_tabler.png rename to public_html/img/icon_tabler.png diff --git a/public_html/img/icon_terminate.png b/public_html/img/icon_terminate.png new file mode 100644 index 0000000..e20d8ec Binary files /dev/null and b/public_html/img/icon_terminate.png differ diff --git a/public_html/img/info.gif b/public_html/img/info.gif new file mode 100644 index 0000000..5769434 Binary files /dev/null and b/public_html/img/info.gif differ diff --git a/public_html/img/listview.gif b/public_html/img/listview.gif new file mode 100644 index 0000000..3ec25ca Binary files /dev/null and b/public_html/img/listview.gif differ diff --git a/public_html/img/pencil.gif b/public_html/img/pencil.gif new file mode 100644 index 0000000..29f78f4 Binary files /dev/null and b/public_html/img/pencil.gif differ diff --git a/public_html/img/row-over-bg.gif b/public_html/img/row-over-bg.gif new file mode 100644 index 0000000..b288e38 Binary files /dev/null and b/public_html/img/row-over-bg.gif differ diff --git a/public_html/img/sort-asc.gif b/public_html/img/sort-asc.gif new file mode 100644 index 0000000..67a2a4c Binary files /dev/null and b/public_html/img/sort-asc.gif differ diff --git a/public_html/img/sort-asc.png b/public_html/img/sort-asc.png new file mode 100644 index 0000000..8604ff4 Binary files /dev/null and b/public_html/img/sort-asc.png differ diff --git a/public_html/img/sort-desc.gif b/public_html/img/sort-desc.gif new file mode 100644 index 0000000..34db47c Binary files /dev/null and b/public_html/img/sort-desc.gif differ diff --git a/public_html/img/sort-desc.png b/public_html/img/sort-desc.png new file mode 100644 index 0000000..a2a6adf Binary files /dev/null and b/public_html/img/sort-desc.png differ diff --git a/public_html/img/stripes.png b/public_html/img/stripes.png new file mode 100644 index 0000000..c3c4b28 Binary files /dev/null and b/public_html/img/stripes.png differ diff --git a/public_html/img/tag_red.png b/public_html/img/tag_red.png new file mode 100644 index 0000000..d290fcd Binary files /dev/null and b/public_html/img/tag_red.png differ diff --git a/public_html/img/tick.png b/public_html/img/tick.png new file mode 100644 index 0000000..3899d71 Binary files /dev/null and b/public_html/img/tick.png differ diff --git a/public_html/img/ui-bg_gloss-wave_30_3d3644_500x100.png b/public_html/img/ui-bg_gloss-wave_30_3d3644_500x100.png new file mode 100644 index 0000000..c30626d Binary files /dev/null and b/public_html/img/ui-bg_gloss-wave_30_3d3644_500x100.png differ diff --git a/public_html/img/ui-bg_highlight-soft_100_dcd9de_1x100.png b/public_html/img/ui-bg_highlight-soft_100_dcd9de_1x100.png new file mode 100644 index 0000000..c83fdf5 Binary files /dev/null and b/public_html/img/ui-bg_highlight-soft_100_dcd9de_1x100.png differ diff --git a/public_html/img/ui-bg_highlight-soft_100_eae6ea_1x100.png b/public_html/img/ui-bg_highlight-soft_100_eae6ea_1x100.png new file mode 100644 index 0000000..01b858d Binary files /dev/null and b/public_html/img/ui-bg_highlight-soft_100_eae6ea_1x100.png differ diff --git a/public_html/img/ui-bg_highlight-soft_25_30273a_1x100.png b/public_html/img/ui-bg_highlight-soft_25_30273a_1x100.png new file mode 100644 index 0000000..6a47c7e Binary files /dev/null and b/public_html/img/ui-bg_highlight-soft_25_30273a_1x100.png differ diff --git a/public_html/img/ui-bg_highlight-soft_45_5f5964_1x100.png b/public_html/img/ui-bg_highlight-soft_45_5f5964_1x100.png new file mode 100644 index 0000000..1b3446a Binary files /dev/null and b/public_html/img/ui-bg_highlight-soft_45_5f5964_1x100.png differ diff --git a/public_html/img/ui-icons_454545_256x240.png b/public_html/img/ui-icons_454545_256x240.png new file mode 100644 index 0000000..d6169e8 Binary files /dev/null and b/public_html/img/ui-icons_454545_256x240.png differ diff --git a/public_html/img/ui-icons_734d99_256x240.png b/public_html/img/ui-icons_734d99_256x240.png new file mode 100644 index 0000000..6742dbe Binary files /dev/null and b/public_html/img/ui-icons_734d99_256x240.png differ diff --git a/public_html/img/ui-icons_8d78a5_256x240.png b/public_html/img/ui-icons_8d78a5_256x240.png new file mode 100644 index 0000000..a4f148a Binary files /dev/null and b/public_html/img/ui-icons_8d78a5_256x240.png differ diff --git a/public_html/img/ui-icons_a8a3ae_256x240.png b/public_html/img/ui-icons_a8a3ae_256x240.png new file mode 100644 index 0000000..fa1ffde Binary files /dev/null and b/public_html/img/ui-icons_a8a3ae_256x240.png differ diff --git a/public_html/img/ui-icons_ebccce_256x240.png b/public_html/img/ui-icons_ebccce_256x240.png new file mode 100644 index 0000000..9379e42 Binary files /dev/null and b/public_html/img/ui-icons_ebccce_256x240.png differ diff --git a/public_html/img/ui-icons_ffffff_256x240.png b/public_html/img/ui-icons_ffffff_256x240.png new file mode 100644 index 0000000..4d66f59 Binary files /dev/null and b/public_html/img/ui-icons_ffffff_256x240.png differ diff --git a/public_html/img/user_identity.gif b/public_html/img/user_identity.gif new file mode 100644 index 0000000..095831b Binary files /dev/null and b/public_html/img/user_identity.gif differ diff --git a/public_html/img/user_identity_plus.gif b/public_html/img/user_identity_plus.gif new file mode 100644 index 0000000..b276a81 Binary files /dev/null and b/public_html/img/user_identity_plus.gif differ diff --git a/public_html/index.html b/public_html/index.html index 44a0908..e066e75 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -1,174 +1,194 @@ - - -Coalition Server - - - - - - - - - - -
-
-
-
Filter -
-
|
-
Select -
-
|
-
Reset
-
|
-
Reset Errors
-
|
-
Start
-
|
-
Pause
-
|
-
Cut
-
|
-
Paste
-
|
-
Delete
-
|
-
Log
-
|
-
Activity
-
|
-
CSV
-
-
-
-
-
- - - - - - - - - - - - - - - - - - - - - - - - -
Title:Priority: -
- - -
-
Command:TimeOut:
Directory:Affinity:
Dependencies:Retry:
URL:User:
Environment:
-
-
-
-
Select All
-
|
-
Select None
-
|
-
Start
-
|
-
Stop
-
|
-
Delete
-
|
-
Activity
-
-
-
- - - - - -
Affinity: -
- -
-
-
-
-
-
Clear
-
-
-
-
-
-
-
Job filter - -
-
|
-
Worker filter - -
-
|
-
How long -
-
-
-
-
-
- - + + + + + + Coalition Server + + + + + + + + + + +
+ +
+ +
+ + +
+
+ + +
+
+ + + + + + + + + + +
+ +
+
+ +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + + +
+
+ + +
+
+
+ +
+
+ + + + + + + +
+ +
+
+ +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + + +
+
+
+
+ +
+
+
+
+ +
+
+ +
+ + + + diff --git a/public_html/iphone.css b/public_html/iphone.css deleted file mode 100644 index 4bc34fc..0000000 --- a/public_html/iphone.css +++ /dev/null @@ -1,73 +0,0 @@ -body -{ - text-decoration: none; - color: black; - font-family: "sans-serif"; - font-size: 10pt; - font-weight: normal; - background-color: #b0b0b0; - background-image: url(bg-gradient.png); - background-repeat: repeat-x; - padding: 0px; - margin: 0px; -} - -h2 -{ - font-size: 12pt; -} - -.jobs -{ -} - -.entry, .entry0, .entry1, .title -{ - font-size: 8pt; -} - -.title -{ - background-color: #943a30; - color: #EEEEEE; -} - -.entry0 -{ - background-color: #DDDDDD; -} - -.entry1 -{ - background-color: #F5F5F5; -} - -.FINISHED -{ - background-color: #00ff00; -} - -.ERROR, .TIMEOUT -{ - background-color: #ff0000; -} - -.WORKING -{ - background-color: #00FFFF; -} - -.WAITING -{ -} - -.logs -{ - font-size: 8pt; -}; - -#header, #main, #footer -{ - width: 320px; -}; - diff --git a/public_html/iphone.js b/public_html/iphone.js deleted file mode 100644 index 0d24064..0000000 --- a/public_html/iphone.js +++ /dev/null @@ -1,142 +0,0 @@ -var xmlrpc; -var service; -var timer; -var page = "jobs"; -var logId = 0; - -$(document).ready(function() -{ - xmlrpc = imprt("xmlrpc"); - service = new xmlrpc.ServerProxy ("/xmlrpc", ["getjobs", "clearjobs", "clearjob", "getworkers", "clearworkers", "getlog", "addjob"]); - timerCB (); -}); - -function clearJobs () -{ - service.clearjobs (); - renderJobs (); -} - -function clearJob (jobId) -{ - service.clearjob (jobId); - renderJobs (); -} - -function renderLog (jobId) -{ - logId = jobId; - $("#main").empty (); - var _log = service.getlog (jobId); - $("#main").append("

Logs for jod "+jobId+":

"+_log+"
"); - - page = "logs"; -} - -function clearWorkers () -{ - service.clearworkers (); - renderWorkers (); -} - -function formatDuration (secondes) -{ - var days = Math.floor (secondes / (60*60*24)); - var hours = Math.floor ((secondes-days*60*60*24) / (60*60)); - var minutes = Math.floor ((secondes-days*60*60*24-hours*60*60) / 60); - var secondes = Math.floor (secondes-days*60*60*24-hours*60*60-minutes*60); - if (days > 0) - return days + " d " + hours + " h " + minutes + " m " + secondes + " s"; - if (hours > 0) - return hours + " h " + minutes + " m " + secondes + " s"; - if (minutes > 0) - return minutes + " m " + secondes + " s"; - return secondes + " s"; -} - -// Timer callback -function timerCB () -{ - refresh (); - - // Fire a new time event - // timer=setTimeout("timerCB ()",4000); -} - -function refresh () -{ - if (page == "jobs") - renderJobs (); - else if (page == "workers") - renderWorkers (); - else if (page == "logs") - renderLog (logId); -} - -function renderJobs () -{ - $("#main").empty (); - - var jobs = service.getjobs (); - var table = ""; - - function renderButtons () - { - $("#main").append("\n"); - } - table += "\n"; - for (i=0; i < jobs.length; i++) - { - var job = jobs[i]; - table += "\n"; - } - table += "
IDTitleStateTryTools
"+job.ID+""+job.Title+""+job.State+""+job.Try+"/"+job.Retry+"Log Remove
"; - $("#main").append(table); - renderButtons (); - - page = "jobs"; -} - -function renderWorkers () -{ - $("#main").empty (); - - var workers = service.getworkers (); - var table = ""; - - function renderButtons () - { - $("#main").append("\n"); - } - table += "\n"; - for (i=0; i < workers.length; i++) - { - var worker = workers[i]; - table += "\n"; - } - table += "
NameStateLoadLastJobFinishedError
"+worker.Name+""+worker.State+""+worker.Load+""+worker.LastJob+""+worker.Finished+""+worker.Error+"
"; - $("#main").append(table); - renderButtons (); - - page = "workers"; -} - -function addjob () -{ - service.addjob($('#title').attr("value"), - $('#cmd').attr("value"), - $('#dir').attr("value"), - $('#priority').attr("value"), - $('#retry').attr("value"), - $('#affinity').attr("value"), - $('#dependencies').attr("value")); - reloadJobs (); -} - -// Ask the server for the jobs and render them -function reloadJobs () -{ - jobs = service.getjobs (); - renderJobs (); -} - diff --git a/public_html/jquery-1.2.6.min.js b/public_html/jquery-1.2.6.min.js deleted file mode 100644 index 82b98e1..0000000 --- a/public_html/jquery-1.2.6.min.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * jQuery 1.2.6 - New Wave Javascript - * - * Copyright (c) 2008 John Resig (jquery.com) - * Dual licensed under the MIT (MIT-LICENSE.txt) - * and GPL (GPL-LICENSE.txt) licenses. - * - * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $ - * $Rev: 5685 $ - */ -(function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else -return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else -return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else -selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else -this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else -return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else -jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else -jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("",""]||!tags.indexOf("",""]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"","
"]||!tags.indexOf("",""]||(!tags.indexOf("",""]||!tags.indexOf("",""]||jQuery.browser.msie&&[1,"div
","
"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf(""&&tags.indexOf("=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else -ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&¬xml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&¬xml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&¬xml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else -while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return im[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else -for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("
").append(res.responseText.replace(//g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else -xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else -jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else -for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else -s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else -e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;ithis.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})(); \ No newline at end of file diff --git a/public_html/js/coalition.js b/public_html/js/coalition.js new file mode 100644 index 0000000..3e01e1c --- /dev/null +++ b/public_html/js/coalition.js @@ -0,0 +1,1918 @@ +var xmlrpc; +var timer; +var page = "jobs"; +var viewJob = 0; +var logId = 0; +var jobs = []; +var selectedJobs = {}; +var cutJobs = {}; +var selectedWorkers = {}; +var selectedActivities = {}; +var MultipleSelection = {} +var workers = []; +var parents = []; +var activities = []; +var affinities = []; +var configJobFilter = []; +var configJobSqlFilterParameters = ["id", "title", "user", "state", "priority", "progress", "affinity", "worker", "start_time", "command", "dependencies"]; +var jobsSortKey = "id"; +var jobsSortKeyToUpper = false; +var workersSortKey = "name"; +var workersSortKeyToUpper = true; +var activitiesSortKey = "start"; +var activitiesSortKeyToUpper = false; +var selectionStart = 0; +var showTools = true; +var controlKeyPressed = false; +var JobProps = + [ + [ "title", "title", "" ], + [ "command", "cmd", "" ], + [ "dir", "dir", "." ], + [ "priority", "priority", "127" ], + [ "affinity", "affinity", "" ], + [ "timeout", "timeout", "0" ], + [ "dependencies", "dependencies", "" ], + [ "user", "user", "" ], + [ "url", "url", "" ], + [ "environment", "env", "" ] + ]; +var updatedJobProps = {} +var WorkerProps = [ [ "affinity", "waffinity", "" ], ]; +var updatedWorkerProps = {} +var jobsTheadBuilt = false; + +/* On document ready */ +$(document).ready(function() { + reloadJobs (); + reloadWorkers (); + reloadActivities (); + showPage ("jobs"); + timer=setTimeout(timerCB,4000); + renderLogoutButton(); + // prevent page reload during ajax request + $("#job-sql-search").on("submit", function(e) { + e.preventDefault(); + getSqlWhereJobs($(this)); + }); +}); + +function setJobKey (id) +{ + // Same key ? + if (jobsSortKey == id) + jobsSortKeyToUpper = !jobsSortKeyToUpper; + else + { + jobsSortKey = id; + jobsSortKeyToUpper = true; + } + configSave("job-sort-key"); + renderJobs (jobs); +} + +function setWorkerKey (id) +{ + // Same key ? + if (workersSortKey == id) + workersSortKeyToUpper = !workersSortKeyToUpper; + else + { + workersSortKey = id; + workersSortKeyToUpper = true; + } + renderWorkers (); +} + +function setActivityKey (id) +{ + // Same key ? + if (activitiesSortKey == id) + activitiesSortKeyToUpper = !activitiesSortKeyToUpper; + else + { + activitiesSortKey = id; + activitiesSortKeyToUpper = true; + } + renderActivities (); +} + +function get_cookie ( cookie_name ) +{ + var results = document.cookie.match ( '(^|;) ?' + cookie_name + '=([^;]*)(;|$)' ); + + if ( results ) + return ( unescape ( results[2] ) ); + else + return ""; +} + +function showHideTools () +{ + showTools = !showTools; + updateTools (); +} + +function updateTools () +{ + if (!showTools) + { + $("#tools").show (); + $("#jobtools").hide (); + $("#workertools").hide (); + $("#showhidetools").show (); + } + else if (page == "jobs") + { + $("#tools").show (); + $("#jobtools").show (); + $("#workertools").hide (); + } + else if (page == "workers") + { + $("#tools").show (); + $("#jobtools").hide (); + $("#workertools").show (); + } + else + { + $("#tools").hide (); + $("#jobtools").hide (); + $("#workertools").hide (); + } +} + +function goToJob (jobId) +{ + viewJob = jobId; + reloadJobs (); +} + +var tabs = + [ + [ "jobs", "#jobsTab", "jobtab" ], + [ "workers", "#workersTab", "workertab" ], + [ "activities", "#activitiesTab", "activitytab" ], + [ "logs", "#logsTab", "logtab" ], + [ "affinities", "#affinitiesTab", "affinitytab" ] + ] + +function showTab (tab) +{ + for (i=0; i

Logs for job "+jobId+":

"+data+""); + + page = "logs"; + updateTools (); + //document.getElementById("refreshbutton").className = "refreshbutton"; + } + }); +} + +function getSelectedWorkers () +{ + var data = []; + for (j=workers.length-1; j >= 0; j--) + { + var name = workers[j].name; + if (selectedWorkers[name]) + data.push (name); + } + return data; +} + +function clearWorkers () +{ + if (confirm("Do you really want to delete the selected workers?")) + { + $.ajax({ type: "DELETE", url: "/api/webfrontend/workers", data: JSON.stringify(getSelectedWorkers ()), dataType: "json", success: + function () + { + selectedWorkers = {} + reloadWorkers (); + updateWorkerProps (); + } + }); + } +} + +function formatDate (_date) +{ + var date = new Date(_date*1000) + return date.getFullYear() + '/' + (date.getMonth()+1) + '/' + date.getDate() + ' ' + date.getHours () + ':' + date.getMinutes () + ':' + date.getSeconds(); +} + +function formatDuration (secondes) +{ + var days = Math.floor (secondes / (60*60*24)); + var hours = Math.floor ((secondes-days*60*60*24) / (60*60)); + var minutes = Math.floor ((secondes-days*60*60*24-hours*60*60) / 60); + var secondes = Math.floor (secondes-days*60*60*24-hours*60*60-minutes*60); + if (days > 0) + return days + " d " + hours + " h " + minutes + " m " + secondes + " s"; + if (hours > 0) + return hours + " h " + minutes + " m " + secondes + " s"; + if (minutes > 0) + return minutes + " m " + secondes + " s"; + return secondes + " s"; +} + +// Timer callback +function timerCB () +{ + if (document.getElementById("autorefresh").checked) + refresh (); + + // Fire a new time event + timer=setTimeout(timerCB,4000); +} + +function refresh () +{ + document.getElementById("refreshbutton").className = "refreshing button"; + if (page == "jobs") + reloadJobs (); + else if (page == "workers") + reloadWorkers (); + else if (page == "activities") + reloadActivities (); + else if (page == "logs") + renderLog (logId); + else if (page == "affinities") + renderAffinities (); +} + +function compareStrings (a,b,toupper) +{ + if (a < b) + return toupper ? -1 : 1; + if (a == b) + return 0; + return toupper ? 1 : -1; +} + +function compareNumbers (a,b,toupper) +{ + return toupper ? a-b : b-a; +} + +// Returns the HTML code for a job title column +function addSumEmpty (str) +{ + if (str == undefined) + return ""; + else + return "" + str + ""; +} + +// Returns the HTML code for a job title column +function addSum (inputs, attribute) +{ + var sum = 0; + for (i=0; i < inputs.length; i++) + { + var job = inputs[i]; + sum += job[attribute]; + } + return "" + sum + ""; +} + +// Returns the HTML code for a job title column +function addSumFinished (inputs, attribute) +{ + var sum = 0; + for (i=0; i < inputs.length; i++) + { + var job = inputs[i]; + if (job[attribute] == "FINISHED") + sum ++; + } + return "" + sum + " FINISHED"; +} + +// Average +function addSumAvgDuration (inputs, attribute) +{ + var sum = 0; + var count = 0; + for (i=0; i < inputs.length; i++) + { + var job = inputs[i]; + sum += job[attribute]; + count++; + } + if (count > 0) + return "Avg " + formatDuration (sum/count) + ""; + else + return ""; +} + +// Returns the HTML code for a job title column +function addSumSimple (inputs) +{ + return "" + inputs.length + " jobs"; +} + +function renderParents () +{ + $("#parents").empty (); + for (i=0; i < parents.length; i++) + { + var parent = parents[i]; + $("#parents").append((i == 0 ? "" : " > ") + ("" + parent.title + "")); + } +} + +// Render the current jobs +function renderJobs (jobsCurrent=[]) { + configLoad("job-sort-key"); + if (jobsCurrent.length) jobs = jobsCurrent; + + var table = ''; + + function _sort (a,b) { + if (jobsSortKey == "Progress") + { + var progressA = a.progress; + var progressB = b.progress; + return compareNumbers (progressA, progressB, jobsSortKeyToUpper); + } + else + { + var aValue = a[jobsSortKey]; + if (typeof aValue == "string") + return compareStrings (aValue, b[jobsSortKey], jobsSortKeyToUpper); + else + return compareNumbers (aValue, b[jobsSortKey], jobsSortKeyToUpper); + } + } + + if (jobs.length) jobs.sort (_sort); + + // Returns the HTML code for a job title column + function addTitleHTMLEx (attribute, alias, input, min, max) { + table += '"; + } + + function addTitleHTML (attribute, alias, input=null, min=0, max=0) { + addTitleHTMLEx (attribute, alias, input, min, max) + } + + table += ""; + table += ""; + //addTitleHTML ("Order"); + addTitleHTML ("id", "id", "search"); + addTitleHTML ("title", "title", "search"); + addTitleHTML ("url", "url"); + addTitleHTML ("user", "user", "checkbox"); + addTitleHTML ("state", "state", "checkbox"); + addTitleHTML ("priority", "priority", "search"); + addTitleHTMLEx ("total_finished", "ok"); + addTitleHTMLEx ("total_working", "wrk"); + addTitleHTMLEx ("total_errors", "err"); + addTitleHTML ("total", "total"); + addTitleHTML ("progress", "progress", "range", "0", "100"); + addTitleHTML ("affinity", "affinity", "search"); + addTitleHTML ("timeout", "timeout"); + addTitleHTML ("worker", "worker", "search"); + addTitleHTML ("start_time", "start time", "datetime-local"); + addTitleHTML ("duration", "duration"); + addTitleHTML ("run", "run"); + addTitleHTML ("command", "command", "search"); + addTitleHTML ("dir", "dir"); + addTitleHTML ("dependencies", "dependencies", "search"); + table += ""; + table += ""; + + table += ""; + for (i=0; i < jobs.length; i++) { + var job = jobs[i]; + + var mouseDownEvent = 'onMouseDown="onClickList(event,'+i+')" onDblClick="onDblClickList(event,'+i+')"'; + + table += ''; + function addTD (attr) { + table += ""; + } + //addTD (job.Order); + addTD (job.id); + addTD (job.title); + + // url + if (job.url != "") + addTD ('Open') + else + addTD ("") + + // check group state! + var mystate = job.paused ? "PAUSED" : job.state; + + addTD (job.user); + table += ''; + addTD (job.priority); + if (job.total > 0) + { + table += ''; + table += ''; + table += ''; + table += ''; + } + else + { + addTD (""); + addTD (""); + addTD (""); + addTD (""); + } + + // *** Progress bar + var progress = "" + var _progress = job.progress + _progress = Math.floor(_progress*100.0); + + // A bar div + progress = '
'; + progress += '
'; + progress += '
' + _progress + '%
'; + progress += '
'; + + addTD (progress); + addTD (job.affinity); + addTD (job.timeout); + addTD (job.worker); + addTD (job.start_time > 0 ? formatDate (job.start_time) : ""); + addTD (formatDuration (job.duration)); + addTD (job.run_done); + addTD (job.command); + addTD (job.dir); + addTD (job.dependencies); + + table += "\n"; + } + table += ""; + + // Footer + table += ""; + table += ''; + table += addSumEmpty (); + table += addSumSimple (jobs); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumFinished (jobs, "state"); + table += addSumEmpty (); + table += addSum (jobs, "total_finished"); + table += addSum (jobs, "total_working"); + table += addSum (jobs, "total_errors"); + table += addSum (jobs, "total"); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumAvgDuration (jobs, "duration"); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumEmpty (); + table += addSumEmpty (); + table += ""; + table += ""; + table += "
'; + table += '
'; + table += '
'; + table += ''; + if (attribute == jobsSortKey && jobsSortKeyToUpper) { + table += '
'; + } else if (attribute == jobsSortKey && !jobsSortKeyToUpper) { + table += '
'; + } else { + table += '' + } + } else { + table += alias+""; + } + if (input) { + var nodeSelector = '.headerCell[data-key=\''+attribute+'\']>div.flex-column'; + switch (input) { + case "search": + table += buildInputForField(nodeSelector, "job-sql-search", attribute, input); + break; + case "checkbox": + var select = buildSelectForField(nodeSelector, "job-sql-search", attribute, input); + if (select) { + table += select; // otherwise it's filled by ajax + } + break; + case "datetime-local": + buildDatetimeForField(nodeSelector, "job-sql-search", attribute, input); + break; + case "range": + buildRangeForField(nodeSelector, "jobs-sql-search", attribute, input); + attachRangeEventForField(nodeSelector, attribute, min, max); + break; + default: + break; + } + } + table += "
" + attr + "'+mystate+''+job.total_finished+''+job.total_working+''+job.total_errors+''+job.total+'
"; + + $.when($("#jobs").html(table)).then(function () { + configLoad("job-filter"); + jobsTheadBuilt = true; + }); +} + +function logSelection () +{ + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + renderLog (job.id); + } +} + +// Set the global variable 'jobs' variable +// filters for state, affinity and title are removed since the sql search is implemented. +function reloadJobs () { + parents = []; + switch (viewJob) { + case 0: // Root job + jobs = getSqlWhereJobs(); + break; + default: // Show viewJob children + $.ajax({ type: "GET", url: "/api/webfrontend/jobs/"+viewJob+"/children", dataType: "json", success: + function(jobs) { + jobs = jobs; + var idtojob = {} + for (i=0; i= 0; j--) + { + var worker = workers[j]; + if (selectedWorkers[worker.name]) + { + title:$('#activityWorker').attr("value", worker.name) + title:$('#activityJob').attr("value", "") + break; + } + } + + reloadActivities () + page = "activities" + showPage ("activities") +} + +function terminateWorkers () +{ + if (confirm("Do you really want to terminate the selected worker instances?")) + { + $.ajax({ type: "POST", url: "/api/webfrontend/terminateworkers", data: JSON.stringify(getSelectedWorkers ()), dataType: "json", success: + function () + { + reloadWorkers (); + } + }); + } +} + +function jobActivity () +{ + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + { + title:$('#activityWorker').attr("value", "") + title:$('#activityJob').attr("value", job.id) + break; + } + } + + reloadActivities () + page = "activities" + showPage ("activities") +} + +function checkSelectionProperties (list, props, selectedList, idName) +{ + var values = []; + + for (i = 0; i < list.length; i++) + { + var item = list[i]; + if (selectedList[item[idName]]) + { + for (j = 0; j < props.length; ++j) + { + var value = item[props[j][0]]; + if (values[j] != null && values[j] != value) + values[j] = MultipleSelection; + else + values[j] = value; + } + } + } + + for (i = 0; i < props.length; ++i) + { + if (values[i] == MultipleSelection) + { + // different values + $('#'+props[i][1]).css("background-color", "orange"); + $('#'+props[i][1]).attr("value", ""); + $('#'+props[i][1]).val(""); + } + else if (values[i] == null) + { + // default value + $('#'+props[i][1]).css("background-color", ""); + $('#'+props[i][1]).attr("value", props[i][2]); + $('#'+props[i][1]).val(props[i][2]); + } + else + { + // unique values + $('#'+props[i][1]).css("background-color", ""); + $('#'+props[i][1]).attr("value", values[i]); + $('#'+props[i][1]).val(values[i]); + } + } + return values; +} + +function updateSelectionProp (values, props, prop) +{ + for (i = 0; i < props.length; ++i) + if (props[i][1] == prop) + { + values[i] = true; + $('#'+props[i][1]).css("background-color", "greenyellow"); + break; + } +} + +function sendSelectionPropChanges (list, idName, values, props, objects, selectedList, func) +{ + if (!props.length) + return; + + var data = {}; + var idsN = 0; + for (j=list.length-1; j >= 0; j--) + { + var id = list[j][idName]; + if (selectedList[id]) { + var _props = {}; + for (i = 0; i < props.length; ++i) + if (values[i] == true) { + var prop = props[i][0]; + // var value = $('#'+props[i][1]).attr("value"); + var value = $('#'+props[i][1]).val(); + if (prop == "dependencies") + value = value.split(","); + _props[prop] = value; + } + data[id] = _props; + idsN++; + } + } + if (!idsN) + return; + + // One single call + $.ajax({ type: "POST", url: "/api/webfrontend/"+objects.toLowerCase(), data: JSON.stringify(data), dataType: "json", success: + function () + { + for (i = 0; i < props.length; ++i) + if (values[i] == true) + { + props[i][2] = value; + } + func (jobs); + } + }); +} + +function setSelectionDefaultProperties (props) +{ + for (i = 0; i < props.length; ++i) + props[i][2] = $('#'+props[i][1]).attr("value"); +} + +function updateWorkerProps () +{ + updatedWorkerProps = checkSelectionProperties (workers, WorkerProps, selectedWorkers, "name"); +} + +function onchangeworkerprop (prop) +{ + updateSelectionProp (updatedWorkerProps, WorkerProps, prop); +} + +function updateworkers () +{ + sendSelectionPropChanges (workers, 'name', updatedWorkerProps, WorkerProps, "Workers", selectedWorkers, + function () + { + reloadWorkers (); + } + ); +} + +function reloadWorkers () +{ + $.ajax({ type: "GET", url: "/api/webfrontend/workers", dataType: "json", success: + function (data) + { + workers = data; + renderWorkers (); + //document.getElementById("refreshbutton").className = "refreshbutton"; + } + }); +} + +function renderWorkers () +{ + $("#workers").empty (); + + var table = ""; + table += ""; + table += "\n"; + + // Returns the HTML code for a worker title column + function addTitleHTML (attribute) + { + table += '"; + } + + addTitleHTML ("name"); + addTitleHTML ("active"); + addTitleHTML ("state"); + addTitleHTML ("affinity"); + addTitleHTML ("ping_time"); + addTitleHTML ("cpu"); + addTitleHTML ("memory"); + addTitleHTML ("last_job"); + addTitleHTML ("finished"); + addTitleHTML ("error"); + addTitleHTML ("ip"); + + table += ""; + table += ""; + table += ""; + + function _sort (a,b) + { + var aValue = a[workersSortKey]; + if (typeof aValue == 'string') + return compareStrings (aValue, b[workersSortKey], workersSortKeyToUpper); + else + return compareNumbers (aValue, b[workersSortKey], workersSortKeyToUpper); + } + + workers.sort (_sort); + + for (i=0; i < workers.length; i++) + { + var worker = workers[i]; + + // *** Build the load tab for this worker + // A global div + var load = "
"; + // Add each cpu load + var loadValue = 0; + try + { + var workerload = JSON.parse (worker.cpu) + for (j=0; j < workerload.length; ++j) + { + //load += "
"; + load += "
" + loadValue + "%
"; + load += "
"; + + // *** Build the memory tab for this worker + var memory = "
"; + memory += "
"; + + function formatMem (a) + { + if (a > 1024) + return Math.round(a/1024*100)/100 + " GB"; + else + return a + " Mo"; + } + + memLabel = formatMem (worker.total_memory-worker.free_memory); + memLabel += " / "; + memLabel += formatMem (worker.total_memory); + + // Add the numerical value of the mem + memory += "
" + memLabel + "
"; + memory += "
"; + + table += "
"+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + "\n"; + } + table += ""; + table += "
'; + table += '
'; + table += '
'; + table += '"; + if (attribute == workersSortKey && workersSortKeyToUpper) + table += '
'; + if (attribute == workersSortKey && !workersSortKeyToUpper) + table += '
'; + } + else + table += attribute+""; + table += ''; + table += "
"+worker.name+""+worker.active+""+worker.state+""+worker.affinity+""+formatDate (worker.ping_time)+""+load+""+memory+""+worker.last_job+""+worker.finished+""+worker.error+""+worker.ip+"
"; + $("#workers").append(table); +} + +function reloadActivities () +{ + var data = {} + var job = $('#activityJob').prop("value") + if (job != "") + data.job = job + var worker = $('#activityWorker').prop("value") + if (worker != "") + data.worker = worker + data.howlong = $('#howlong').prop("value") + $.ajax({ type: "GET", url: "/api/webfrontend/events", data: data, dataType: "json", success: + function (data) + { + activities = data; + renderActivities (); + //document.getElementById("refreshbutton").className = "refreshbutton"; + } + }); +} + +function renderActivities () +{ + $("#activities").empty (); + + var table = ""; + table += "\n"; + + // Returns the HTML code for a worker title column + function addTitleHTML (attribute) + { + table += ""; + } + + addTitleHTML ("start"); + addTitleHTML ("job_id"); + addTitleHTML ("job_title"); + addTitleHTML ("state"); + addTitleHTML ("worker"); + addTitleHTML ("duration"); + + table += "\n"; + + function _sort (a,b) + { + var aValue = a[activitiesSortKey]; + if (typeof aValue == 'string') + return compareStrings (aValue, b[activitiesSortKey], activitiesSortKeyToUpper); + else + return compareNumbers (aValue, b[activitiesSortKey], activitiesSortKeyToUpper); + } + + activities.sort (_sort); + + for (i=0; i < activities.length; i++) + { + var activity = activities[i]; + + date = formatDate (activity.start); + dura = formatDuration (activity.duration); + + var mouseDownEvent = "onMouseDown='onClickList(event,"+i+")' onDblClick='onDblClickList(event,"+i+")'"; + table += ""+ + // table += ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + ""+ + "\n"; + } + + // Footer + table += ""; + table += addSumEmpty (); + table += addSumSimple (activities); + table += addSumEmpty (); + table += addSumFinished (activities, "state"); + table += addSumEmpty (); + table += addSumAvgDuration (activities, "duration"); + table += "\n"; + + table += "
"; + var value = activities[0]; + if (value && value[attribute] != null) + { + table += attribute; + if (attribute == activitiesSortKey && activitiesSortKeyToUpper) + table += " ↓"; + if (attribute == activitiesSortKey && !activitiesSortKeyToUpper) + table += " ↑"; + } + else + table += attribute; + table += "
"+date+""+activity.job_id+""+activity.job_title+""+activity.state+""+activity.worker+""+dura+"
"; + $("#activities").append(table); + $("#activities").append("
"); +} + +function renderAffinities () +{ + $("#affinities").empty (); + + var table = ""; + table += ""; + table += ""; + + function addTitleHTML (attribute) + { + table += ""; + } + + addTitleHTML ("id"); + addTitleHTML ("name"); + + table += "\n"; + table += ""; + + for (i = 1; i <= 63; ++i) + { + table += ""; + table += "" + table += "\n"; + } + + updateAffinities (); + + table += "
"; + var value = activities[0]; + if (value && value[attribute] != null) + { + table += attribute; + if (attribute == activitiesSortKey && activitiesSortKeyToUpper) + table += " ↓"; + if (attribute == activitiesSortKey && !activitiesSortKeyToUpper) + table += " ↑"; + } + else + table += attribute; + table += "
"+i+"
"; + $("#affinities").append(table); + $("#affinities").append("
"); +} + +function onchangeaffinityprop (affinity) +{ + $('#affinity'+affinity).css("background-color", "greenyellow"); +} + +function updateAffinities () +{ + $.ajax({ type: "GET", url: "/api/webfrontend/affinities", dataType: "json", success: + function (data) + { + affinities = data; + for (i = 1; i <= 63; ++i) + { + var def = affinities[i]; + if (def) + $("#affinity"+i).attr("value", def); + } + } + }); +} + +function sendAffinities () +{ + var affinities = {}; + for (i = 1; i <= 63; ++i) + { + var affinity = $("#affinity"+i).val(); + if (affinity != null) + affinities[i] = affinity; + } + + var data = JSON.stringify(affinities) + $.ajax({ type: "POST", url: "/api/webfrontend/affinities", data: data, dataType: "json", success: + function (data) + { + updateAffinities (); + } + }); +} + +function onchangejobprop (prop) +{ + updateSelectionProp (updatedJobProps, JobProps, prop); +} + +function updatejobs () +{ + sendSelectionPropChanges (jobs, 'id', updatedJobProps, JobProps, "Jobs", selectedJobs, + function (jobs) + { + reloadJobs (); + updateJobProps (jobs); + } + ); +} + +function addjob () +{ + dependencies = $.trim($('#dependencies').attr("value")); + dependencies = dependencies.split(',') + dependencies = dependencies != "" ? dependencies : [] + var data = { + title:$('#title')[0].value, + command:$('#cmd')[0].value, + dir:$('#dir')[0].value, + env:$('#env')[0].value, + priority:$('#priority')[0].value, + timeout:$('#timeout')[0].value, + affinity:$('#affinity')[0].value, + dependencies:$("#dependencies")[0].value, + user:$('#user')[0].value, + url:$('#url')[0].value, + parent:viewJob + }; + $.ajax({ type: "PUT", url: "/api/webfrontend/jobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + setSelectionDefaultProperties (JobProps); + reloadJobs (); + } + }); +} + +function selectJobs () +{ + var tag = document.getElementById("selectJobs").value; + if (tag == "CUSTOM") + ; + else if (tag == "NONE") + selectAll (false); + else if (tag == "ALL") + selectAll (true); + else + selectAll (true, tag); +} + +function onDblClickList (e, i) +{ + if (page == "activities") { + var activity = activities[i]; + renderLog (activity.job_id); + } else { + var job = jobs[i]; + job.command != "" ? renderLog (job.id) : goToJob (job.id); + } +} + +// List selection handler +function onClickList (e, i) +{ + if (!e) var e = window.event + + document.getElementById("selectJobs").value = "CUSTOM"; + + // Unselect if not ctrl keys + if (!e.ctrlKey) + { + if (page == "jobs") + { + selectedJobs = {}; + } + else if (page == "workers") + selectedWorkers = {}; + else if (page == "activities") + selectedActivities = {}; + } + + var thelist; + var selectedList; + var idName; + var tableId; + if (page == "jobs") + { + thelist = jobs; + selectedList = selectedJobs; + idName = "id"; + tableId = "jobtable"; + } + else if (page == "workers") + { + thelist = workers; + selectedList = selectedWorkers; + idName = "name"; + tableId = "workertable"; + } + else if (page == "activities") + { + thelist = activities; + selectedList = selectedActivities; + idName = "id"; + tableId = "activitytable"; + } + else + return; + + // Unselect if not ctrl keys + if (!e.ctrlKey) + { + for (j=0; j < thelist.length; j++) + document.getElementById(tableId+j).className = "entry"+(j%2); + } + + var begin = e.shiftKey ? Math.min (selectionStart, i) : i + var end = e.shiftKey ? Math.max (selectionStart, i) : i + + selectionStart = e.shiftKey ? selectionStart : i; + + for (j = begin; j <= end; j++) + { + var item = thelist[j]; + if (item) + { + var selected = e.ctrlKey ? !selectedList[item[idName]] : true; + selectedList[item[idName]] = selected; + document.getElementById(tableId+j).className = "entry"+(j%2)+(selected?"Selected":""); + } + } + + if (page == "jobs") { + updateJobProps (jobs); + } else if (page == "workers") { + updateWorkerProps (); + } + + // Remove selection + window.getSelection ().removeAllRanges(); +} + +function selectAll (state, filter) +{ + var thelist; + var selectedList; + var idName; + var tableId; + if (page == "jobs") + { + thelist = jobs; + selectedJobs = {}; + selectedList = selectedJobs; + idName = "id"; + tableId = "jobtable"; + } + else if (page == "workers") + { + thelist = workers; + selectedWorkers = {}; + selectedList = selectedWorkers; + idName = "name"; + tableId = "workertable"; + } + else + return; + + if (!state) + { + for (j=0; j < thelist.length; j++) + document.getElementById(tableId+j).className = "entry"+(j%2); + } + else + { + for (j=0; j < thelist.length; j++) + { + var item = thelist[j]; + if (filter == null || item.state == filter) + { + selectedList[item[idName]] = true; + document.getElementById(tableId+j).className = "entry"+(j%2)+"Selected"; + } + else + { + selectedList[item[idName]] = false; + document.getElementById(tableId+j).className = "entry"+(j%2); + } + } + } + + if (page == "jobs") { + updateJobProps (jobs); + } else if (page == "workers") { + updateWorkerProps (); + } +} + +function removeSelection () +{ + if (confirm("Do you really want to remove the selected jobs ?")) + { + var data = []; + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + data.push (job.id); + } + $.ajax({ type: "DELETE", url: "/api/webfrontend/jobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + selectedJobs = {}; + reloadJobs (); + updateJobProps (jobs); + } + }); + } +} + +function startSelection () +{ + var data = []; + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + data.push (job.id); + } + $.ajax({ type: "POST", url: "/api/webfrontend/startjobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + reloadJobs (); + } + }); +} + +function viewSelection() +{ + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id] && job.url) + window.open(job.url); + } +} + +function resetSelection () +{ + if (confirm("Do you really want to reset the selected jobs and all their children jobs ?")) + { + var data = []; + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + data.push (job.id); + } + $.ajax({ type: "POST", url: "/api/webfrontend/resetjobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + reloadJobs (); + } + }); + } +} + +function resetErrorSelection () +{ + if (confirm("Do you really want to reset the selected jobs and all their children jobs tagged in ERROR ?")) + { + var data = []; + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + data.push (job.id); + } + $.ajax({ type: "POST", url: "/api/webfrontend/reseterrorjobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + reloadJobs (); + } + }); + } +} + +function pauseSelection () +{ + var data = []; + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + data.push (job.id); + } + $.ajax({ type: "POST", url: "/api/webfrontend/pausejobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + reloadJobs (); + } + }); +} + +function updateJobProps (jobs) +{ + updatedJobProps = checkSelectionProperties (jobs, JobProps, selectedJobs, "id"); +} + +function exportCSV() +{ + window.open('csv.html?id=' + viewJob); +} + +function cutSelection () +{ + cutJobs = {} + for (j=jobs.length-1; j >= 0; j--) + { + var job = jobs[j]; + if (selectedJobs[job.id]) + { + cutJobs[job.id] = true + } + } + selectAll (false) +} + +function pasteSelection () +{ + var count = 0; + var data = {} + for (var id in cutJobs) + data[id] = {parent:viewJob} + $.ajax({ type: "POST", url: "/api/webfrontend/jobs", data: JSON.stringify(data), dataType: "json", success: + function () + { + reloadJobs (); + } + }); +} + +/* logout functions */ +function renderLogoutButton() { + var userName = getCookie("authenticated_user"); + if ( userName != "" ) + $("#logout-button").html(''); +} + +function onLogout() { + /* Set the auth user to "logout" and get a 401 error response to reset the cached crendentials */ + $.ajax({ + type: "POST", + url: "/api/webfrontend/logout", + username: "logout", + error: function() { + window.location = "/"; + /* expiration time set to 0 to delete the cookie */ + setCookie("authenticated_user", "", 0); + } + }) +} + +/* Cookie functions */ +function setCookie(cname, cvalue, exp) { + var d = new Date(); + d.setTime(exp); + var expires = "expires="+d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; +} + +function getCookie(cname) { + var name = cname + "="; + var ca = document.cookie.split(';'); + for(var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1); + } + if (c.indexOf(name) == 0) { + return c.substring(name.length, c.length); + } + } + return ""; +} + +/* Jobs SQL requests */ + +function buildInputForField(nodeSelector, form, field, type, min, max) { + switch (type) { + case "number": + //var content = ''; + //case "datetime": + //var content = '
+ //\ + // to \ + //\ + //\ + //
'; + default: + //var content = ''; + var content = ''; + } + return '
'+content+'
'; +} + +function buildSelectForField(nodeSelector, form, field, type) { + // Prevent ajax if head has been previously built + if (jobsTheadBuilt) { + return '
'+$(nodeSelector+" .job-sql-search-field").html()+'
'; + } + + var items; + switch (field) { + case "user": + ajax = getAjaxJobsUsers(); + break; + case "state": + //ajax = getAjaxJobsStates(); + //break; + content = getSelectForFieldStatesStatic(form, field); + return '
'+content+'
'; + } + ajax.done(function(items) { + var content = ''; + //$(nodeSelector).append('
'+content+'
'); + $(nodeSelector).append('
'+content+'
'); + }); + return false; +} + +function buildDatetimeForField(nodeSelector, form, field, type) { + content = ""; + return '
'+content+'
'; +} + +function buildRangeForField(nodeSelector, form, field, type, min, max) { + content = ''; + content += '
'; + return '
'+content+'
'; +} + +function attachRangeEventForField(nodeSelector, field, min, max) { + $(nodeSelector).slider({ + range: true, + min: min, + max: max, + values: [min, max], + slide: function(event, ui) { + $('job-filter-'+field+'-values').val($(nodeSelector).slider("values", 0)+'-'+$(nodeSelector).slider("values", 1)); + } + }); +} + +function toggleSearchField(node) { + var node = $(node).children(".job-sql-search-field"); + if (node.css("visibility") == "hidden") { + node.css("visibility", "visible"); + node.children("select, input").focus(); + } else { + node.css("visibility", "hidden"); + node.children("select, input").blur(); + } +} + +function checkJobSqlInputChange(field, event) { + var keyCodeEnter = 13; + var keyCodeControl = 17; + switch (event.type) { + case "click": + if (!controlKeyPressed) // single selection + $("#job-sql-search").submit(); + break; + case "keydown": + switch (event.keyCode) { + case keyCodeEnter: + $("#job-sql-search").submit(); + break; + case keyCodeControl: + controlKeyPressed = true; + break; + } + break; + case "keyup": + // multiple selection + if (event.keyCode != keyCodeControl) break; // not + controlKeyPressed = false; + var itemName = "#job-filter-"+field; + var values = $(itemName).val(); + if (values != configGet(itemName)) // the selection changed + $("#job-sql-search").submit(); + break; + } +} + +function getAjaxJobsUsers() { + return $.ajax({ + type: "GET", + url: "/api/jobs/users/", + dataType: "json", + }) +} + +function getAjaxJobsStates() { + return $.ajax({ + type: "GET", + url: "/api/jobs/states/", + dataType: "json", + }) +} + +function getSelectForFieldStatesStatic(form, field) { + var content = ''; + return content; +} + +function getAjaxSqlWhereCountJobs(where_clause) { + return $.ajax({ + type: "GET", + url: "/api/jobs/count/where/", + data: {where_clause: where_clause}, + dataType: "json", + }) +} + +function getAjaxSqlWhereJobs(data) { + return $.ajax({ + type: "GET", + url: "/api/jobs/where/", + data: data, + datatype: "json", + }) +} + +function getSqlWhereJobs() { + if (jobsTheadBuilt) configSave(); + else configLoad(); + // Limit search to children of viewJob + var sql = "(parent = "+viewJob+")"; + + for (i in configJobFilter) { + key = configJobFilter[i][0]; + values = configJobFilter[i][1]; + if (typeof(values) == "string") values = [values] + // build sql clause + originalKey = key; + switch (key) { + case "id": + case "priority": + case "dependencies": + if (values[0] == "" && values.length == 1) break; + sql += " and ("; + for (j in values) { + var value = values[j]; + if (!value) continue; + sql += key+"="+value; + if (j+1' + $("#pagination").append(button); + $("#pagination button").last().data(data); + getAjaxSqlWhereJobs(data).done(function(jobs) { + dataViewjobs.setItems(JSON.parse(jobs)); + }); + } + } + } else { + jobs = []; + renderJobs(jobs); + } + }) +} + + +/* localstorage functions */ +function configSave(category="job-filter") { + // Save configuration in browser local storage + switch (category) { + case "job-filter": + configJobFilter = []; + for (i in configJobSqlFilterParameters) { + var param = configJobSqlFilterParameters[i]; + var nodeId = category+'-'+param; + var value = $('#'+nodeId).val(); + localStorage.setItem(nodeId, value); + if (value) configJobFilter.push([param, value]); + } + break; + case "job-sort-key": + localStorage.setItem("job-sort-key", jobsSortKey); + localStorage.setItem("job-sort-key-to-upper", jobsSortKeyToUpper); + break; + } +} + +function configGet(itemName) { + // Get stored item + config = localStorage.getItem(itemName); + return (config != "undefined") ? config : ""; +} + +function configLoad(category="job-filter") { + switch (category) { + case "job-filter": + configJobFilter = []; + for (i in configJobSqlFilterParameters) { + var param = configJobSqlFilterParameters[i]; + var value = configGet(category+'-'+param); + if (value) { + value = value.split(","); + var nodeId = category+'-'+param; + $('#'+nodeId).val(value); + configJobFilter.push([param, value]); + } + } + break; + case "job-sort-key": + jobsSortKey = configGet("job-sort-key"); + jobsSortKeyToUpper = (configGet("job-sort-key-to-upper") === "true"); + break; + } + return false; +} + +function configDefinedFor(category="job-filter") { + // return true if the configuration is not empty for the provided category + switch (category) { + case "job-filter": + for (i in configJobSqlFilterParameters) { + var param = configJobSqlFilterParameters[i]; + var value = configGet(category+'-'+param); + if (value) { + return true; + } + } + break; + } + return false; +} + +function configReset() { + localStorage.clear(); +} + +function resetSqlFilter() { + // Empty form filter data and save configuration + jobsTheadBuilt = false; + configJobFilter = false; + form = $("#job-sql-search")[0]; + form.reset(); + for (i = 0; i < form.length; i++) { + form[i].value = null; + } + configSave(); +} + +function configJobsTable() { + if ($("#config-jobs-table").length != 0) return + var config_menu = '\ +
\ + \ + \ + \ + \ + \ + \ + \ +
' + $(config_menu).insertBefore("#jobs"); + $("#config-jobs-table").submit(function(e) { + e.preventDefault(); + var action = $(document.activeElement)[0].value; + switch (action) { + case "preview": + configJobsTablePreview(); + break; + case "reset": + $("#config-jobs-table").remove(); + case "save": + configJobsTableSave(); + break; + } + }); +} + +function configJobsTablePreview() { + console.log("preview"); +} + +function configJobsTableSave() { + console.log("save"); +} diff --git a/public_html/csv.js b/public_html/js/csv.js similarity index 100% rename from public_html/csv.js rename to public_html/js/csv.js diff --git a/public_html/js/jquery-ui.min.js b/public_html/js/jquery-ui.min.js new file mode 100644 index 0000000..25398a1 --- /dev/null +++ b/public_html/js/jquery-ui.min.js @@ -0,0 +1,13 @@ +/*! jQuery UI - v1.12.1 - 2016-09-14 +* http://jqueryui.com +* Includes: widget.js, position.js, data.js, disable-selection.js, effect.js, effects/effect-blind.js, effects/effect-bounce.js, effects/effect-clip.js, effects/effect-drop.js, effects/effect-explode.js, effects/effect-fade.js, effects/effect-fold.js, effects/effect-highlight.js, effects/effect-puff.js, effects/effect-pulsate.js, effects/effect-scale.js, effects/effect-shake.js, effects/effect-size.js, effects/effect-slide.js, effects/effect-transfer.js, focusable.js, form-reset-mixin.js, jquery-1-7.js, keycode.js, labels.js, scroll-parent.js, tabbable.js, unique-id.js, widgets/accordion.js, widgets/autocomplete.js, widgets/button.js, widgets/checkboxradio.js, widgets/controlgroup.js, widgets/datepicker.js, widgets/dialog.js, widgets/draggable.js, widgets/droppable.js, widgets/menu.js, widgets/mouse.js, widgets/progressbar.js, widgets/resizable.js, widgets/selectable.js, widgets/selectmenu.js, widgets/slider.js, widgets/sortable.js, widgets/spinner.js, widgets/tabs.js, widgets/tooltip.js +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +(function(t){"function"==typeof define&&define.amd?define(["jquery"],t):t(jQuery)})(function(t){function e(t){for(var e=t.css("visibility");"inherit"===e;)t=t.parent(),e=t.css("visibility");return"hidden"!==e}function i(t){for(var e,i;t.length&&t[0]!==document;){if(e=t.css("position"),("absolute"===e||"relative"===e||"fixed"===e)&&(i=parseInt(t.css("zIndex"),10),!isNaN(i)&&0!==i))return i;t=t.parent()}return 0}function s(){this._curInst=null,this._keyEvent=!1,this._disabledInputs=[],this._datepickerShowing=!1,this._inDialog=!1,this._mainDivId="ui-datepicker-div",this._inlineClass="ui-datepicker-inline",this._appendClass="ui-datepicker-append",this._triggerClass="ui-datepicker-trigger",this._dialogClass="ui-datepicker-dialog",this._disableClass="ui-datepicker-disabled",this._unselectableClass="ui-datepicker-unselectable",this._currentClass="ui-datepicker-current-day",this._dayOverClass="ui-datepicker-days-cell-over",this.regional=[],this.regional[""]={closeText:"Done",prevText:"Prev",nextText:"Next",currentText:"Today",monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],monthNamesShort:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],dayNamesShort:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"],weekHeader:"Wk",dateFormat:"mm/dd/yy",firstDay:0,isRTL:!1,showMonthAfterYear:!1,yearSuffix:""},this._defaults={showOn:"focus",showAnim:"fadeIn",showOptions:{},defaultDate:null,appendText:"",buttonText:"...",buttonImage:"",buttonImageOnly:!1,hideIfNoPrevNext:!1,navigationAsDateFormat:!1,gotoCurrent:!1,changeMonth:!1,changeYear:!1,yearRange:"c-10:c+10",showOtherMonths:!1,selectOtherMonths:!1,showWeek:!1,calculateWeek:this.iso8601Week,shortYearCutoff:"+10",minDate:null,maxDate:null,duration:"fast",beforeShowDay:null,beforeShow:null,onSelect:null,onChangeMonthYear:null,onClose:null,numberOfMonths:1,showCurrentAtPos:0,stepMonths:1,stepBigMonths:12,altField:"",altFormat:"",constrainInput:!0,showButtonPanel:!1,autoSize:!1,disabled:!1},t.extend(this._defaults,this.regional[""]),this.regional.en=t.extend(!0,{},this.regional[""]),this.regional["en-US"]=t.extend(!0,{},this.regional.en),this.dpDiv=n(t("
"))}function n(e){var i="button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a";return e.on("mouseout",i,function(){t(this).removeClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).removeClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).removeClass("ui-datepicker-next-hover")}).on("mouseover",i,o)}function o(){t.datepicker._isDisabledDatepicker(m.inline?m.dpDiv.parent()[0]:m.input[0])||(t(this).parents(".ui-datepicker-calendar").find("a").removeClass("ui-state-hover"),t(this).addClass("ui-state-hover"),-1!==this.className.indexOf("ui-datepicker-prev")&&t(this).addClass("ui-datepicker-prev-hover"),-1!==this.className.indexOf("ui-datepicker-next")&&t(this).addClass("ui-datepicker-next-hover"))}function a(e,i){t.extend(e,i);for(var s in i)null==i[s]&&(e[s]=i[s]);return e}function r(t){return function(){var e=this.element.val();t.apply(this,arguments),this._refresh(),e!==this.element.val()&&this._trigger("change")}}t.ui=t.ui||{},t.ui.version="1.12.1";var h=0,l=Array.prototype.slice;t.cleanData=function(e){return function(i){var s,n,o;for(o=0;null!=(n=i[o]);o++)try{s=t._data(n,"events"),s&&s.remove&&t(n).triggerHandler("remove")}catch(a){}e(i)}}(t.cleanData),t.widget=function(e,i,s){var n,o,a,r={},h=e.split(".")[0];e=e.split(".")[1];var l=h+"-"+e;return s||(s=i,i=t.Widget),t.isArray(s)&&(s=t.extend.apply(null,[{}].concat(s))),t.expr[":"][l.toLowerCase()]=function(e){return!!t.data(e,l)},t[h]=t[h]||{},n=t[h][e],o=t[h][e]=function(t,e){return this._createWidget?(arguments.length&&this._createWidget(t,e),void 0):new o(t,e)},t.extend(o,n,{version:s.version,_proto:t.extend({},s),_childConstructors:[]}),a=new i,a.options=t.widget.extend({},a.options),t.each(s,function(e,s){return t.isFunction(s)?(r[e]=function(){function t(){return i.prototype[e].apply(this,arguments)}function n(t){return i.prototype[e].apply(this,t)}return function(){var e,i=this._super,o=this._superApply;return this._super=t,this._superApply=n,e=s.apply(this,arguments),this._super=i,this._superApply=o,e}}(),void 0):(r[e]=s,void 0)}),o.prototype=t.widget.extend(a,{widgetEventPrefix:n?a.widgetEventPrefix||e:e},r,{constructor:o,namespace:h,widgetName:e,widgetFullName:l}),n?(t.each(n._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,o,i._proto)}),delete n._childConstructors):i._childConstructors.push(o),t.widget.bridge(e,o),o},t.widget.extend=function(e){for(var i,s,n=l.call(arguments,1),o=0,a=n.length;a>o;o++)for(i in n[o])s=n[o][i],n[o].hasOwnProperty(i)&&void 0!==s&&(e[i]=t.isPlainObject(s)?t.isPlainObject(e[i])?t.widget.extend({},e[i],s):t.widget.extend({},s):s);return e},t.widget.bridge=function(e,i){var s=i.prototype.widgetFullName||e;t.fn[e]=function(n){var o="string"==typeof n,a=l.call(arguments,1),r=this;return o?this.length||"instance"!==n?this.each(function(){var i,o=t.data(this,s);return"instance"===n?(r=o,!1):o?t.isFunction(o[n])&&"_"!==n.charAt(0)?(i=o[n].apply(o,a),i!==o&&void 0!==i?(r=i&&i.jquery?r.pushStack(i.get()):i,!1):void 0):t.error("no such method '"+n+"' for "+e+" widget instance"):t.error("cannot call methods on "+e+" prior to initialization; "+"attempted to call method '"+n+"'")}):r=void 0:(a.length&&(n=t.widget.extend.apply(null,[n].concat(a))),this.each(function(){var e=t.data(this,s);e?(e.option(n||{}),e._init&&e._init()):t.data(this,s,new i(n,this))})),r}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{classes:{},disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=h++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),this.classesElementLookup={},i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this.options.disabled&&this._setOptionDisabled(this.options.disabled),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:function(){return{}},_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){var e=this;this._destroy(),t.each(this.classesElementLookup,function(t,i){e._removeClass(i,t)}),this.element.off(this.eventNamespace).removeData(this.widgetFullName),this.widget().off(this.eventNamespace).removeAttr("aria-disabled"),this.bindings.off(this.eventNamespace)},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,o,a=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(a={},s=e.split("."),e=s.shift(),s.length){for(n=a[e]=t.widget.extend({},this.options[e]),o=0;s.length-1>o;o++)n[s[o]]=n[s[o]]||{},n=n[s[o]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];a[e]=i}return this._setOptions(a),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return"classes"===t&&this._setOptionClasses(e),this.options[t]=e,"disabled"===t&&this._setOptionDisabled(e),this},_setOptionClasses:function(e){var i,s,n;for(i in e)n=this.classesElementLookup[i],e[i]!==this.options.classes[i]&&n&&n.length&&(s=t(n.get()),this._removeClass(n,i),s.addClass(this._classes({element:s,keys:i,classes:e,add:!0})))},_setOptionDisabled:function(t){this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,!!t),t&&(this._removeClass(this.hoverable,null,"ui-state-hover"),this._removeClass(this.focusable,null,"ui-state-focus"))},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_classes:function(e){function i(i,o){var a,r;for(r=0;i.length>r;r++)a=n.classesElementLookup[i[r]]||t(),a=e.add?t(t.unique(a.get().concat(e.element.get()))):t(a.not(e.element).get()),n.classesElementLookup[i[r]]=a,s.push(i[r]),o&&e.classes[i[r]]&&s.push(e.classes[i[r]])}var s=[],n=this;return e=t.extend({element:this.element,classes:this.options.classes||{}},e),this._on(e.element,{remove:"_untrackClassesElement"}),e.keys&&i(e.keys.match(/\S+/g)||[],!0),e.extra&&i(e.extra.match(/\S+/g)||[]),s.join(" ")},_untrackClassesElement:function(e){var i=this;t.each(i.classesElementLookup,function(s,n){-1!==t.inArray(e.target,n)&&(i.classesElementLookup[s]=t(n.not(e.target).get()))})},_removeClass:function(t,e,i){return this._toggleClass(t,e,i,!1)},_addClass:function(t,e,i){return this._toggleClass(t,e,i,!0)},_toggleClass:function(t,e,i,s){s="boolean"==typeof s?s:i;var n="string"==typeof t||null===t,o={extra:n?e:i,keys:n?t:e,element:n?this.element:t,add:s};return o.element.toggleClass(this._classes(o),s),this},_on:function(e,i,s){var n,o=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,a){function r(){return e||o.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof a?o[a]:a).apply(o,arguments):void 0}"string"!=typeof a&&(r.guid=a.guid=a.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+o.eventNamespace,c=h[2];c?n.on(l,c,r):i.on(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.off(i).off(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){this._addClass(t(e.currentTarget),null,"ui-state-hover")},mouseleave:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){this._addClass(t(e.currentTarget),null,"ui-state-focus")},focusout:function(e){this._removeClass(t(e.currentTarget),null,"ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}}),t.widget,function(){function e(t,e,i){return[parseFloat(t[0])*(u.test(t[0])?e/100:1),parseFloat(t[1])*(u.test(t[1])?i/100:1)]}function i(e,i){return parseInt(t.css(e,i),10)||0}function s(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}var n,o=Math.max,a=Math.abs,r=/left|center|right/,h=/top|center|bottom/,l=/[\+\-]\d+(\.[\d]+)?%?/,c=/^\w+/,u=/%$/,d=t.fn.position;t.position={scrollbarWidth:function(){if(void 0!==n)return n;var e,i,s=t("
"),o=s.children()[0];return t("body").append(s),e=o.offsetWidth,s.css("overflow","scroll"),i=o.offsetWidth,e===i&&(i=s[0].clientWidth),s.remove(),n=e-i},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widthi?"left":e>0?"right":"center",vertical:0>r?"top":s>0?"bottom":"middle"};l>p&&p>a(e+i)&&(u.horizontal="center"),c>f&&f>a(s+r)&&(u.vertical="middle"),u.important=o(a(e),a(i))>o(a(s),a(r))?"horizontal":"vertical",n.using.call(this,t,u)}),h.offset(t.extend(D,{using:r}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,h=n-r,l=r+e.collisionWidth-a-n;e.collisionWidth>a?h>0&&0>=l?(i=t.left+h+e.collisionWidth-a-n,t.left+=h-i):t.left=l>0&&0>=h?n:h>l?n+a-e.collisionWidth:n:h>0?t.left+=h:l>0?t.left-=l:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,h=n-r,l=r+e.collisionHeight-a-n;e.collisionHeight>a?h>0&&0>=l?(i=t.top+h+e.collisionHeight-a-n,t.top+=h-i):t.top=l>0&&0>=h?n:h>l?n+a-e.collisionHeight:n:h>0?t.top+=h:l>0?t.top-=l:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,o=n.offset.left+n.scrollLeft,r=n.width,h=n.isWindow?n.scrollLeft:n.offset.left,l=t.left-e.collisionPosition.marginLeft,c=l-h,u=l+e.collisionWidth-r-h,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-r-o,(0>i||a(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-h,(s>0||u>a(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,o=n.offset.top+n.scrollTop,r=n.height,h=n.isWindow?n.scrollTop:n.offset.top,l=t.top-e.collisionPosition.marginTop,c=l-h,u=l+e.collisionHeight-r-h,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-r-o,(0>s||a(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-h,(i>0||u>a(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}}}(),t.ui.position,t.extend(t.expr[":"],{data:t.expr.createPseudo?t.expr.createPseudo(function(e){return function(i){return!!t.data(i,e)}}):function(e,i,s){return!!t.data(e,s[3])}}),t.fn.extend({disableSelection:function(){var t="onselectstart"in document.createElement("div")?"selectstart":"mousedown";return function(){return this.on(t+".ui-disableSelection",function(t){t.preventDefault()})}}(),enableSelection:function(){return this.off(".ui-disableSelection")}});var c="ui-effects-",u="ui-effects-style",d="ui-effects-animated",p=t;t.effects={effect:{}},function(t,e){function i(t,e,i){var s=u[e.type]||{};return null==t?i||!e.def?null:e.def:(t=s.floor?~~t:parseFloat(t),isNaN(t)?e.def:s.mod?(t+s.mod)%s.mod:0>t?0:t>s.max?s.max:t)}function s(i){var s=l(),n=s._rgba=[];return i=i.toLowerCase(),f(h,function(t,o){var a,r=o.re.exec(i),h=r&&o.parse(r),l=o.space||"rgba";return h?(a=s[l](h),s[c[l].cache]=a[c[l].cache],n=s._rgba=a._rgba,!1):e}),n.length?("0,0,0,0"===n.join()&&t.extend(n,o.transparent),s):o[i]}function n(t,e,i){return i=(i+1)%1,1>6*i?t+6*(e-t)*i:1>2*i?e:2>3*i?t+6*(e-t)*(2/3-i):t}var o,a="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor",r=/^([\-+])=\s*(\d+\.?\d*)/,h=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[t[1],t[2],t[3],t[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,parse:function(t){return[2.55*t[1],2.55*t[2],2.55*t[3],t[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(t){return[parseInt(t[1],16),parseInt(t[2],16),parseInt(t[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(t){return[parseInt(t[1]+t[1],16),parseInt(t[2]+t[2],16),parseInt(t[3]+t[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d?(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(t){return[t[1],t[2]/100,t[3]/100,t[4]]}}],l=t.Color=function(e,i,s,n){return new t.Color.fn.parse(e,i,s,n)},c={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},u={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},d=l.support={},p=t("

")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),l.fn=t.extend(l.prototype,{parse:function(n,a,r,h){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,h],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof l?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=l(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=l(t),n=s._space(),o=c[n],a=0===this.alpha()?l("transparent"):this,r=a[o.cache]||o.to(a._rgba),h=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],l=s[o],c=u[n.type]||{};null!==l&&(null===a?h[o]=l:(c.mod&&(l-a>c.mod/2?a+=c.mod:a-l>c.mod/2&&(a-=c.mod)),h[o]=i((l-a)*e+a,n)))}),this[n](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=l(e)._rgba;return l(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),l.fn.parse.prototype=l.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),h=Math.min(s,n,o),l=r-h,c=r+h,u=.5*c;return e=h===r?0:s===r?60*(n-o)/l+360:n===r?60*(o-s)/l+120:60*(s-n)/l+240,i=0===l?0:.5>=u?l/c:l/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,h=n.to,c=n.from;l.fn[s]=function(s){if(h&&!this[a]&&(this[a]=h(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=l(c(d)),n[a]=d,n):l(d)},f(o,function(e,i){l.fn[e]||(l.fn[e]=function(n){var o,a=t.type(n),h="alpha"===e?this._hsla?"hsla":"rgba":s,l=this[h](),c=l[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),l[i.idx]=n,this[h](l)))})})}),l.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=l(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(h){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(h){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=l(e.elem,i),e.end=l(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},l.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(p),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,o,a={};for(s in i)o=i[s],e[s]!==o&&(n[s]||(t.fx.step[s]||!isNaN(parseFloat(o)))&&(a[s]=o));return a}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(p.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var h=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",l=h.children?a.find("*").addBack():a;l=l.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),l=l.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),l=l.map(function(){var e=this,i=t.Deferred(),s=t.extend({},h,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,l.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),h.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function e(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function i(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function s(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(d)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,e){for(var i=0,s=e.length;s>i;i++)null!==e[i]&&t.data(c+e[i],t[0].style[e[i]])},restore:function(t,e){for(var i,s=0,n=e.length;n>s;s++)null!==e[s]&&(i=t.data(c+e[s]),t.css(e[s],i))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("

").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(u,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(u)||"",t.removeData(u)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(e){var i,s=e.css("position"),n=e.position();return e.css({marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()),/^(static|relative)/.test(s)&&(s="absolute",i=t("<"+e[0].nodeName+">").insertAfter(e).css({display:/^(inline|ruby)/.test(e.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:e.css("marginTop"),marginBottom:e.css("marginBottom"),marginLeft:e.css("marginLeft"),marginRight:e.css("marginRight"),"float":e.css("float")}).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).addClass("ui-effects-placeholder"),e.data(c+"placeholder",i)),e.css({position:s,left:n.left,top:n.top}),i},removePlaceholder:function(t){var e=c+"placeholder",i=t.data(e);i&&(i.remove(),t.removeData(e))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function i(e){function i(){r.removeData(d),t.effects.cleanUp(r),"hide"===s.mode&&r.hide(),a()}function a(){t.isFunction(h)&&h.call(r[0]),t.isFunction(e)&&e()}var r=t(this);s.mode=c.shift(),t.uiBackCompat===!1||o?"none"===s.mode?(r[l](),a()):n.call(r[0],s,i):(r.is(":hidden")?"hide"===l:"show"===l)?(r[l](),a()):n.call(r[0],s,a)}var s=e.apply(this,arguments),n=t.effects.effect[s.effect],o=n.mode,a=s.queue,r=a||"fx",h=s.complete,l=s.mode,c=[],u=function(e){var i=t(this),s=t.effects.mode(i,l)||o;i.data(d,!0),c.push(s),o&&("show"===s||s===o&&"hide"===s)&&i.show(),o&&"none"===s||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!n?l?this[l](s.duration,h):this.each(function(){h&&h.call(this)}):a===!1?this.each(u).each(i):this.queue(r,u).queue(r,i)},show:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="show",this.effect.call(this,n) +}}(t.fn.show),hide:function(t){return function(s){if(i(s))return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="hide",this.effect.call(this,n)}}(t.fn.hide),toggle:function(t){return function(s){if(i(s)||"boolean"==typeof s)return t.apply(this,arguments);var n=e.apply(this,arguments);return n.mode="toggle",this.effect.call(this,n)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):s(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,h=o?a.scrollLeft():0,l=n.offset(),c={top:l.top-r,left:l.left-h,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("
").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-h,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=s(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}();var f=t.effects;t.effects.define("blind","hide",function(e,i){var s={up:["bottom","top"],vertical:["bottom","top"],down:["top","bottom"],left:["right","left"],horizontal:["right","left"],right:["left","right"]},n=t(this),o=e.direction||"up",a=n.cssClip(),r={clip:t.extend({},a)},h=t.effects.createPlaceholder(n);r.clip[s[o][0]]=r.clip[s[o][1]],"show"===e.mode&&(n.cssClip(r.clip),h&&h.css(t.effects.clipToBox(r)),r.clip=a),h&&h.animate(t.effects.clipToBox(r),e.duration,e.easing),n.animate(r,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("bounce",function(e,i){var s,n,o,a=t(this),r=e.mode,h="hide"===r,l="show"===r,c=e.direction||"up",u=e.distance,d=e.times||5,p=2*d+(l||h?1:0),f=e.duration/p,g=e.easing,m="up"===c||"down"===c?"top":"left",_="up"===c||"left"===c,v=0,b=a.queue().length;for(t.effects.createPlaceholder(a),o=a.css(m),u||(u=a["top"===m?"outerHeight":"outerWidth"]()/3),l&&(n={opacity:1},n[m]=o,a.css("opacity",0).css(m,_?2*-u:2*u).animate(n,f,g)),h&&(u/=Math.pow(2,d-1)),n={},n[m]=o;d>v;v++)s={},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g).animate(n,f,g),u=h?2*u:u/2;h&&(s={opacity:0},s[m]=(_?"-=":"+=")+u,a.animate(s,f,g)),a.queue(i),t.effects.unshift(a,b,p+1)}),t.effects.define("clip","hide",function(e,i){var s,n={},o=t(this),a=e.direction||"vertical",r="both"===a,h=r||"horizontal"===a,l=r||"vertical"===a;s=o.cssClip(),n.clip={top:l?(s.bottom-s.top)/2:s.top,right:h?(s.right-s.left)/2:s.right,bottom:l?(s.bottom-s.top)/2:s.bottom,left:h?(s.right-s.left)/2:s.left},t.effects.createPlaceholder(o),"show"===e.mode&&(o.cssClip(n.clip),n.clip=s),o.animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("drop","hide",function(e,i){var s,n=t(this),o=e.mode,a="show"===o,r=e.direction||"left",h="up"===r||"down"===r?"top":"left",l="up"===r||"left"===r?"-=":"+=",c="+="===l?"-=":"+=",u={opacity:0};t.effects.createPlaceholder(n),s=e.distance||n["top"===h?"outerHeight":"outerWidth"](!0)/2,u[h]=l+s,a&&(n.css(u),u[h]=c+s,u.opacity=1),n.animate(u,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("explode","hide",function(e,i){function s(){b.push(this),b.length===u*d&&n()}function n(){p.css({visibility:"visible"}),t(b).remove(),i()}var o,a,r,h,l,c,u=e.pieces?Math.round(Math.sqrt(e.pieces)):3,d=u,p=t(this),f=e.mode,g="show"===f,m=p.show().css("visibility","hidden").offset(),_=Math.ceil(p.outerWidth()/d),v=Math.ceil(p.outerHeight()/u),b=[];for(o=0;u>o;o++)for(h=m.top+o*v,c=o-(u-1)/2,a=0;d>a;a++)r=m.left+a*_,l=a-(d-1)/2,p.clone().appendTo("body").wrap("
").css({position:"absolute",visibility:"visible",left:-a*_,top:-o*v}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:_,height:v,left:r+(g?l*_:0),top:h+(g?c*v:0),opacity:g?0:1}).animate({left:r+(g?0:l*_),top:h+(g?0:c*v),opacity:g?1:0},e.duration||500,e.easing,s)}),t.effects.define("fade","toggle",function(e,i){var s="show"===e.mode;t(this).css("opacity",s?0:1).animate({opacity:s?1:0},{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("fold","hide",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=e.size||15,h=/([0-9]+)%/.exec(r),l=!!e.horizFirst,c=l?["right","bottom"]:["bottom","right"],u=e.duration/2,d=t.effects.createPlaceholder(s),p=s.cssClip(),f={clip:t.extend({},p)},g={clip:t.extend({},p)},m=[p[c[0]],p[c[1]]],_=s.queue().length;h&&(r=parseInt(h[1],10)/100*m[a?0:1]),f.clip[c[0]]=r,g.clip[c[0]]=r,g.clip[c[1]]=0,o&&(s.cssClip(g.clip),d&&d.css(t.effects.clipToBox(g)),g.clip=p),s.queue(function(i){d&&d.animate(t.effects.clipToBox(f),u,e.easing).animate(t.effects.clipToBox(g),u,e.easing),i()}).animate(f,u,e.easing).animate(g,u,e.easing).queue(i),t.effects.unshift(s,_,4)}),t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})}),t.effects.define("size",function(e,i){var s,n,o,a=t(this),r=["fontSize"],h=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],l=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],c=e.mode,u="effect"!==c,d=e.scale||"both",p=e.origin||["middle","center"],f=a.css("position"),g=a.position(),m=t.effects.scaledDimensions(a),_=e.from||m,v=e.to||t.effects.scaledDimensions(a,0);t.effects.createPlaceholder(a),"show"===c&&(o=_,_=v,v=o),n={from:{y:_.height/m.height,x:_.width/m.width},to:{y:v.height/m.height,x:v.width/m.width}},("box"===d||"both"===d)&&(n.from.y!==n.to.y&&(_=t.effects.setTransition(a,h,n.from.y,_),v=t.effects.setTransition(a,h,n.to.y,v)),n.from.x!==n.to.x&&(_=t.effects.setTransition(a,l,n.from.x,_),v=t.effects.setTransition(a,l,n.to.x,v))),("content"===d||"both"===d)&&n.from.y!==n.to.y&&(_=t.effects.setTransition(a,r,n.from.y,_),v=t.effects.setTransition(a,r,n.to.y,v)),p&&(s=t.effects.getBaseline(p,m),_.top=(m.outerHeight-_.outerHeight)*s.y+g.top,_.left=(m.outerWidth-_.outerWidth)*s.x+g.left,v.top=(m.outerHeight-v.outerHeight)*s.y+g.top,v.left=(m.outerWidth-v.outerWidth)*s.x+g.left),a.css(_),("content"===d||"both"===d)&&(h=h.concat(["marginTop","marginBottom"]).concat(r),l=l.concat(["marginLeft","marginRight"]),a.find("*[width]").each(function(){var i=t(this),s=t.effects.scaledDimensions(i),o={height:s.height*n.from.y,width:s.width*n.from.x,outerHeight:s.outerHeight*n.from.y,outerWidth:s.outerWidth*n.from.x},a={height:s.height*n.to.y,width:s.width*n.to.x,outerHeight:s.height*n.to.y,outerWidth:s.width*n.to.x};n.from.y!==n.to.y&&(o=t.effects.setTransition(i,h,n.from.y,o),a=t.effects.setTransition(i,h,n.to.y,a)),n.from.x!==n.to.x&&(o=t.effects.setTransition(i,l,n.from.x,o),a=t.effects.setTransition(i,l,n.to.x,a)),u&&t.effects.saveStyle(i),i.css(o),i.animate(a,e.duration,e.easing,function(){u&&t.effects.restoreStyle(i)})})),a.animate(v,{queue:!1,duration:e.duration,easing:e.easing,complete:function(){var e=a.offset();0===v.opacity&&a.css("opacity",_.opacity),u||(a.css("position","static"===f?"relative":f).offset(e),t.effects.saveStyle(a)),i()}})}),t.effects.define("scale",function(e,i){var s=t(this),n=e.mode,o=parseInt(e.percent,10)||(0===parseInt(e.percent,10)?0:"effect"!==n?0:100),a=t.extend(!0,{from:t.effects.scaledDimensions(s),to:t.effects.scaledDimensions(s,o,e.direction||"both"),origin:e.origin||["middle","center"]},e);e.fade&&(a.from.opacity=1,a.to.opacity=0),t.effects.effect.size.call(this,a,i)}),t.effects.define("puff","hide",function(e,i){var s=t.extend(!0,{},e,{fade:!0,percent:parseInt(e.percent,10)||150});t.effects.effect.scale.call(this,s,i)}),t.effects.define("pulsate","show",function(e,i){var s=t(this),n=e.mode,o="show"===n,a="hide"===n,r=o||a,h=2*(e.times||5)+(r?1:0),l=e.duration/h,c=0,u=1,d=s.queue().length;for((o||!s.is(":visible"))&&(s.css("opacity",0).show(),c=1);h>u;u++)s.animate({opacity:c},l,e.easing),c=1-c;s.animate({opacity:c},l,e.easing),s.queue(i),t.effects.unshift(s,d,h+1)}),t.effects.define("shake",function(e,i){var s=1,n=t(this),o=e.direction||"left",a=e.distance||20,r=e.times||3,h=2*r+1,l=Math.round(e.duration/h),c="up"===o||"down"===o?"top":"left",u="up"===o||"left"===o,d={},p={},f={},g=n.queue().length;for(t.effects.createPlaceholder(n),d[c]=(u?"-=":"+=")+a,p[c]=(u?"+=":"-=")+2*a,f[c]=(u?"-=":"+=")+2*a,n.animate(d,l,e.easing);r>s;s++)n.animate(p,l,e.easing).animate(f,l,e.easing);n.animate(p,l,e.easing).animate(d,l/2,e.easing).queue(i),t.effects.unshift(n,g,h+1)}),t.effects.define("slide","show",function(e,i){var s,n,o=t(this),a={up:["bottom","top"],down:["top","bottom"],left:["right","left"],right:["left","right"]},r=e.mode,h=e.direction||"left",l="up"===h||"down"===h?"top":"left",c="up"===h||"left"===h,u=e.distance||o["top"===l?"outerHeight":"outerWidth"](!0),d={};t.effects.createPlaceholder(o),s=o.cssClip(),n=o.position()[l],d[l]=(c?-1:1)*u+n,d.clip=o.cssClip(),d.clip[a[h][1]]=d.clip[a[h][0]],"show"===r&&(o.cssClip(d.clip),o.css(l,d[l]),d.clip=s,d[l]=n),o.animate(d,{queue:!1,duration:e.duration,easing:e.easing,complete:i})});var f;t.uiBackCompat!==!1&&(f=t.effects.define("transfer",function(e,i){t(this).transfer(e,i)})),t.ui.focusable=function(i,s){var n,o,a,r,h,l=i.nodeName.toLowerCase();return"area"===l?(n=i.parentNode,o=n.name,i.href&&o&&"map"===n.nodeName.toLowerCase()?(a=t("img[usemap='#"+o+"']"),a.length>0&&a.is(":visible")):!1):(/^(input|select|textarea|button|object)$/.test(l)?(r=!i.disabled,r&&(h=t(i).closest("fieldset")[0],h&&(r=!h.disabled))):r="a"===l?i.href||s:s,r&&t(i).is(":visible")&&e(t(i)))},t.extend(t.expr[":"],{focusable:function(e){return t.ui.focusable(e,null!=t.attr(e,"tabindex"))}}),t.ui.focusable,t.fn.form=function(){return"string"==typeof this[0].form?this.closest("form"):t(this[0].form)},t.ui.formResetMixin={_formResetHandler:function(){var e=t(this);setTimeout(function(){var i=e.data("ui-form-reset-instances");t.each(i,function(){this.refresh()})})},_bindFormResetHandler:function(){if(this.form=this.element.form(),this.form.length){var t=this.form.data("ui-form-reset-instances")||[];t.length||this.form.on("reset.ui-form-reset",this._formResetHandler),t.push(this),this.form.data("ui-form-reset-instances",t)}},_unbindFormResetHandler:function(){if(this.form.length){var e=this.form.data("ui-form-reset-instances");e.splice(t.inArray(this,e),1),e.length?this.form.data("ui-form-reset-instances",e):this.form.removeData("ui-form-reset-instances").off("reset.ui-form-reset")}}},"1.7"===t.fn.jquery.substring(0,3)&&(t.each(["Width","Height"],function(e,i){function s(e,i,s,o){return t.each(n,function(){i-=parseFloat(t.css(e,"padding"+this))||0,s&&(i-=parseFloat(t.css(e,"border"+this+"Width"))||0),o&&(i-=parseFloat(t.css(e,"margin"+this))||0)}),i}var n="Width"===i?["Left","Right"]:["Top","Bottom"],o=i.toLowerCase(),a={innerWidth:t.fn.innerWidth,innerHeight:t.fn.innerHeight,outerWidth:t.fn.outerWidth,outerHeight:t.fn.outerHeight};t.fn["inner"+i]=function(e){return void 0===e?a["inner"+i].call(this):this.each(function(){t(this).css(o,s(this,e)+"px")})},t.fn["outer"+i]=function(e,n){return"number"!=typeof e?a["outer"+i].call(this,e):this.each(function(){t(this).css(o,s(this,e,!0,n)+"px")})}}),t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.ui.keyCode={BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38},t.ui.escapeSelector=function(){var t=/([!"#$%&'()*+,.\/:;<=>?@[\]^`{|}~])/g;return function(e){return e.replace(t,"\\$1")}}(),t.fn.labels=function(){var e,i,s,n,o;return this[0].labels&&this[0].labels.length?this.pushStack(this[0].labels):(n=this.eq(0).parents("label"),s=this.attr("id"),s&&(e=this.eq(0).parents().last(),o=e.add(e.length?e.siblings():this.siblings()),i="label[for='"+t.ui.escapeSelector(s)+"']",n=n.add(o.find(i).addBack(i))),this.pushStack(n))},t.fn.scrollParent=function(e){var i=this.css("position"),s="absolute"===i,n=e?/(auto|scroll|hidden)/:/(auto|scroll)/,o=this.parents().filter(function(){var e=t(this);return s&&"static"===e.css("position")?!1:n.test(e.css("overflow")+e.css("overflow-y")+e.css("overflow-x"))}).eq(0);return"fixed"!==i&&o.length?o:t(this[0].ownerDocument||document)},t.extend(t.expr[":"],{tabbable:function(e){var i=t.attr(e,"tabindex"),s=null!=i;return(!s||i>=0)&&t.ui.focusable(e,s)}}),t.fn.extend({uniqueId:function(){var t=0;return function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++t)})}}(),removeUniqueId:function(){return this.each(function(){/^ui-id-\d+$/.test(this.id)&&t(this).removeAttr("id")})}}),t.widget("ui.accordion",{version:"1.12.1",options:{active:0,animate:{},classes:{"ui-accordion-header":"ui-corner-top","ui-accordion-header-collapsed":"ui-corner-all","ui-accordion-content":"ui-corner-bottom"},collapsible:!1,event:"click",header:"> li > :first-child, > :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},hideProps:{borderTopWidth:"hide",borderBottomWidth:"hide",paddingTop:"hide",paddingBottom:"hide",height:"hide"},showProps:{borderTopWidth:"show",borderBottomWidth:"show",paddingTop:"show",paddingBottom:"show",height:"show"},_create:function(){var e=this.options;this.prevShow=this.prevHide=t(),this._addClass("ui-accordion","ui-widget ui-helper-reset"),this.element.attr("role","tablist"),e.collapsible||e.active!==!1&&null!=e.active||(e.active=0),this._processPanels(),0>e.active&&(e.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():t()}},_createIcons:function(){var e,i,s=this.options.icons;s&&(e=t(""),this._addClass(e,"ui-accordion-header-icon","ui-icon "+s.header),e.prependTo(this.headers),i=this.active.children(".ui-accordion-header-icon"),this._removeClass(i,s.header)._addClass(i,null,s.activeHeader)._addClass(this.headers,"ui-accordion-icons"))},_destroyIcons:function(){this._removeClass(this.headers,"ui-accordion-icons"),this.headers.children(".ui-accordion-header-icon").remove()},_destroy:function(){var t;this.element.removeAttr("role"),this.headers.removeAttr("role aria-expanded aria-selected aria-controls tabIndex").removeUniqueId(),this._destroyIcons(),t=this.headers.next().css("display","").removeAttr("role aria-hidden aria-labelledby").removeUniqueId(),"content"!==this.options.heightStyle&&t.css("height","")},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):("event"===t&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(e)),this._super(t,e),"collapsible"!==t||e||this.options.active!==!1||this._activate(0),"icons"===t&&(this._destroyIcons(),e&&this._createIcons()),void 0)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t),this._toggleClass(this.headers.add(this.headers.next()),null,"ui-state-disabled",!!t)},_keydown:function(e){if(!e.altKey&&!e.ctrlKey){var i=t.ui.keyCode,s=this.headers.length,n=this.headers.index(e.target),o=!1;switch(e.keyCode){case i.RIGHT:case i.DOWN:o=this.headers[(n+1)%s];break;case i.LEFT:case i.UP:o=this.headers[(n-1+s)%s];break;case i.SPACE:case i.ENTER:this._eventHandler(e);break;case i.HOME:o=this.headers[0];break;case i.END:o=this.headers[s-1]}o&&(t(e.target).attr("tabIndex",-1),t(o).attr("tabIndex",0),t(o).trigger("focus"),e.preventDefault())}},_panelKeyDown:function(e){e.keyCode===t.ui.keyCode.UP&&e.ctrlKey&&t(e.currentTarget).prev().trigger("focus")},refresh:function(){var e=this.options;this._processPanels(),e.active===!1&&e.collapsible===!0||!this.headers.length?(e.active=!1,this.active=t()):e.active===!1?this._activate(0):this.active.length&&!t.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(e.active=!1,this.active=t()):this._activate(Math.max(0,e.active-1)):e.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){var t=this.headers,e=this.panels;this.headers=this.element.find(this.options.header),this._addClass(this.headers,"ui-accordion-header ui-accordion-header-collapsed","ui-state-default"),this.panels=this.headers.next().filter(":not(.ui-accordion-content-active)").hide(),this._addClass(this.panels,"ui-accordion-content","ui-helper-reset ui-widget-content"),e&&(this._off(t.not(this.headers)),this._off(e.not(this.panels)))},_refresh:function(){var e,i=this.options,s=i.heightStyle,n=this.element.parent();this.active=this._findActive(i.active),this._addClass(this.active,"ui-accordion-header-active","ui-state-active")._removeClass(this.active,"ui-accordion-header-collapsed"),this._addClass(this.active.next(),"ui-accordion-content-active"),this.active.next().show(),this.headers.attr("role","tab").each(function(){var e=t(this),i=e.uniqueId().attr("id"),s=e.next(),n=s.uniqueId().attr("id");e.attr("aria-controls",n),s.attr("aria-labelledby",i)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(i.event),"fill"===s?(e=n.height(),this.element.siblings(":visible").each(function(){var i=t(this),s=i.css("position");"absolute"!==s&&"fixed"!==s&&(e-=i.outerHeight(!0))}),this.headers.each(function(){e-=t(this).outerHeight(!0)}),this.headers.next().each(function(){t(this).height(Math.max(0,e-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===s&&(e=0,this.headers.next().each(function(){var i=t(this).is(":visible");i||t(this).show(),e=Math.max(e,t(this).css("height","").height()),i||t(this).hide()}).height(e))},_activate:function(e){var i=this._findActive(e)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return"number"==typeof e?this.headers.eq(e):t()},_setupEvents:function(e){var i={keydown:"_keydown"};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(e){var i,s,n=this.options,o=this.active,a=t(e.currentTarget),r=a[0]===o[0],h=r&&n.collapsible,l=h?t():a.next(),c=o.next(),u={oldHeader:o,oldPanel:c,newHeader:h?t():a,newPanel:l};e.preventDefault(),r&&!n.collapsible||this._trigger("beforeActivate",e,u)===!1||(n.active=h?!1:this.headers.index(a),this.active=r?t():a,this._toggle(u),this._removeClass(o,"ui-accordion-header-active","ui-state-active"),n.icons&&(i=o.children(".ui-accordion-header-icon"),this._removeClass(i,null,n.icons.activeHeader)._addClass(i,null,n.icons.header)),r||(this._removeClass(a,"ui-accordion-header-collapsed")._addClass(a,"ui-accordion-header-active","ui-state-active"),n.icons&&(s=a.children(".ui-accordion-header-icon"),this._removeClass(s,null,n.icons.header)._addClass(s,null,n.icons.activeHeader)),this._addClass(a.next(),"ui-accordion-content-active")))},_toggle:function(e){var i=e.newPanel,s=this.prevShow.length?this.prevShow:e.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=s,this.options.animate?this._animate(i,s,e):(s.hide(),i.show(),this._toggleComplete(e)),s.attr({"aria-hidden":"true"}),s.prev().attr({"aria-selected":"false","aria-expanded":"false"}),i.length&&s.length?s.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===parseInt(t(this).attr("tabIndex"),10)}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_animate:function(t,e,i){var s,n,o,a=this,r=0,h=t.css("box-sizing"),l=t.length&&(!e.length||t.index()",delay:300,options:{icons:{submenu:"ui-icon-caret-1-e"},items:"> *",menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.mouseHandled=!1,this.element.uniqueId().attr({role:this.options.role,tabIndex:0}),this._addClass("ui-menu","ui-widget ui-widget-content"),this._on({"mousedown .ui-menu-item":function(t){t.preventDefault()},"click .ui-menu-item":function(e){var i=t(e.target),s=t(t.ui.safeActiveElement(this.document[0]));!this.mouseHandled&&i.not(".ui-state-disabled").length&&(this.select(e),e.isPropagationStopped()||(this.mouseHandled=!0),i.has(".ui-menu").length?this.expand(e):!this.element.is(":focus")&&s.closest(".ui-menu").length&&(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(e){if(!this.previousFilter){var i=t(e.target).closest(".ui-menu-item"),s=t(e.currentTarget);i[0]===s[0]&&(this._removeClass(s.siblings().children(".ui-state-active"),null,"ui-state-active"),this.focus(e,s))}},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(t,e){var i=this.active||this.element.find(this.options.items).eq(0);e||this.focus(t,i)},blur:function(e){this._delay(function(){var i=!t.contains(this.element[0],t.ui.safeActiveElement(this.document[0]));i&&this.collapseAll(e)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(t){this._closeOnDocumentClick(t)&&this.collapseAll(t),this.mouseHandled=!1}})},_destroy:function(){var e=this.element.find(".ui-menu-item").removeAttr("role aria-disabled"),i=e.children(".ui-menu-item-wrapper").removeUniqueId().removeAttr("tabIndex role aria-haspopup");this.element.removeAttr("aria-activedescendant").find(".ui-menu").addBack().removeAttr("role aria-labelledby aria-expanded aria-hidden aria-disabled tabIndex").removeUniqueId().show(),i.children().each(function(){var e=t(this);e.data("ui-menu-submenu-caret")&&e.remove()})},_keydown:function(e){var i,s,n,o,a=!0;switch(e.keyCode){case t.ui.keyCode.PAGE_UP:this.previousPage(e);break;case t.ui.keyCode.PAGE_DOWN:this.nextPage(e);break;case t.ui.keyCode.HOME:this._move("first","first",e);break;case t.ui.keyCode.END:this._move("last","last",e);break;case t.ui.keyCode.UP:this.previous(e);break;case t.ui.keyCode.DOWN:this.next(e);break;case t.ui.keyCode.LEFT:this.collapse(e);break;case t.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(e);break;case t.ui.keyCode.ENTER:case t.ui.keyCode.SPACE:this._activate(e);break;case t.ui.keyCode.ESCAPE:this.collapse(e);break;default:a=!1,s=this.previousFilter||"",o=!1,n=e.keyCode>=96&&105>=e.keyCode?""+(e.keyCode-96):String.fromCharCode(e.keyCode),clearTimeout(this.filterTimer),n===s?o=!0:n=s+n,i=this._filterMenuItems(n),i=o&&-1!==i.index(this.active.next())?this.active.nextAll(".ui-menu-item"):i,i.length||(n=String.fromCharCode(e.keyCode),i=this._filterMenuItems(n)),i.length?(this.focus(e,i),this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter}a&&e.preventDefault()},_activate:function(t){this.active&&!this.active.is(".ui-state-disabled")&&(this.active.children("[aria-haspopup='true']").length?this.expand(t):this.select(t))},refresh:function(){var e,i,s,n,o,a=this,r=this.options.icons.submenu,h=this.element.find(this.options.menus);this._toggleClass("ui-menu-icons",null,!!this.element.find(".ui-icon").length),s=h.filter(":not(.ui-menu)").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var e=t(this),i=e.prev(),s=t("").data("ui-menu-submenu-caret",!0);a._addClass(s,"ui-menu-icon","ui-icon "+r),i.attr("aria-haspopup","true").prepend(s),e.attr("aria-labelledby",i.attr("id"))}),this._addClass(s,"ui-menu","ui-widget ui-widget-content ui-front"),e=h.add(this.element),i=e.find(this.options.items),i.not(".ui-menu-item").each(function(){var e=t(this);a._isDivider(e)&&a._addClass(e,"ui-menu-divider","ui-widget-content")}),n=i.not(".ui-menu-item, .ui-menu-divider"),o=n.children().not(".ui-menu").uniqueId().attr({tabIndex:-1,role:this._itemRole()}),this._addClass(n,"ui-menu-item")._addClass(o,"ui-menu-item-wrapper"),i.filter(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!t.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},_setOption:function(t,e){if("icons"===t){var i=this.element.find(".ui-menu-icon");this._removeClass(i,null,this.options.icons.submenu)._addClass(i,null,e.submenu)}this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t+""),this._toggleClass(null,"ui-state-disabled",!!t)},focus:function(t,e){var i,s,n;this.blur(t,t&&"focus"===t.type),this._scrollIntoView(e),this.active=e.first(),s=this.active.children(".ui-menu-item-wrapper"),this._addClass(s,null,"ui-state-active"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),n=this.active.parent().closest(".ui-menu-item").children(".ui-menu-item-wrapper"),this._addClass(n,null,"ui-state-active"),t&&"keydown"===t.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=e.children(".ui-menu"),i.length&&t&&/^mouse/.test(t.type)&&this._startOpening(i),this.activeMenu=e.parent(),this._trigger("focus",t,{item:e})},_scrollIntoView:function(e){var i,s,n,o,a,r;this._hasScroll()&&(i=parseFloat(t.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(t.css(this.activeMenu[0],"paddingTop"))||0,n=e.offset().top-this.activeMenu.offset().top-i-s,o=this.activeMenu.scrollTop(),a=this.activeMenu.height(),r=e.outerHeight(),0>n?this.activeMenu.scrollTop(o+n):n+r>a&&this.activeMenu.scrollTop(o+n-a+r))},blur:function(t,e){e||clearTimeout(this.timer),this.active&&(this._removeClass(this.active.children(".ui-menu-item-wrapper"),null,"ui-state-active"),this._trigger("blur",t,{item:this.active}),this.active=null)},_startOpening:function(t){clearTimeout(this.timer),"true"===t.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(t)},this.delay))},_open:function(e){var i=t.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(e.parents(".ui-menu")).hide().attr("aria-hidden","true"),e.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(e,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:t(e&&e.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(e),this._removeClass(s.find(".ui-state-active"),null,"ui-state-active"),this.activeMenu=s},this.delay)},_close:function(t){t||(t=this.active?this.active.parent():this.element),t.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false")},_closeOnDocumentClick:function(e){return!t(e.target).closest(".ui-menu").length},_isDivider:function(t){return!/[^\-\u2014\u2013\s]/.test(t.text())},collapse:function(t){var e=this.active&&this.active.parent().closest(".ui-menu-item",this.element);e&&e.length&&(this._close(),this.focus(t,e))},expand:function(t){var e=this.active&&this.active.children(".ui-menu ").find(this.options.items).first();e&&e.length&&(this._open(e.parent()),this._delay(function(){this.focus(t,e)}))},next:function(t){this._move("next","first",t)},previous:function(t){this._move("prev","last",t)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(t,e,i){var s;this.active&&(s="first"===t||"last"===t?this.active["first"===t?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[t+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.find(this.options.items)[e]()),this.focus(i,s)},nextPage:function(e){var i,s,n;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=t(this),0>i.offset().top-s-n}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items)[this.active?"last":"first"]())),void 0):(this.next(e),void 0)},previousPage:function(e){var i,s,n;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,n=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=t(this),i.offset().top-s+n>0}),this.focus(e,i)):this.focus(e,this.activeMenu.find(this.options.items).first())),void 0):(this.next(e),void 0)},_hasScroll:function(){return this.element.outerHeight()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var e,i,s,n=this.element[0].nodeName.toLowerCase(),o="textarea"===n,a="input"===n; +this.isMultiLine=o||!a&&this._isContentEditable(this.element),this.valueMethod=this.element[o||a?"val":"text"],this.isNewMenu=!0,this._addClass("ui-autocomplete-input"),this.element.attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return e=!0,s=!0,i=!0,void 0;e=!1,s=!1,i=!1;var o=t.ui.keyCode;switch(n.keyCode){case o.PAGE_UP:e=!0,this._move("previousPage",n);break;case o.PAGE_DOWN:e=!0,this._move("nextPage",n);break;case o.UP:e=!0,this._keyEvent("previous",n);break;case o.DOWN:e=!0,this._keyEvent("next",n);break;case o.ENTER:this.menu.active&&(e=!0,n.preventDefault(),this.menu.select(n));break;case o.TAB:this.menu.active&&this.menu.select(n);break;case o.ESCAPE:this.menu.element.is(":visible")&&(this.isMultiLine||this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(e)return e=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),void 0;if(!i){var n=t.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(t){return s?(s=!1,t.preventDefault(),void 0):(this._searchTimeout(t),void 0)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(clearTimeout(this.searching),this.close(t),this._change(t),void 0)}}),this._initSource(),this.menu=t("
    ").appendTo(this._appendTo()).menu({role:null}).hide().menu("instance"),this._addClass(this.menu.element,"ui-autocomplete","ui-front"),this._on(this.menu.element,{mousedown:function(e){e.preventDefault(),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,this.element[0]!==t.ui.safeActiveElement(this.document[0])&&this.element.trigger("focus")})},menufocus:function(e,i){var s,n;return this.isNewMenu&&(this.isNewMenu=!1,e.originalEvent&&/^mouse/.test(e.originalEvent.type))?(this.menu.blur(),this.document.one("mousemove",function(){t(e.target).trigger(e.originalEvent)}),void 0):(n=i.item.data("ui-autocomplete-item"),!1!==this._trigger("focus",e,{item:n})&&e.originalEvent&&/^key/.test(e.originalEvent.type)&&this._value(n.value),s=i.item.attr("aria-label")||n.value,s&&t.trim(s).length&&(this.liveRegion.children().hide(),t("
    ").text(s).appendTo(this.liveRegion)),void 0)},menuselect:function(e,i){var s=i.item.data("ui-autocomplete-item"),n=this.previous;this.element[0]!==t.ui.safeActiveElement(this.document[0])&&(this.element.trigger("focus"),this.previous=n,this._delay(function(){this.previous=n,this.selectedItem=s})),!1!==this._trigger("select",e,{item:s})&&this._value(s.value),this.term=this._value(),this.close(e),this.selectedItem=s}}),this.liveRegion=t("
    ",{role:"status","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_destroy:function(){clearTimeout(this.searching),this.element.removeAttr("autocomplete"),this.menu.element.remove(),this.liveRegion.remove()},_setOption:function(t,e){this._super(t,e),"source"===t&&this._initSource(),"appendTo"===t&&this.menu.element.appendTo(this._appendTo()),"disabled"===t&&e&&this.xhr&&this.xhr.abort()},_isEventTargetInWidget:function(e){var i=this.menu.element[0];return e.target===this.element[0]||e.target===i||t.contains(i,e.target)},_closeOnClickOutside:function(t){this._isEventTargetInWidget(t)||this.close()},_appendTo:function(){var e=this.options.appendTo;return e&&(e=e.jquery||e.nodeType?t(e):this.document.find(e).eq(0)),e&&e[0]||(e=this.element.closest(".ui-front, dialog")),e.length||(e=this.document[0].body),e},_initSource:function(){var e,i,s=this;t.isArray(this.options.source)?(e=this.options.source,this.source=function(i,s){s(t.ui.autocomplete.filter(e,i.term))}):"string"==typeof this.options.source?(i=this.options.source,this.source=function(e,n){s.xhr&&s.xhr.abort(),s.xhr=t.ajax({url:i,data:e,dataType:"json",success:function(t){n(t)},error:function(){n([])}})}):this.source=this.options.source},_searchTimeout:function(t){clearTimeout(this.searching),this.searching=this._delay(function(){var e=this.term===this._value(),i=this.menu.element.is(":visible"),s=t.altKey||t.ctrlKey||t.metaKey||t.shiftKey;(!e||e&&!i&&!s)&&(this.selectedItem=null,this.search(null,t))},this.options.delay)},search:function(t,e){return t=null!=t?t:this._value(),this.term=this._value(),t.length").append(t("
    ").text(i.label)).appendTo(e)},_move:function(t,e){return this.menu.element.is(":visible")?this.menu.isFirstItem()&&/^previous/.test(t)||this.menu.isLastItem()&&/^next/.test(t)?(this.isMultiLine||this._value(this.term),this.menu.blur(),void 0):(this.menu[t](e),void 0):(this.search(null,e),void 0)},widget:function(){return this.menu.element},_value:function(){return this.valueMethod.apply(this.element,arguments)},_keyEvent:function(t,e){(!this.isMultiLine||this.menu.element.is(":visible"))&&(this._move(t,e),e.preventDefault())},_isContentEditable:function(t){if(!t.length)return!1;var e=t.prop("contentEditable");return"inherit"===e?this._isContentEditable(t.parent()):"true"===e}}),t.extend(t.ui.autocomplete,{escapeRegex:function(t){return t.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")},filter:function(e,i){var s=RegExp(t.ui.autocomplete.escapeRegex(i),"i");return t.grep(e,function(t){return s.test(t.label||t.value||t)})}}),t.widget("ui.autocomplete",t.ui.autocomplete,{options:{messages:{noResults:"No search results.",results:function(t){return t+(t>1?" results are":" result is")+" available, use up and down arrow keys to navigate."}}},__response:function(e){var i;this._superApply(arguments),this.options.disabled||this.cancelSearch||(i=e&&e.length?this.options.messages.results(e.length):this.options.messages.noResults,this.liveRegion.children().hide(),t("
    ").text(i).appendTo(this.liveRegion))}}),t.ui.autocomplete;var g=/ui-corner-([a-z]){2,6}/g;t.widget("ui.controlgroup",{version:"1.12.1",defaultElement:"
    ",options:{direction:"horizontal",disabled:null,onlyVisible:!0,items:{button:"input[type=button], input[type=submit], input[type=reset], button, a",controlgroupLabel:".ui-controlgroup-label",checkboxradio:"input[type='checkbox'], input[type='radio']",selectmenu:"select",spinner:".ui-spinner-input"}},_create:function(){this._enhance()},_enhance:function(){this.element.attr("role","toolbar"),this.refresh()},_destroy:function(){this._callChildMethod("destroy"),this.childWidgets.removeData("ui-controlgroup-data"),this.element.removeAttr("role"),this.options.items.controlgroupLabel&&this.element.find(this.options.items.controlgroupLabel).find(".ui-controlgroup-label-contents").contents().unwrap()},_initWidgets:function(){var e=this,i=[];t.each(this.options.items,function(s,n){var o,a={};return n?"controlgroupLabel"===s?(o=e.element.find(n),o.each(function(){var e=t(this);e.children(".ui-controlgroup-label-contents").length||e.contents().wrapAll("")}),e._addClass(o,null,"ui-widget ui-widget-content ui-state-default"),i=i.concat(o.get()),void 0):(t.fn[s]&&(a=e["_"+s+"Options"]?e["_"+s+"Options"]("middle"):{classes:{}},e.element.find(n).each(function(){var n=t(this),o=n[s]("instance"),r=t.widget.extend({},a);if("button"!==s||!n.parent(".ui-spinner").length){o||(o=n[s]()[s]("instance")),o&&(r.classes=e._resolveClassesValues(r.classes,o)),n[s](r);var h=n[s]("widget");t.data(h[0],"ui-controlgroup-data",o?o:n[s]("instance")),i.push(h[0])}})),void 0):void 0}),this.childWidgets=t(t.unique(i)),this._addClass(this.childWidgets,"ui-controlgroup-item")},_callChildMethod:function(e){this.childWidgets.each(function(){var i=t(this),s=i.data("ui-controlgroup-data");s&&s[e]&&s[e]()})},_updateCornerClass:function(t,e){var i="ui-corner-top ui-corner-bottom ui-corner-left ui-corner-right ui-corner-all",s=this._buildSimpleOptions(e,"label").classes.label;this._removeClass(t,null,i),this._addClass(t,null,s)},_buildSimpleOptions:function(t,e){var i="vertical"===this.options.direction,s={classes:{}};return s.classes[e]={middle:"",first:"ui-corner-"+(i?"top":"left"),last:"ui-corner-"+(i?"bottom":"right"),only:"ui-corner-all"}[t],s},_spinnerOptions:function(t){var e=this._buildSimpleOptions(t,"ui-spinner");return e.classes["ui-spinner-up"]="",e.classes["ui-spinner-down"]="",e},_buttonOptions:function(t){return this._buildSimpleOptions(t,"ui-button")},_checkboxradioOptions:function(t){return this._buildSimpleOptions(t,"ui-checkboxradio-label")},_selectmenuOptions:function(t){var e="vertical"===this.options.direction;return{width:e?"auto":!1,classes:{middle:{"ui-selectmenu-button-open":"","ui-selectmenu-button-closed":""},first:{"ui-selectmenu-button-open":"ui-corner-"+(e?"top":"tl"),"ui-selectmenu-button-closed":"ui-corner-"+(e?"top":"left")},last:{"ui-selectmenu-button-open":e?"":"ui-corner-tr","ui-selectmenu-button-closed":"ui-corner-"+(e?"bottom":"right")},only:{"ui-selectmenu-button-open":"ui-corner-top","ui-selectmenu-button-closed":"ui-corner-all"}}[t]}},_resolveClassesValues:function(e,i){var s={};return t.each(e,function(n){var o=i.options.classes[n]||"";o=t.trim(o.replace(g,"")),s[n]=(o+" "+e[n]).replace(/\s+/g," ")}),s},_setOption:function(t,e){return"direction"===t&&this._removeClass("ui-controlgroup-"+this.options.direction),this._super(t,e),"disabled"===t?(this._callChildMethod(e?"disable":"enable"),void 0):(this.refresh(),void 0)},refresh:function(){var e,i=this;this._addClass("ui-controlgroup ui-controlgroup-"+this.options.direction),"horizontal"===this.options.direction&&this._addClass(null,"ui-helper-clearfix"),this._initWidgets(),e=this.childWidgets,this.options.onlyVisible&&(e=e.filter(":visible")),e.length&&(t.each(["first","last"],function(t,s){var n=e[s]().data("ui-controlgroup-data");if(n&&i["_"+n.widgetName+"Options"]){var o=i["_"+n.widgetName+"Options"](1===e.length?"only":s);o.classes=i._resolveClassesValues(o.classes,n),n.element[n.widgetName](o)}else i._updateCornerClass(e[s](),s)}),this._callChildMethod("refresh"))}}),t.widget("ui.checkboxradio",[t.ui.formResetMixin,{version:"1.12.1",options:{disabled:null,label:null,icon:!0,classes:{"ui-checkboxradio-label":"ui-corner-all","ui-checkboxradio-icon":"ui-corner-all"}},_getCreateOptions:function(){var e,i,s=this,n=this._super()||{};return this._readType(),i=this.element.labels(),this.label=t(i[i.length-1]),this.label.length||t.error("No label found for checkboxradio widget"),this.originalLabel="",this.label.contents().not(this.element[0]).each(function(){s.originalLabel+=3===this.nodeType?t(this).text():this.outerHTML}),this.originalLabel&&(n.label=this.originalLabel),e=this.element[0].disabled,null!=e&&(n.disabled=e),n},_create:function(){var t=this.element[0].checked;this._bindFormResetHandler(),null==this.options.disabled&&(this.options.disabled=this.element[0].disabled),this._setOption("disabled",this.options.disabled),this._addClass("ui-checkboxradio","ui-helper-hidden-accessible"),this._addClass(this.label,"ui-checkboxradio-label","ui-button ui-widget"),"radio"===this.type&&this._addClass(this.label,"ui-checkboxradio-radio-label"),this.options.label&&this.options.label!==this.originalLabel?this._updateLabel():this.originalLabel&&(this.options.label=this.originalLabel),this._enhance(),t&&(this._addClass(this.label,"ui-checkboxradio-checked","ui-state-active"),this.icon&&this._addClass(this.icon,null,"ui-state-hover")),this._on({change:"_toggleClasses",focus:function(){this._addClass(this.label,null,"ui-state-focus ui-visual-focus")},blur:function(){this._removeClass(this.label,null,"ui-state-focus ui-visual-focus")}})},_readType:function(){var e=this.element[0].nodeName.toLowerCase();this.type=this.element[0].type,"input"===e&&/radio|checkbox/.test(this.type)||t.error("Can't create checkboxradio on element.nodeName="+e+" and element.type="+this.type)},_enhance:function(){this._updateIcon(this.element[0].checked)},widget:function(){return this.label},_getRadioGroup:function(){var e,i=this.element[0].name,s="input[name='"+t.ui.escapeSelector(i)+"']";return i?(e=this.form.length?t(this.form[0].elements).filter(s):t(s).filter(function(){return 0===t(this).form().length}),e.not(this.element)):t([])},_toggleClasses:function(){var e=this.element[0].checked;this._toggleClass(this.label,"ui-checkboxradio-checked","ui-state-active",e),this.options.icon&&"checkbox"===this.type&&this._toggleClass(this.icon,null,"ui-icon-check ui-state-checked",e)._toggleClass(this.icon,null,"ui-icon-blank",!e),"radio"===this.type&&this._getRadioGroup().each(function(){var e=t(this).checkboxradio("instance");e&&e._removeClass(e.label,"ui-checkboxradio-checked","ui-state-active")})},_destroy:function(){this._unbindFormResetHandler(),this.icon&&(this.icon.remove(),this.iconSpace.remove())},_setOption:function(t,e){return"label"!==t||e?(this._super(t,e),"disabled"===t?(this._toggleClass(this.label,null,"ui-state-disabled",e),this.element[0].disabled=e,void 0):(this.refresh(),void 0)):void 0},_updateIcon:function(e){var i="ui-icon ui-icon-background ";this.options.icon?(this.icon||(this.icon=t(""),this.iconSpace=t(" "),this._addClass(this.iconSpace,"ui-checkboxradio-icon-space")),"checkbox"===this.type?(i+=e?"ui-icon-check ui-state-checked":"ui-icon-blank",this._removeClass(this.icon,null,e?"ui-icon-blank":"ui-icon-check")):i+="ui-icon-blank",this._addClass(this.icon,"ui-checkboxradio-icon",i),e||this._removeClass(this.icon,null,"ui-icon-check ui-state-checked"),this.icon.prependTo(this.label).after(this.iconSpace)):void 0!==this.icon&&(this.icon.remove(),this.iconSpace.remove(),delete this.icon)},_updateLabel:function(){var t=this.label.contents().not(this.element[0]);this.icon&&(t=t.not(this.icon[0])),this.iconSpace&&(t=t.not(this.iconSpace[0])),t.remove(),this.label.append(this.options.label)},refresh:function(){var t=this.element[0].checked,e=this.element[0].disabled;this._updateIcon(t),this._toggleClass(this.label,"ui-checkboxradio-checked","ui-state-active",t),null!==this.options.label&&this._updateLabel(),e!==this.options.disabled&&this._setOptions({disabled:e})}}]),t.ui.checkboxradio,t.widget("ui.button",{version:"1.12.1",defaultElement:"").addClass(this._triggerClass).html(o?t("").attr({src:o,alt:n,title:n}):n)),e[r?"before":"after"](i.trigger),i.trigger.on("click",function(){return t.datepicker._datepickerShowing&&t.datepicker._lastInput===e[0]?t.datepicker._hideDatepicker():t.datepicker._datepickerShowing&&t.datepicker._lastInput!==e[0]?(t.datepicker._hideDatepicker(),t.datepicker._showDatepicker(e[0])):t.datepicker._showDatepicker(e[0]),!1}))},_autoSize:function(t){if(this._get(t,"autoSize")&&!t.inline){var e,i,s,n,o=new Date(2009,11,20),a=this._get(t,"dateFormat");a.match(/[DM]/)&&(e=function(t){for(i=0,s=0,n=0;t.length>n;n++)t[n].length>i&&(i=t[n].length,s=n);return s},o.setMonth(e(this._get(t,a.match(/MM/)?"monthNames":"monthNamesShort"))),o.setDate(e(this._get(t,a.match(/DD/)?"dayNames":"dayNamesShort"))+20-o.getDay())),t.input.attr("size",this._formatDate(t,o).length)}},_inlineDatepicker:function(e,i){var s=t(e);s.hasClass(this.markerClassName)||(s.addClass(this.markerClassName).append(i.dpDiv),t.data(e,"datepicker",i),this._setDate(i,this._getDefaultDate(i),!0),this._updateDatepicker(i),this._updateAlternate(i),i.settings.disabled&&this._disableDatepicker(e),i.dpDiv.css("display","block"))},_dialogDatepicker:function(e,i,s,n,o){var r,h,l,c,u,d=this._dialogInst;return d||(this.uuid+=1,r="dp"+this.uuid,this._dialogInput=t(""),this._dialogInput.on("keydown",this._doKeyDown),t("body").append(this._dialogInput),d=this._dialogInst=this._newInst(this._dialogInput,!1),d.settings={},t.data(this._dialogInput[0],"datepicker",d)),a(d.settings,n||{}),i=i&&i.constructor===Date?this._formatDate(d,i):i,this._dialogInput.val(i),this._pos=o?o.length?o:[o.pageX,o.pageY]:null,this._pos||(h=document.documentElement.clientWidth,l=document.documentElement.clientHeight,c=document.documentElement.scrollLeft||document.body.scrollLeft,u=document.documentElement.scrollTop||document.body.scrollTop,this._pos=[h/2-100+c,l/2-150+u]),this._dialogInput.css("left",this._pos[0]+20+"px").css("top",this._pos[1]+"px"),d.settings.onSelect=s,this._inDialog=!0,this.dpDiv.addClass(this._dialogClass),this._showDatepicker(this._dialogInput[0]),t.blockUI&&t.blockUI(this.dpDiv),t.data(this._dialogInput[0],"datepicker",d),this},_destroyDatepicker:function(e){var i,s=t(e),n=t.data(e,"datepicker");s.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),t.removeData(e,"datepicker"),"input"===i?(n.append.remove(),n.trigger.remove(),s.removeClass(this.markerClassName).off("focus",this._showDatepicker).off("keydown",this._doKeyDown).off("keypress",this._doKeyPress).off("keyup",this._doKeyUp)):("div"===i||"span"===i)&&s.removeClass(this.markerClassName).empty(),m===n&&(m=null))},_enableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!1,o.trigger.filter("button").each(function(){this.disabled=!1}).end().filter("img").css({opacity:"1.0",cursor:""})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().removeClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!1)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}))},_disableDatepicker:function(e){var i,s,n=t(e),o=t.data(e,"datepicker");n.hasClass(this.markerClassName)&&(i=e.nodeName.toLowerCase(),"input"===i?(e.disabled=!0,o.trigger.filter("button").each(function(){this.disabled=!0}).end().filter("img").css({opacity:"0.5",cursor:"default"})):("div"===i||"span"===i)&&(s=n.children("."+this._inlineClass),s.children().addClass("ui-state-disabled"),s.find("select.ui-datepicker-month, select.ui-datepicker-year").prop("disabled",!0)),this._disabledInputs=t.map(this._disabledInputs,function(t){return t===e?null:t}),this._disabledInputs[this._disabledInputs.length]=e)},_isDisabledDatepicker:function(t){if(!t)return!1;for(var e=0;this._disabledInputs.length>e;e++)if(this._disabledInputs[e]===t)return!0;return!1},_getInst:function(e){try{return t.data(e,"datepicker")}catch(i){throw"Missing instance data for this datepicker"}},_optionDatepicker:function(e,i,s){var n,o,r,h,l=this._getInst(e);return 2===arguments.length&&"string"==typeof i?"defaults"===i?t.extend({},t.datepicker._defaults):l?"all"===i?t.extend({},l.settings):this._get(l,i):null:(n=i||{},"string"==typeof i&&(n={},n[i]=s),l&&(this._curInst===l&&this._hideDatepicker(),o=this._getDateDatepicker(e,!0),r=this._getMinMaxDate(l,"min"),h=this._getMinMaxDate(l,"max"),a(l.settings,n),null!==r&&void 0!==n.dateFormat&&void 0===n.minDate&&(l.settings.minDate=this._formatDate(l,r)),null!==h&&void 0!==n.dateFormat&&void 0===n.maxDate&&(l.settings.maxDate=this._formatDate(l,h)),"disabled"in n&&(n.disabled?this._disableDatepicker(e):this._enableDatepicker(e)),this._attachments(t(e),l),this._autoSize(l),this._setDate(l,o),this._updateAlternate(l),this._updateDatepicker(l)),void 0)},_changeDatepicker:function(t,e,i){this._optionDatepicker(t,e,i)},_refreshDatepicker:function(t){var e=this._getInst(t);e&&this._updateDatepicker(e)},_setDateDatepicker:function(t,e){var i=this._getInst(t);i&&(this._setDate(i,e),this._updateDatepicker(i),this._updateAlternate(i))},_getDateDatepicker:function(t,e){var i=this._getInst(t);return i&&!i.inline&&this._setDateFromField(i,e),i?this._getDate(i):null},_doKeyDown:function(e){var i,s,n,o=t.datepicker._getInst(e.target),a=!0,r=o.dpDiv.is(".ui-datepicker-rtl");if(o._keyEvent=!0,t.datepicker._datepickerShowing)switch(e.keyCode){case 9:t.datepicker._hideDatepicker(),a=!1;break;case 13:return n=t("td."+t.datepicker._dayOverClass+":not(."+t.datepicker._currentClass+")",o.dpDiv),n[0]&&t.datepicker._selectDay(e.target,o.selectedMonth,o.selectedYear,n[0]),i=t.datepicker._get(o,"onSelect"),i?(s=t.datepicker._formatDate(o),i.apply(o.input?o.input[0]:null,[s,o])):t.datepicker._hideDatepicker(),!1;case 27:t.datepicker._hideDatepicker();break;case 33:t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 34:t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 35:(e.ctrlKey||e.metaKey)&&t.datepicker._clearDate(e.target),a=e.ctrlKey||e.metaKey;break;case 36:(e.ctrlKey||e.metaKey)&&t.datepicker._gotoToday(e.target),a=e.ctrlKey||e.metaKey;break;case 37:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?1:-1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?-t.datepicker._get(o,"stepBigMonths"):-t.datepicker._get(o,"stepMonths"),"M");break;case 38:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,-7,"D"),a=e.ctrlKey||e.metaKey;break;case 39:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,r?-1:1,"D"),a=e.ctrlKey||e.metaKey,e.originalEvent.altKey&&t.datepicker._adjustDate(e.target,e.ctrlKey?+t.datepicker._get(o,"stepBigMonths"):+t.datepicker._get(o,"stepMonths"),"M");break;case 40:(e.ctrlKey||e.metaKey)&&t.datepicker._adjustDate(e.target,7,"D"),a=e.ctrlKey||e.metaKey;break;default:a=!1}else 36===e.keyCode&&e.ctrlKey?t.datepicker._showDatepicker(this):a=!1;a&&(e.preventDefault(),e.stopPropagation())},_doKeyPress:function(e){var i,s,n=t.datepicker._getInst(e.target);return t.datepicker._get(n,"constrainInput")?(i=t.datepicker._possibleChars(t.datepicker._get(n,"dateFormat")),s=String.fromCharCode(null==e.charCode?e.keyCode:e.charCode),e.ctrlKey||e.metaKey||" ">s||!i||i.indexOf(s)>-1):void 0},_doKeyUp:function(e){var i,s=t.datepicker._getInst(e.target);if(s.input.val()!==s.lastVal)try{i=t.datepicker.parseDate(t.datepicker._get(s,"dateFormat"),s.input?s.input.val():null,t.datepicker._getFormatConfig(s)),i&&(t.datepicker._setDateFromField(s),t.datepicker._updateAlternate(s),t.datepicker._updateDatepicker(s))}catch(n){}return!0},_showDatepicker:function(e){if(e=e.target||e,"input"!==e.nodeName.toLowerCase()&&(e=t("input",e.parentNode)[0]),!t.datepicker._isDisabledDatepicker(e)&&t.datepicker._lastInput!==e){var s,n,o,r,h,l,c;s=t.datepicker._getInst(e),t.datepicker._curInst&&t.datepicker._curInst!==s&&(t.datepicker._curInst.dpDiv.stop(!0,!0),s&&t.datepicker._datepickerShowing&&t.datepicker._hideDatepicker(t.datepicker._curInst.input[0])),n=t.datepicker._get(s,"beforeShow"),o=n?n.apply(e,[e,s]):{},o!==!1&&(a(s.settings,o),s.lastVal=null,t.datepicker._lastInput=e,t.datepicker._setDateFromField(s),t.datepicker._inDialog&&(e.value=""),t.datepicker._pos||(t.datepicker._pos=t.datepicker._findPos(e),t.datepicker._pos[1]+=e.offsetHeight),r=!1,t(e).parents().each(function(){return r|="fixed"===t(this).css("position"),!r}),h={left:t.datepicker._pos[0],top:t.datepicker._pos[1]},t.datepicker._pos=null,s.dpDiv.empty(),s.dpDiv.css({position:"absolute",display:"block",top:"-1000px"}),t.datepicker._updateDatepicker(s),h=t.datepicker._checkOffset(s,h,r),s.dpDiv.css({position:t.datepicker._inDialog&&t.blockUI?"static":r?"fixed":"absolute",display:"none",left:h.left+"px",top:h.top+"px"}),s.inline||(l=t.datepicker._get(s,"showAnim"),c=t.datepicker._get(s,"duration"),s.dpDiv.css("z-index",i(t(e))+1),t.datepicker._datepickerShowing=!0,t.effects&&t.effects.effect[l]?s.dpDiv.show(l,t.datepicker._get(s,"showOptions"),c):s.dpDiv[l||"show"](l?c:null),t.datepicker._shouldFocusInput(s)&&s.input.trigger("focus"),t.datepicker._curInst=s)) +}},_updateDatepicker:function(e){this.maxRows=4,m=e,e.dpDiv.empty().append(this._generateHTML(e)),this._attachHandlers(e);var i,s=this._getNumberOfMonths(e),n=s[1],a=17,r=e.dpDiv.find("."+this._dayOverClass+" a");r.length>0&&o.apply(r.get(0)),e.dpDiv.removeClass("ui-datepicker-multi-2 ui-datepicker-multi-3 ui-datepicker-multi-4").width(""),n>1&&e.dpDiv.addClass("ui-datepicker-multi-"+n).css("width",a*n+"em"),e.dpDiv[(1!==s[0]||1!==s[1]?"add":"remove")+"Class"]("ui-datepicker-multi"),e.dpDiv[(this._get(e,"isRTL")?"add":"remove")+"Class"]("ui-datepicker-rtl"),e===t.datepicker._curInst&&t.datepicker._datepickerShowing&&t.datepicker._shouldFocusInput(e)&&e.input.trigger("focus"),e.yearshtml&&(i=e.yearshtml,setTimeout(function(){i===e.yearshtml&&e.yearshtml&&e.dpDiv.find("select.ui-datepicker-year:first").replaceWith(e.yearshtml),i=e.yearshtml=null},0))},_shouldFocusInput:function(t){return t.input&&t.input.is(":visible")&&!t.input.is(":disabled")&&!t.input.is(":focus")},_checkOffset:function(e,i,s){var n=e.dpDiv.outerWidth(),o=e.dpDiv.outerHeight(),a=e.input?e.input.outerWidth():0,r=e.input?e.input.outerHeight():0,h=document.documentElement.clientWidth+(s?0:t(document).scrollLeft()),l=document.documentElement.clientHeight+(s?0:t(document).scrollTop());return i.left-=this._get(e,"isRTL")?n-a:0,i.left-=s&&i.left===e.input.offset().left?t(document).scrollLeft():0,i.top-=s&&i.top===e.input.offset().top+r?t(document).scrollTop():0,i.left-=Math.min(i.left,i.left+n>h&&h>n?Math.abs(i.left+n-h):0),i.top-=Math.min(i.top,i.top+o>l&&l>o?Math.abs(o+r):0),i},_findPos:function(e){for(var i,s=this._getInst(e),n=this._get(s,"isRTL");e&&("hidden"===e.type||1!==e.nodeType||t.expr.filters.hidden(e));)e=e[n?"previousSibling":"nextSibling"];return i=t(e).offset(),[i.left,i.top]},_hideDatepicker:function(e){var i,s,n,o,a=this._curInst;!a||e&&a!==t.data(e,"datepicker")||this._datepickerShowing&&(i=this._get(a,"showAnim"),s=this._get(a,"duration"),n=function(){t.datepicker._tidyDialog(a)},t.effects&&(t.effects.effect[i]||t.effects[i])?a.dpDiv.hide(i,t.datepicker._get(a,"showOptions"),s,n):a.dpDiv["slideDown"===i?"slideUp":"fadeIn"===i?"fadeOut":"hide"](i?s:null,n),i||n(),this._datepickerShowing=!1,o=this._get(a,"onClose"),o&&o.apply(a.input?a.input[0]:null,[a.input?a.input.val():"",a]),this._lastInput=null,this._inDialog&&(this._dialogInput.css({position:"absolute",left:"0",top:"-100px"}),t.blockUI&&(t.unblockUI(),t("body").append(this.dpDiv))),this._inDialog=!1)},_tidyDialog:function(t){t.dpDiv.removeClass(this._dialogClass).off(".ui-datepicker-calendar")},_checkExternalClick:function(e){if(t.datepicker._curInst){var i=t(e.target),s=t.datepicker._getInst(i[0]);(i[0].id!==t.datepicker._mainDivId&&0===i.parents("#"+t.datepicker._mainDivId).length&&!i.hasClass(t.datepicker.markerClassName)&&!i.closest("."+t.datepicker._triggerClass).length&&t.datepicker._datepickerShowing&&(!t.datepicker._inDialog||!t.blockUI)||i.hasClass(t.datepicker.markerClassName)&&t.datepicker._curInst!==s)&&t.datepicker._hideDatepicker()}},_adjustDate:function(e,i,s){var n=t(e),o=this._getInst(n[0]);this._isDisabledDatepicker(n[0])||(this._adjustInstDate(o,i+("M"===s?this._get(o,"showCurrentAtPos"):0),s),this._updateDatepicker(o))},_gotoToday:function(e){var i,s=t(e),n=this._getInst(s[0]);this._get(n,"gotoCurrent")&&n.currentDay?(n.selectedDay=n.currentDay,n.drawMonth=n.selectedMonth=n.currentMonth,n.drawYear=n.selectedYear=n.currentYear):(i=new Date,n.selectedDay=i.getDate(),n.drawMonth=n.selectedMonth=i.getMonth(),n.drawYear=n.selectedYear=i.getFullYear()),this._notifyChange(n),this._adjustDate(s)},_selectMonthYear:function(e,i,s){var n=t(e),o=this._getInst(n[0]);o["selected"+("M"===s?"Month":"Year")]=o["draw"+("M"===s?"Month":"Year")]=parseInt(i.options[i.selectedIndex].value,10),this._notifyChange(o),this._adjustDate(n)},_selectDay:function(e,i,s,n){var o,a=t(e);t(n).hasClass(this._unselectableClass)||this._isDisabledDatepicker(a[0])||(o=this._getInst(a[0]),o.selectedDay=o.currentDay=t("a",n).html(),o.selectedMonth=o.currentMonth=i,o.selectedYear=o.currentYear=s,this._selectDate(e,this._formatDate(o,o.currentDay,o.currentMonth,o.currentYear)))},_clearDate:function(e){var i=t(e);this._selectDate(i,"")},_selectDate:function(e,i){var s,n=t(e),o=this._getInst(n[0]);i=null!=i?i:this._formatDate(o),o.input&&o.input.val(i),this._updateAlternate(o),s=this._get(o,"onSelect"),s?s.apply(o.input?o.input[0]:null,[i,o]):o.input&&o.input.trigger("change"),o.inline?this._updateDatepicker(o):(this._hideDatepicker(),this._lastInput=o.input[0],"object"!=typeof o.input[0]&&o.input.trigger("focus"),this._lastInput=null)},_updateAlternate:function(e){var i,s,n,o=this._get(e,"altField");o&&(i=this._get(e,"altFormat")||this._get(e,"dateFormat"),s=this._getDate(e),n=this.formatDate(i,s,this._getFormatConfig(e)),t(o).val(n))},noWeekends:function(t){var e=t.getDay();return[e>0&&6>e,""]},iso8601Week:function(t){var e,i=new Date(t.getTime());return i.setDate(i.getDate()+4-(i.getDay()||7)),e=i.getTime(),i.setMonth(0),i.setDate(1),Math.floor(Math.round((e-i)/864e5)/7)+1},parseDate:function(e,i,s){if(null==e||null==i)throw"Invalid arguments";if(i="object"==typeof i?""+i:i+"",""===i)return null;var n,o,a,r,h=0,l=(s?s.shortYearCutoff:null)||this._defaults.shortYearCutoff,c="string"!=typeof l?l:(new Date).getFullYear()%100+parseInt(l,10),u=(s?s.dayNamesShort:null)||this._defaults.dayNamesShort,d=(s?s.dayNames:null)||this._defaults.dayNames,p=(s?s.monthNamesShort:null)||this._defaults.monthNamesShort,f=(s?s.monthNames:null)||this._defaults.monthNames,g=-1,m=-1,_=-1,v=-1,b=!1,y=function(t){var i=e.length>n+1&&e.charAt(n+1)===t;return i&&n++,i},w=function(t){var e=y(t),s="@"===t?14:"!"===t?20:"y"===t&&e?4:"o"===t?3:2,n="y"===t?s:1,o=RegExp("^\\d{"+n+","+s+"}"),a=i.substring(h).match(o);if(!a)throw"Missing number at position "+h;return h+=a[0].length,parseInt(a[0],10)},k=function(e,s,n){var o=-1,a=t.map(y(e)?n:s,function(t,e){return[[e,t]]}).sort(function(t,e){return-(t[1].length-e[1].length)});if(t.each(a,function(t,e){var s=e[1];return i.substr(h,s.length).toLowerCase()===s.toLowerCase()?(o=e[0],h+=s.length,!1):void 0}),-1!==o)return o+1;throw"Unknown name at position "+h},x=function(){if(i.charAt(h)!==e.charAt(n))throw"Unexpected literal at position "+h;h++};for(n=0;e.length>n;n++)if(b)"'"!==e.charAt(n)||y("'")?x():b=!1;else switch(e.charAt(n)){case"d":_=w("d");break;case"D":k("D",u,d);break;case"o":v=w("o");break;case"m":m=w("m");break;case"M":m=k("M",p,f);break;case"y":g=w("y");break;case"@":r=new Date(w("@")),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();break;case"!":r=new Date((w("!")-this._ticksTo1970)/1e4),g=r.getFullYear(),m=r.getMonth()+1,_=r.getDate();break;case"'":y("'")?x():b=!0;break;default:x()}if(i.length>h&&(a=i.substr(h),!/^\s+/.test(a)))throw"Extra/unparsed characters found in date: "+a;if(-1===g?g=(new Date).getFullYear():100>g&&(g+=(new Date).getFullYear()-(new Date).getFullYear()%100+(c>=g?0:-100)),v>-1)for(m=1,_=v;;){if(o=this._getDaysInMonth(g,m-1),o>=_)break;m++,_-=o}if(r=this._daylightSavingAdjust(new Date(g,m-1,_)),r.getFullYear()!==g||r.getMonth()+1!==m||r.getDate()!==_)throw"Invalid date";return r},ATOM:"yy-mm-dd",COOKIE:"D, dd M yy",ISO_8601:"yy-mm-dd",RFC_822:"D, d M y",RFC_850:"DD, dd-M-y",RFC_1036:"D, d M y",RFC_1123:"D, d M yy",RFC_2822:"D, d M yy",RSS:"D, d M y",TICKS:"!",TIMESTAMP:"@",W3C:"yy-mm-dd",_ticksTo1970:1e7*60*60*24*(718685+Math.floor(492.5)-Math.floor(19.7)+Math.floor(4.925)),formatDate:function(t,e,i){if(!e)return"";var s,n=(i?i.dayNamesShort:null)||this._defaults.dayNamesShort,o=(i?i.dayNames:null)||this._defaults.dayNames,a=(i?i.monthNamesShort:null)||this._defaults.monthNamesShort,r=(i?i.monthNames:null)||this._defaults.monthNames,h=function(e){var i=t.length>s+1&&t.charAt(s+1)===e;return i&&s++,i},l=function(t,e,i){var s=""+e;if(h(t))for(;i>s.length;)s="0"+s;return s},c=function(t,e,i,s){return h(t)?s[e]:i[e]},u="",d=!1;if(e)for(s=0;t.length>s;s++)if(d)"'"!==t.charAt(s)||h("'")?u+=t.charAt(s):d=!1;else switch(t.charAt(s)){case"d":u+=l("d",e.getDate(),2);break;case"D":u+=c("D",e.getDay(),n,o);break;case"o":u+=l("o",Math.round((new Date(e.getFullYear(),e.getMonth(),e.getDate()).getTime()-new Date(e.getFullYear(),0,0).getTime())/864e5),3);break;case"m":u+=l("m",e.getMonth()+1,2);break;case"M":u+=c("M",e.getMonth(),a,r);break;case"y":u+=h("y")?e.getFullYear():(10>e.getFullYear()%100?"0":"")+e.getFullYear()%100;break;case"@":u+=e.getTime();break;case"!":u+=1e4*e.getTime()+this._ticksTo1970;break;case"'":h("'")?u+="'":d=!0;break;default:u+=t.charAt(s)}return u},_possibleChars:function(t){var e,i="",s=!1,n=function(i){var s=t.length>e+1&&t.charAt(e+1)===i;return s&&e++,s};for(e=0;t.length>e;e++)if(s)"'"!==t.charAt(e)||n("'")?i+=t.charAt(e):s=!1;else switch(t.charAt(e)){case"d":case"m":case"y":case"@":i+="0123456789";break;case"D":case"M":return null;case"'":n("'")?i+="'":s=!0;break;default:i+=t.charAt(e)}return i},_get:function(t,e){return void 0!==t.settings[e]?t.settings[e]:this._defaults[e]},_setDateFromField:function(t,e){if(t.input.val()!==t.lastVal){var i=this._get(t,"dateFormat"),s=t.lastVal=t.input?t.input.val():null,n=this._getDefaultDate(t),o=n,a=this._getFormatConfig(t);try{o=this.parseDate(i,s,a)||n}catch(r){s=e?"":s}t.selectedDay=o.getDate(),t.drawMonth=t.selectedMonth=o.getMonth(),t.drawYear=t.selectedYear=o.getFullYear(),t.currentDay=s?o.getDate():0,t.currentMonth=s?o.getMonth():0,t.currentYear=s?o.getFullYear():0,this._adjustInstDate(t)}},_getDefaultDate:function(t){return this._restrictMinMax(t,this._determineDate(t,this._get(t,"defaultDate"),new Date))},_determineDate:function(e,i,s){var n=function(t){var e=new Date;return e.setDate(e.getDate()+t),e},o=function(i){try{return t.datepicker.parseDate(t.datepicker._get(e,"dateFormat"),i,t.datepicker._getFormatConfig(e))}catch(s){}for(var n=(i.toLowerCase().match(/^c/)?t.datepicker._getDate(e):null)||new Date,o=n.getFullYear(),a=n.getMonth(),r=n.getDate(),h=/([+\-]?[0-9]+)\s*(d|D|w|W|m|M|y|Y)?/g,l=h.exec(i);l;){switch(l[2]||"d"){case"d":case"D":r+=parseInt(l[1],10);break;case"w":case"W":r+=7*parseInt(l[1],10);break;case"m":case"M":a+=parseInt(l[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a));break;case"y":case"Y":o+=parseInt(l[1],10),r=Math.min(r,t.datepicker._getDaysInMonth(o,a))}l=h.exec(i)}return new Date(o,a,r)},a=null==i||""===i?s:"string"==typeof i?o(i):"number"==typeof i?isNaN(i)?s:n(i):new Date(i.getTime());return a=a&&"Invalid Date"==""+a?s:a,a&&(a.setHours(0),a.setMinutes(0),a.setSeconds(0),a.setMilliseconds(0)),this._daylightSavingAdjust(a)},_daylightSavingAdjust:function(t){return t?(t.setHours(t.getHours()>12?t.getHours()+2:0),t):null},_setDate:function(t,e,i){var s=!e,n=t.selectedMonth,o=t.selectedYear,a=this._restrictMinMax(t,this._determineDate(t,e,new Date));t.selectedDay=t.currentDay=a.getDate(),t.drawMonth=t.selectedMonth=t.currentMonth=a.getMonth(),t.drawYear=t.selectedYear=t.currentYear=a.getFullYear(),n===t.selectedMonth&&o===t.selectedYear||i||this._notifyChange(t),this._adjustInstDate(t),t.input&&t.input.val(s?"":this._formatDate(t))},_getDate:function(t){var e=!t.currentYear||t.input&&""===t.input.val()?null:this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return e},_attachHandlers:function(e){var i=this._get(e,"stepMonths"),s="#"+e.id.replace(/\\\\/g,"\\");e.dpDiv.find("[data-handler]").map(function(){var e={prev:function(){t.datepicker._adjustDate(s,-i,"M")},next:function(){t.datepicker._adjustDate(s,+i,"M")},hide:function(){t.datepicker._hideDatepicker()},today:function(){t.datepicker._gotoToday(s)},selectDay:function(){return t.datepicker._selectDay(s,+this.getAttribute("data-month"),+this.getAttribute("data-year"),this),!1},selectMonth:function(){return t.datepicker._selectMonthYear(s,this,"M"),!1},selectYear:function(){return t.datepicker._selectMonthYear(s,this,"Y"),!1}};t(this).on(this.getAttribute("data-event"),e[this.getAttribute("data-handler")])})},_generateHTML:function(t){var e,i,s,n,o,a,r,h,l,c,u,d,p,f,g,m,_,v,b,y,w,k,x,C,D,I,T,P,M,S,H,z,O,A,N,W,E,F,L,R=new Date,B=this._daylightSavingAdjust(new Date(R.getFullYear(),R.getMonth(),R.getDate())),Y=this._get(t,"isRTL"),j=this._get(t,"showButtonPanel"),q=this._get(t,"hideIfNoPrevNext"),K=this._get(t,"navigationAsDateFormat"),U=this._getNumberOfMonths(t),V=this._get(t,"showCurrentAtPos"),$=this._get(t,"stepMonths"),X=1!==U[0]||1!==U[1],G=this._daylightSavingAdjust(t.currentDay?new Date(t.currentYear,t.currentMonth,t.currentDay):new Date(9999,9,9)),Q=this._getMinMaxDate(t,"min"),J=this._getMinMaxDate(t,"max"),Z=t.drawMonth-V,te=t.drawYear;if(0>Z&&(Z+=12,te--),J)for(e=this._daylightSavingAdjust(new Date(J.getFullYear(),J.getMonth()-U[0]*U[1]+1,J.getDate())),e=Q&&Q>e?Q:e;this._daylightSavingAdjust(new Date(te,Z,1))>e;)Z--,0>Z&&(Z=11,te--);for(t.drawMonth=Z,t.drawYear=te,i=this._get(t,"prevText"),i=K?this.formatDate(i,this._daylightSavingAdjust(new Date(te,Z-$,1)),this._getFormatConfig(t)):i,s=this._canAdjustMonth(t,-1,te,Z)?""+i+"":q?"":""+i+"",n=this._get(t,"nextText"),n=K?this.formatDate(n,this._daylightSavingAdjust(new Date(te,Z+$,1)),this._getFormatConfig(t)):n,o=this._canAdjustMonth(t,1,te,Z)?""+n+"":q?"":""+n+"",a=this._get(t,"currentText"),r=this._get(t,"gotoCurrent")&&t.currentDay?G:B,a=K?this.formatDate(a,r,this._getFormatConfig(t)):a,h=t.inline?"":"",l=j?"
    "+(Y?h:"")+(this._isInRange(t,r)?"":"")+(Y?"":h)+"
    ":"",c=parseInt(this._get(t,"firstDay"),10),c=isNaN(c)?0:c,u=this._get(t,"showWeek"),d=this._get(t,"dayNames"),p=this._get(t,"dayNamesMin"),f=this._get(t,"monthNames"),g=this._get(t,"monthNamesShort"),m=this._get(t,"beforeShowDay"),_=this._get(t,"showOtherMonths"),v=this._get(t,"selectOtherMonths"),b=this._getDefaultDate(t),y="",k=0;U[0]>k;k++){for(x="",this.maxRows=4,C=0;U[1]>C;C++){if(D=this._daylightSavingAdjust(new Date(te,Z,t.selectedDay)),I=" ui-corner-all",T="",X){if(T+="
    "}for(T+="
    "+(/all|left/.test(I)&&0===k?Y?o:s:"")+(/all|right/.test(I)&&0===k?Y?s:o:"")+this._generateMonthYearHeader(t,Z,te,Q,J,k>0||C>0,f,g)+"
    "+"",P=u?"":"",w=0;7>w;w++)M=(w+c)%7,P+="";for(T+=P+"",S=this._getDaysInMonth(te,Z),te===t.selectedYear&&Z===t.selectedMonth&&(t.selectedDay=Math.min(t.selectedDay,S)),H=(this._getFirstDayOfMonth(te,Z)-c+7)%7,z=Math.ceil((H+S)/7),O=X?this.maxRows>z?this.maxRows:z:z,this.maxRows=O,A=this._daylightSavingAdjust(new Date(te,Z,1-H)),N=0;O>N;N++){for(T+="",W=u?"":"",w=0;7>w;w++)E=m?m.apply(t.input?t.input[0]:null,[A]):[!0,""],F=A.getMonth()!==Z,L=F&&!v||!E[0]||Q&&Q>A||J&&A>J,W+="",A.setDate(A.getDate()+1),A=this._daylightSavingAdjust(A);T+=W+""}Z++,Z>11&&(Z=0,te++),T+="
    "+this._get(t,"weekHeader")+"=5?" class='ui-datepicker-week-end'":"")+">"+""+p[M]+"
    "+this._get(t,"calculateWeek")(A)+""+(F&&!_?" ":L?""+A.getDate()+"":""+A.getDate()+"")+"
    "+(X?"
    "+(U[0]>0&&C===U[1]-1?"
    ":""):""),x+=T}y+=x}return y+=l,t._keyEvent=!1,y},_generateMonthYearHeader:function(t,e,i,s,n,o,a,r){var h,l,c,u,d,p,f,g,m=this._get(t,"changeMonth"),_=this._get(t,"changeYear"),v=this._get(t,"showMonthAfterYear"),b="
    ",y="";if(o||!m)y+=""+a[e]+"";else{for(h=s&&s.getFullYear()===i,l=n&&n.getFullYear()===i,y+=""}if(v||(b+=y+(!o&&m&&_?"":" ")),!t.yearshtml)if(t.yearshtml="",o||!_)b+=""+i+"";else{for(u=this._get(t,"yearRange").split(":"),d=(new Date).getFullYear(),p=function(t){var e=t.match(/c[+\-].*/)?i+parseInt(t.substring(1),10):t.match(/[+\-].*/)?d+parseInt(t,10):parseInt(t,10);return isNaN(e)?d:e},f=p(u[0]),g=Math.max(f,p(u[1]||"")),f=s?Math.max(f,s.getFullYear()):f,g=n?Math.min(g,n.getFullYear()):g,t.yearshtml+="",b+=t.yearshtml,t.yearshtml=null}return b+=this._get(t,"yearSuffix"),v&&(b+=(!o&&m&&_?"":" ")+y),b+="
    "},_adjustInstDate:function(t,e,i){var s=t.selectedYear+("Y"===i?e:0),n=t.selectedMonth+("M"===i?e:0),o=Math.min(t.selectedDay,this._getDaysInMonth(s,n))+("D"===i?e:0),a=this._restrictMinMax(t,this._daylightSavingAdjust(new Date(s,n,o)));t.selectedDay=a.getDate(),t.drawMonth=t.selectedMonth=a.getMonth(),t.drawYear=t.selectedYear=a.getFullYear(),("M"===i||"Y"===i)&&this._notifyChange(t)},_restrictMinMax:function(t,e){var i=this._getMinMaxDate(t,"min"),s=this._getMinMaxDate(t,"max"),n=i&&i>e?i:e;return s&&n>s?s:n},_notifyChange:function(t){var e=this._get(t,"onChangeMonthYear");e&&e.apply(t.input?t.input[0]:null,[t.selectedYear,t.selectedMonth+1,t])},_getNumberOfMonths:function(t){var e=this._get(t,"numberOfMonths");return null==e?[1,1]:"number"==typeof e?[1,e]:e},_getMinMaxDate:function(t,e){return this._determineDate(t,this._get(t,e+"Date"),null)},_getDaysInMonth:function(t,e){return 32-this._daylightSavingAdjust(new Date(t,e,32)).getDate()},_getFirstDayOfMonth:function(t,e){return new Date(t,e,1).getDay()},_canAdjustMonth:function(t,e,i,s){var n=this._getNumberOfMonths(t),o=this._daylightSavingAdjust(new Date(i,s+(0>e?e:n[0]*n[1]),1));return 0>e&&o.setDate(this._getDaysInMonth(o.getFullYear(),o.getMonth())),this._isInRange(t,o)},_isInRange:function(t,e){var i,s,n=this._getMinMaxDate(t,"min"),o=this._getMinMaxDate(t,"max"),a=null,r=null,h=this._get(t,"yearRange");return h&&(i=h.split(":"),s=(new Date).getFullYear(),a=parseInt(i[0],10),r=parseInt(i[1],10),i[0].match(/[+\-].*/)&&(a+=s),i[1].match(/[+\-].*/)&&(r+=s)),(!n||e.getTime()>=n.getTime())&&(!o||e.getTime()<=o.getTime())&&(!a||e.getFullYear()>=a)&&(!r||r>=e.getFullYear())},_getFormatConfig:function(t){var e=this._get(t,"shortYearCutoff");return e="string"!=typeof e?e:(new Date).getFullYear()%100+parseInt(e,10),{shortYearCutoff:e,dayNamesShort:this._get(t,"dayNamesShort"),dayNames:this._get(t,"dayNames"),monthNamesShort:this._get(t,"monthNamesShort"),monthNames:this._get(t,"monthNames")}},_formatDate:function(t,e,i,s){e||(t.currentDay=t.selectedDay,t.currentMonth=t.selectedMonth,t.currentYear=t.selectedYear);var n=e?"object"==typeof e?e:this._daylightSavingAdjust(new Date(s,i,e)):this._daylightSavingAdjust(new Date(t.currentYear,t.currentMonth,t.currentDay));return this.formatDate(this._get(t,"dateFormat"),n,this._getFormatConfig(t))}}),t.fn.datepicker=function(e){if(!this.length)return this;t.datepicker.initialized||(t(document).on("mousedown",t.datepicker._checkExternalClick),t.datepicker.initialized=!0),0===t("#"+t.datepicker._mainDivId).length&&t("body").append(t.datepicker.dpDiv);var i=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!==e&&"getDate"!==e&&"widget"!==e?"option"===e&&2===arguments.length&&"string"==typeof arguments[1]?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i)):this.each(function(){"string"==typeof e?t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this].concat(i)):t.datepicker._attachDatepicker(this,e)}):t.datepicker["_"+e+"Datepicker"].apply(t.datepicker,[this[0]].concat(i))},t.datepicker=new s,t.datepicker.initialized=!1,t.datepicker.uuid=(new Date).getTime(),t.datepicker.version="1.12.1",t.datepicker,t.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase());var _=!1;t(document).on("mouseup",function(){_=!1}),t.widget("ui.mouse",{version:"1.12.1",options:{cancel:"input, textarea, button, select, option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.on("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).on("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.off("."+this.widgetName),this._mouseMoveDelegate&&this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!_){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,s=1===e.which,n="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return s&&!n&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.on("mousemove."+this.widgetName,this._mouseMoveDelegate).on("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),_=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)if(e.originalEvent.altKey||e.originalEvent.ctrlKey||e.originalEvent.metaKey||e.originalEvent.shiftKey)this.ignoreMissingWhich=!0;else if(!this.ignoreMissingWhich)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){this.document.off("mousemove."+this.widgetName,this._mouseMoveDelegate).off("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),this._mouseDelayTimer&&(clearTimeout(this._mouseDelayTimer),delete this._mouseDelayTimer),this.ignoreMissingWhich=!1,_=!1,e.preventDefault()},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.ui.plugin={add:function(e,i,s){var n,o=t.ui[e].prototype;for(n in s)o.plugins[n]=o.plugins[n]||[],o.plugins[n].push([i,s[n]])},call:function(t,e,i,s){var n,o=t.plugins[e];if(o&&(s||t.element[0].parentNode&&11!==t.element[0].parentNode.nodeType))for(n=0;o.length>n;n++)t.options[o[n][0]]&&o[n][1].apply(t.element,i)}},t.ui.safeBlur=function(e){e&&"body"!==e.nodeName.toLowerCase()&&t(e).trigger("blur")},t.widget("ui.draggable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"drag",options:{addClasses:!0,appendTo:"parent",axis:!1,connectToSortable:!1,containment:!1,cursor:"auto",cursorAt:!1,grid:!1,handle:!1,helper:"original",iframeFix:!1,opacity:!1,refreshPositions:!1,revert:!1,revertDuration:500,scope:"default",scroll:!0,scrollSensitivity:20,scrollSpeed:20,snap:!1,snapMode:"both",snapTolerance:20,stack:!1,zIndex:!1,drag:null,start:null,stop:null},_create:function(){"original"===this.options.helper&&this._setPositionRelative(),this.options.addClasses&&this._addClass("ui-draggable"),this._setHandleClassName(),this._mouseInit()},_setOption:function(t,e){this._super(t,e),"handle"===t&&(this._removeHandleClassName(),this._setHandleClassName())},_destroy:function(){return(this.helper||this.element).is(".ui-draggable-dragging")?(this.destroyOnClear=!0,void 0):(this._removeHandleClassName(),this._mouseDestroy(),void 0)},_mouseCapture:function(e){var i=this.options;return this.helper||i.disabled||t(e.target).closest(".ui-resizable-handle").length>0?!1:(this.handle=this._getHandle(e),this.handle?(this._blurActiveElement(e),this._blockFrames(i.iframeFix===!0?"iframe":i.iframeFix),!0):!1)},_blockFrames:function(e){this.iframeBlocks=this.document.find(e).map(function(){var e=t(this);return t("
    ").css("position","absolute").appendTo(e.parent()).outerWidth(e.outerWidth()).outerHeight(e.outerHeight()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_blurActiveElement:function(e){var i=t.ui.safeActiveElement(this.document[0]),s=t(e.target);s.closest(i).length||t.ui.safeBlur(i)},_mouseStart:function(e){var i=this.options;return this.helper=this._createHelper(e),this._addClass(this.helper,"ui-draggable-dragging"),this._cacheHelperProportions(),t.ui.ddmanager&&(t.ui.ddmanager.current=this),this._cacheMargins(),this.cssPosition=this.helper.css("position"),this.scrollParent=this.helper.scrollParent(!0),this.offsetParent=this.helper.offsetParent(),this.hasFixedAncestor=this.helper.parents().filter(function(){return"fixed"===t(this).css("position")}).length>0,this.positionAbs=this.element.offset(),this._refreshOffsets(e),this.originalPosition=this.position=this._generatePosition(e,!1),this.originalPageX=e.pageX,this.originalPageY=e.pageY,i.cursorAt&&this._adjustOffsetFromHelper(i.cursorAt),this._setContainment(),this._trigger("start",e)===!1?(this._clear(),!1):(this._cacheHelperProportions(),t.ui.ddmanager&&!i.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this._mouseDrag(e,!0),t.ui.ddmanager&&t.ui.ddmanager.dragStart(this,e),!0)},_refreshOffsets:function(t){this.offset={top:this.positionAbs.top-this.margins.top,left:this.positionAbs.left-this.margins.left,scroll:!1,parent:this._getParentOffset(),relative:this._getRelativeOffset()},this.offset.click={left:t.pageX-this.offset.left,top:t.pageY-this.offset.top}},_mouseDrag:function(e,i){if(this.hasFixedAncestor&&(this.offset.parent=this._getParentOffset()),this.position=this._generatePosition(e,!0),this.positionAbs=this._convertPositionTo("absolute"),!i){var s=this._uiHash();if(this._trigger("drag",e,s)===!1)return this._mouseUp(new t.Event("mouseup",e)),!1;this.position=s.position}return this.helper[0].style.left=this.position.left+"px",this.helper[0].style.top=this.position.top+"px",t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),!1},_mouseStop:function(e){var i=this,s=!1;return t.ui.ddmanager&&!this.options.dropBehaviour&&(s=t.ui.ddmanager.drop(this,e)),this.dropped&&(s=this.dropped,this.dropped=!1),"invalid"===this.options.revert&&!s||"valid"===this.options.revert&&s||this.options.revert===!0||t.isFunction(this.options.revert)&&this.options.revert.call(this.element,s)?t(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){i._trigger("stop",e)!==!1&&i._clear()}):this._trigger("stop",e)!==!1&&this._clear(),!1},_mouseUp:function(e){return this._unblockFrames(),t.ui.ddmanager&&t.ui.ddmanager.dragStop(this,e),this.handleElement.is(e.target)&&this.element.trigger("focus"),t.ui.mouse.prototype._mouseUp.call(this,e)},cancel:function(){return this.helper.is(".ui-draggable-dragging")?this._mouseUp(new t.Event("mouseup",{target:this.element[0]})):this._clear(),this},_getHandle:function(e){return this.options.handle?!!t(e.target).closest(this.element.find(this.options.handle)).length:!0},_setHandleClassName:function(){this.handleElement=this.options.handle?this.element.find(this.options.handle):this.element,this._addClass(this.handleElement,"ui-draggable-handle")},_removeHandleClassName:function(){this._removeClass(this.handleElement,"ui-draggable-handle")},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper),n=s?t(i.helper.apply(this.element[0],[e])):"clone"===i.helper?this.element.clone().removeAttr("id"):this.element;return n.parents("body").length||n.appendTo("parent"===i.appendTo?this.element[0].parentNode:i.appendTo),s&&n[0]===this.element[0]&&this._setPositionRelative(),n[0]===this.element[0]||/(fixed|absolute)/.test(n.css("position"))||n.css("position","absolute"),n},_setPositionRelative:function(){/^(?:r|a|f)/.test(this.element.css("position"))||(this.element[0].style.position="relative")},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_isRootNode:function(t){return/(html|body)/i.test(t.tagName)||t===this.document[0]},_getParentOffset:function(){var e=this.offsetParent.offset(),i=this.document[0];return"absolute"===this.cssPosition&&this.scrollParent[0]!==i&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),this._isRootNode(this.offsetParent[0])&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"!==this.cssPosition)return{top:0,left:0};var t=this.element.position(),e=this._isRootNode(this.scrollParent[0]);return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+(e?0:this.scrollParent.scrollTop()),left:t.left-(parseInt(this.helper.css("left"),10)||0)+(e?0:this.scrollParent.scrollLeft())} +},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0,right:parseInt(this.element.css("marginRight"),10)||0,bottom:parseInt(this.element.css("marginBottom"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options,o=this.document[0];return this.relativeContainer=null,n.containment?"window"===n.containment?(this.containment=[t(window).scrollLeft()-this.offset.relative.left-this.offset.parent.left,t(window).scrollTop()-this.offset.relative.top-this.offset.parent.top,t(window).scrollLeft()+t(window).width()-this.helperProportions.width-this.margins.left,t(window).scrollTop()+(t(window).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):"document"===n.containment?(this.containment=[0,0,t(o).width()-this.helperProportions.width-this.margins.left,(t(o).height()||o.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top],void 0):n.containment.constructor===Array?(this.containment=n.containment,void 0):("parent"===n.containment&&(n.containment=this.helper[0].parentNode),i=t(n.containment),s=i[0],s&&(e=/(scroll|auto)/.test(i.css("overflow")),this.containment=[(parseInt(i.css("borderLeftWidth"),10)||0)+(parseInt(i.css("paddingLeft"),10)||0),(parseInt(i.css("borderTopWidth"),10)||0)+(parseInt(i.css("paddingTop"),10)||0),(e?Math.max(s.scrollWidth,s.offsetWidth):s.offsetWidth)-(parseInt(i.css("borderRightWidth"),10)||0)-(parseInt(i.css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left-this.margins.right,(e?Math.max(s.scrollHeight,s.offsetHeight):s.offsetHeight)-(parseInt(i.css("borderBottomWidth"),10)||0)-(parseInt(i.css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top-this.margins.bottom],this.relativeContainer=i),void 0):(this.containment=null,void 0)},_convertPositionTo:function(t,e){e||(e=this.position);var i="absolute"===t?1:-1,s=this._isRootNode(this.scrollParent[0]);return{top:e.top+this.offset.relative.top*i+this.offset.parent.top*i-("fixed"===this.cssPosition?-this.offset.scroll.top:s?0:this.offset.scroll.top)*i,left:e.left+this.offset.relative.left*i+this.offset.parent.left*i-("fixed"===this.cssPosition?-this.offset.scroll.left:s?0:this.offset.scroll.left)*i}},_generatePosition:function(t,e){var i,s,n,o,a=this.options,r=this._isRootNode(this.scrollParent[0]),h=t.pageX,l=t.pageY;return r&&this.offset.scroll||(this.offset.scroll={top:this.scrollParent.scrollTop(),left:this.scrollParent.scrollLeft()}),e&&(this.containment&&(this.relativeContainer?(s=this.relativeContainer.offset(),i=[this.containment[0]+s.left,this.containment[1]+s.top,this.containment[2]+s.left,this.containment[3]+s.top]):i=this.containment,t.pageX-this.offset.click.lefti[2]&&(h=i[2]+this.offset.click.left),t.pageY-this.offset.click.top>i[3]&&(l=i[3]+this.offset.click.top)),a.grid&&(n=a.grid[1]?this.originalPageY+Math.round((l-this.originalPageY)/a.grid[1])*a.grid[1]:this.originalPageY,l=i?n-this.offset.click.top>=i[1]||n-this.offset.click.top>i[3]?n:n-this.offset.click.top>=i[1]?n-a.grid[1]:n+a.grid[1]:n,o=a.grid[0]?this.originalPageX+Math.round((h-this.originalPageX)/a.grid[0])*a.grid[0]:this.originalPageX,h=i?o-this.offset.click.left>=i[0]||o-this.offset.click.left>i[2]?o:o-this.offset.click.left>=i[0]?o-a.grid[0]:o+a.grid[0]:o),"y"===a.axis&&(h=this.originalPageX),"x"===a.axis&&(l=this.originalPageY)),{top:l-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.offset.scroll.top:r?0:this.offset.scroll.top),left:h-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.offset.scroll.left:r?0:this.offset.scroll.left)}},_clear:function(){this._removeClass(this.helper,"ui-draggable-dragging"),this.helper[0]===this.element[0]||this.cancelHelperRemoval||this.helper.remove(),this.helper=null,this.cancelHelperRemoval=!1,this.destroyOnClear&&this.destroy()},_trigger:function(e,i,s){return s=s||this._uiHash(),t.ui.plugin.call(this,e,[i,s,this],!0),/^(drag|start|stop)/.test(e)&&(this.positionAbs=this._convertPositionTo("absolute"),s.offset=this.positionAbs),t.Widget.prototype._trigger.call(this,e,i,s)},plugins:{},_uiHash:function(){return{helper:this.helper,position:this.position,originalPosition:this.originalPosition,offset:this.positionAbs}}}),t.ui.plugin.add("draggable","connectToSortable",{start:function(e,i,s){var n=t.extend({},i,{item:s.element});s.sortables=[],t(s.options.connectToSortable).each(function(){var i=t(this).sortable("instance");i&&!i.options.disabled&&(s.sortables.push(i),i.refreshPositions(),i._trigger("activate",e,n))})},stop:function(e,i,s){var n=t.extend({},i,{item:s.element});s.cancelHelperRemoval=!1,t.each(s.sortables,function(){var t=this;t.isOver?(t.isOver=0,s.cancelHelperRemoval=!0,t.cancelHelperRemoval=!1,t._storedCSS={position:t.placeholder.css("position"),top:t.placeholder.css("top"),left:t.placeholder.css("left")},t._mouseStop(e),t.options.helper=t.options._helper):(t.cancelHelperRemoval=!0,t._trigger("deactivate",e,n))})},drag:function(e,i,s){t.each(s.sortables,function(){var n=!1,o=this;o.positionAbs=s.positionAbs,o.helperProportions=s.helperProportions,o.offset.click=s.offset.click,o._intersectsWith(o.containerCache)&&(n=!0,t.each(s.sortables,function(){return this.positionAbs=s.positionAbs,this.helperProportions=s.helperProportions,this.offset.click=s.offset.click,this!==o&&this._intersectsWith(this.containerCache)&&t.contains(o.element[0],this.element[0])&&(n=!1),n})),n?(o.isOver||(o.isOver=1,s._parent=i.helper.parent(),o.currentItem=i.helper.appendTo(o.element).data("ui-sortable-item",!0),o.options._helper=o.options.helper,o.options.helper=function(){return i.helper[0]},e.target=o.currentItem[0],o._mouseCapture(e,!0),o._mouseStart(e,!0,!0),o.offset.click.top=s.offset.click.top,o.offset.click.left=s.offset.click.left,o.offset.parent.left-=s.offset.parent.left-o.offset.parent.left,o.offset.parent.top-=s.offset.parent.top-o.offset.parent.top,s._trigger("toSortable",e),s.dropped=o.element,t.each(s.sortables,function(){this.refreshPositions()}),s.currentItem=s.element,o.fromOutside=s),o.currentItem&&(o._mouseDrag(e),i.position=o.position)):o.isOver&&(o.isOver=0,o.cancelHelperRemoval=!0,o.options._revert=o.options.revert,o.options.revert=!1,o._trigger("out",e,o._uiHash(o)),o._mouseStop(e,!0),o.options.revert=o.options._revert,o.options.helper=o.options._helper,o.placeholder&&o.placeholder.remove(),i.helper.appendTo(s._parent),s._refreshOffsets(e),i.position=s._generatePosition(e,!0),s._trigger("fromSortable",e),s.dropped=!1,t.each(s.sortables,function(){this.refreshPositions()}))})}}),t.ui.plugin.add("draggable","cursor",{start:function(e,i,s){var n=t("body"),o=s.options;n.css("cursor")&&(o._cursor=n.css("cursor")),n.css("cursor",o.cursor)},stop:function(e,i,s){var n=s.options;n._cursor&&t("body").css("cursor",n._cursor)}}),t.ui.plugin.add("draggable","opacity",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("opacity")&&(o._opacity=n.css("opacity")),n.css("opacity",o.opacity)},stop:function(e,i,s){var n=s.options;n._opacity&&t(i.helper).css("opacity",n._opacity)}}),t.ui.plugin.add("draggable","scroll",{start:function(t,e,i){i.scrollParentNotHidden||(i.scrollParentNotHidden=i.helper.scrollParent(!1)),i.scrollParentNotHidden[0]!==i.document[0]&&"HTML"!==i.scrollParentNotHidden[0].tagName&&(i.overflowOffset=i.scrollParentNotHidden.offset())},drag:function(e,i,s){var n=s.options,o=!1,a=s.scrollParentNotHidden[0],r=s.document[0];a!==r&&"HTML"!==a.tagName?(n.axis&&"x"===n.axis||(s.overflowOffset.top+a.offsetHeight-e.pageY=0;d--)h=s.snapElements[d].left-s.margins.left,l=h+s.snapElements[d].width,c=s.snapElements[d].top-s.margins.top,u=c+s.snapElements[d].height,h-g>_||m>l+g||c-g>b||v>u+g||!t.contains(s.snapElements[d].item.ownerDocument,s.snapElements[d].item)?(s.snapElements[d].snapping&&s.options.snap.release&&s.options.snap.release.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=!1):("inner"!==f.snapMode&&(n=g>=Math.abs(c-b),o=g>=Math.abs(u-v),a=g>=Math.abs(h-_),r=g>=Math.abs(l-m),n&&(i.position.top=s._convertPositionTo("relative",{top:c-s.helperProportions.height,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h-s.helperProportions.width}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l}).left)),p=n||o||a||r,"outer"!==f.snapMode&&(n=g>=Math.abs(c-v),o=g>=Math.abs(u-b),a=g>=Math.abs(h-m),r=g>=Math.abs(l-_),n&&(i.position.top=s._convertPositionTo("relative",{top:c,left:0}).top),o&&(i.position.top=s._convertPositionTo("relative",{top:u-s.helperProportions.height,left:0}).top),a&&(i.position.left=s._convertPositionTo("relative",{top:0,left:h}).left),r&&(i.position.left=s._convertPositionTo("relative",{top:0,left:l-s.helperProportions.width}).left)),!s.snapElements[d].snapping&&(n||o||a||r||p)&&s.options.snap.snap&&s.options.snap.snap.call(s.element,e,t.extend(s._uiHash(),{snapItem:s.snapElements[d].item})),s.snapElements[d].snapping=n||o||a||r||p)}}),t.ui.plugin.add("draggable","stack",{start:function(e,i,s){var n,o=s.options,a=t.makeArray(t(o.stack)).sort(function(e,i){return(parseInt(t(e).css("zIndex"),10)||0)-(parseInt(t(i).css("zIndex"),10)||0)});a.length&&(n=parseInt(t(a[0]).css("zIndex"),10)||0,t(a).each(function(e){t(this).css("zIndex",n+e)}),this.css("zIndex",n+a.length))}}),t.ui.plugin.add("draggable","zIndex",{start:function(e,i,s){var n=t(i.helper),o=s.options;n.css("zIndex")&&(o._zIndex=n.css("zIndex")),n.css("zIndex",o.zIndex)},stop:function(e,i,s){var n=s.options;n._zIndex&&t(i.helper).css("zIndex",n._zIndex)}}),t.ui.draggable,t.widget("ui.resizable",t.ui.mouse,{version:"1.12.1",widgetEventPrefix:"resize",options:{alsoResize:!1,animate:!1,animateDuration:"slow",animateEasing:"swing",aspectRatio:!1,autoHide:!1,classes:{"ui-resizable-se":"ui-icon ui-icon-gripsmall-diagonal-se"},containment:!1,ghost:!1,grid:!1,handles:"e,s,se",helper:!1,maxHeight:null,maxWidth:null,minHeight:10,minWidth:10,zIndex:90,resize:null,start:null,stop:null},_num:function(t){return parseFloat(t)||0},_isNumber:function(t){return!isNaN(parseFloat(t))},_hasScroll:function(e,i){if("hidden"===t(e).css("overflow"))return!1;var s=i&&"left"===i?"scrollLeft":"scrollTop",n=!1;return e[s]>0?!0:(e[s]=1,n=e[s]>0,e[s]=0,n)},_create:function(){var e,i=this.options,s=this;this._addClass("ui-resizable"),t.extend(this,{_aspectRatio:!!i.aspectRatio,aspectRatio:i.aspectRatio,originalElement:this.element,_proportionallyResizeElements:[],_helper:i.helper||i.ghost||i.animate?i.helper||"ui-resizable-helper":null}),this.element[0].nodeName.match(/^(canvas|textarea|input|select|button|img)$/i)&&(this.element.wrap(t("
    ").css({position:this.element.css("position"),width:this.element.outerWidth(),height:this.element.outerHeight(),top:this.element.css("top"),left:this.element.css("left")})),this.element=this.element.parent().data("ui-resizable",this.element.resizable("instance")),this.elementIsWrapper=!0,e={marginTop:this.originalElement.css("marginTop"),marginRight:this.originalElement.css("marginRight"),marginBottom:this.originalElement.css("marginBottom"),marginLeft:this.originalElement.css("marginLeft")},this.element.css(e),this.originalElement.css("margin",0),this.originalResizeStyle=this.originalElement.css("resize"),this.originalElement.css("resize","none"),this._proportionallyResizeElements.push(this.originalElement.css({position:"static",zoom:1,display:"block"})),this.originalElement.css(e),this._proportionallyResize()),this._setupHandles(),i.autoHide&&t(this.element).on("mouseenter",function(){i.disabled||(s._removeClass("ui-resizable-autohide"),s._handles.show())}).on("mouseleave",function(){i.disabled||s.resizing||(s._addClass("ui-resizable-autohide"),s._handles.hide())}),this._mouseInit()},_destroy:function(){this._mouseDestroy();var e,i=function(e){t(e).removeData("resizable").removeData("ui-resizable").off(".resizable").find(".ui-resizable-handle").remove()};return this.elementIsWrapper&&(i(this.element),e=this.element,this.originalElement.css({position:e.css("position"),width:e.outerWidth(),height:e.outerHeight(),top:e.css("top"),left:e.css("left")}).insertAfter(e),e.remove()),this.originalElement.css("resize",this.originalResizeStyle),i(this.originalElement),this},_setOption:function(t,e){switch(this._super(t,e),t){case"handles":this._removeHandles(),this._setupHandles();break;default:}},_setupHandles:function(){var e,i,s,n,o,a=this.options,r=this;if(this.handles=a.handles||(t(".ui-resizable-handle",this.element).length?{n:".ui-resizable-n",e:".ui-resizable-e",s:".ui-resizable-s",w:".ui-resizable-w",se:".ui-resizable-se",sw:".ui-resizable-sw",ne:".ui-resizable-ne",nw:".ui-resizable-nw"}:"e,s,se"),this._handles=t(),this.handles.constructor===String)for("all"===this.handles&&(this.handles="n,e,s,w,se,sw,ne,nw"),s=this.handles.split(","),this.handles={},i=0;s.length>i;i++)e=t.trim(s[i]),n="ui-resizable-"+e,o=t("
    "),this._addClass(o,"ui-resizable-handle "+n),o.css({zIndex:a.zIndex}),this.handles[e]=".ui-resizable-"+e,this.element.append(o);this._renderAxis=function(e){var i,s,n,o;e=e||this.element;for(i in this.handles)this.handles[i].constructor===String?this.handles[i]=this.element.children(this.handles[i]).first().show():(this.handles[i].jquery||this.handles[i].nodeType)&&(this.handles[i]=t(this.handles[i]),this._on(this.handles[i],{mousedown:r._mouseDown})),this.elementIsWrapper&&this.originalElement[0].nodeName.match(/^(textarea|input|select|button)$/i)&&(s=t(this.handles[i],this.element),o=/sw|ne|nw|se|n|s/.test(i)?s.outerHeight():s.outerWidth(),n=["padding",/ne|nw|n/.test(i)?"Top":/se|sw|s/.test(i)?"Bottom":/^e$/.test(i)?"Right":"Left"].join(""),e.css(n,o),this._proportionallyResize()),this._handles=this._handles.add(this.handles[i])},this._renderAxis(this.element),this._handles=this._handles.add(this.element.find(".ui-resizable-handle")),this._handles.disableSelection(),this._handles.on("mouseover",function(){r.resizing||(this.className&&(o=this.className.match(/ui-resizable-(se|sw|ne|nw|n|e|s|w)/i)),r.axis=o&&o[1]?o[1]:"se")}),a.autoHide&&(this._handles.hide(),this._addClass("ui-resizable-autohide"))},_removeHandles:function(){this._handles.remove()},_mouseCapture:function(e){var i,s,n=!1;for(i in this.handles)s=t(this.handles[i])[0],(s===e.target||t.contains(s,e.target))&&(n=!0);return!this.options.disabled&&n},_mouseStart:function(e){var i,s,n,o=this.options,a=this.element;return this.resizing=!0,this._renderProxy(),i=this._num(this.helper.css("left")),s=this._num(this.helper.css("top")),o.containment&&(i+=t(o.containment).scrollLeft()||0,s+=t(o.containment).scrollTop()||0),this.offset=this.helper.offset(),this.position={left:i,top:s},this.size=this._helper?{width:this.helper.width(),height:this.helper.height()}:{width:a.width(),height:a.height()},this.originalSize=this._helper?{width:a.outerWidth(),height:a.outerHeight()}:{width:a.width(),height:a.height()},this.sizeDiff={width:a.outerWidth()-a.width(),height:a.outerHeight()-a.height()},this.originalPosition={left:i,top:s},this.originalMousePosition={left:e.pageX,top:e.pageY},this.aspectRatio="number"==typeof o.aspectRatio?o.aspectRatio:this.originalSize.width/this.originalSize.height||1,n=t(".ui-resizable-"+this.axis).css("cursor"),t("body").css("cursor","auto"===n?this.axis+"-resize":n),this._addClass("ui-resizable-resizing"),this._propagate("start",e),!0},_mouseDrag:function(e){var i,s,n=this.originalMousePosition,o=this.axis,a=e.pageX-n.left||0,r=e.pageY-n.top||0,h=this._change[o];return this._updatePrevProperties(),h?(i=h.apply(this,[e,a,r]),this._updateVirtualBoundaries(e.shiftKey),(this._aspectRatio||e.shiftKey)&&(i=this._updateRatio(i,e)),i=this._respectSize(i,e),this._updateCache(i),this._propagate("resize",e),s=this._applyChanges(),!this._helper&&this._proportionallyResizeElements.length&&this._proportionallyResize(),t.isEmptyObject(s)||(this._updatePrevProperties(),this._trigger("resize",e,this.ui()),this._applyChanges()),!1):!1},_mouseStop:function(e){this.resizing=!1;var i,s,n,o,a,r,h,l=this.options,c=this;return this._helper&&(i=this._proportionallyResizeElements,s=i.length&&/textarea/i.test(i[0].nodeName),n=s&&this._hasScroll(i[0],"left")?0:c.sizeDiff.height,o=s?0:c.sizeDiff.width,a={width:c.helper.width()-o,height:c.helper.height()-n},r=parseFloat(c.element.css("left"))+(c.position.left-c.originalPosition.left)||null,h=parseFloat(c.element.css("top"))+(c.position.top-c.originalPosition.top)||null,l.animate||this.element.css(t.extend(a,{top:h,left:r})),c.helper.height(c.size.height),c.helper.width(c.size.width),this._helper&&!l.animate&&this._proportionallyResize()),t("body").css("cursor","auto"),this._removeClass("ui-resizable-resizing"),this._propagate("stop",e),this._helper&&this.helper.remove(),!1},_updatePrevProperties:function(){this.prevPosition={top:this.position.top,left:this.position.left},this.prevSize={width:this.size.width,height:this.size.height}},_applyChanges:function(){var t={};return this.position.top!==this.prevPosition.top&&(t.top=this.position.top+"px"),this.position.left!==this.prevPosition.left&&(t.left=this.position.left+"px"),this.size.width!==this.prevSize.width&&(t.width=this.size.width+"px"),this.size.height!==this.prevSize.height&&(t.height=this.size.height+"px"),this.helper.css(t),t},_updateVirtualBoundaries:function(t){var e,i,s,n,o,a=this.options;o={minWidth:this._isNumber(a.minWidth)?a.minWidth:0,maxWidth:this._isNumber(a.maxWidth)?a.maxWidth:1/0,minHeight:this._isNumber(a.minHeight)?a.minHeight:0,maxHeight:this._isNumber(a.maxHeight)?a.maxHeight:1/0},(this._aspectRatio||t)&&(e=o.minHeight*this.aspectRatio,s=o.minWidth/this.aspectRatio,i=o.maxHeight*this.aspectRatio,n=o.maxWidth/this.aspectRatio,e>o.minWidth&&(o.minWidth=e),s>o.minHeight&&(o.minHeight=s),o.maxWidth>i&&(o.maxWidth=i),o.maxHeight>n&&(o.maxHeight=n)),this._vBoundaries=o},_updateCache:function(t){this.offset=this.helper.offset(),this._isNumber(t.left)&&(this.position.left=t.left),this._isNumber(t.top)&&(this.position.top=t.top),this._isNumber(t.height)&&(this.size.height=t.height),this._isNumber(t.width)&&(this.size.width=t.width)},_updateRatio:function(t){var e=this.position,i=this.size,s=this.axis;return this._isNumber(t.height)?t.width=t.height*this.aspectRatio:this._isNumber(t.width)&&(t.height=t.width/this.aspectRatio),"sw"===s&&(t.left=e.left+(i.width-t.width),t.top=null),"nw"===s&&(t.top=e.top+(i.height-t.height),t.left=e.left+(i.width-t.width)),t},_respectSize:function(t){var e=this._vBoundaries,i=this.axis,s=this._isNumber(t.width)&&e.maxWidth&&e.maxWidtht.width,a=this._isNumber(t.height)&&e.minHeight&&e.minHeight>t.height,r=this.originalPosition.left+this.originalSize.width,h=this.originalPosition.top+this.originalSize.height,l=/sw|nw|w/.test(i),c=/nw|ne|n/.test(i);return o&&(t.width=e.minWidth),a&&(t.height=e.minHeight),s&&(t.width=e.maxWidth),n&&(t.height=e.maxHeight),o&&l&&(t.left=r-e.minWidth),s&&l&&(t.left=r-e.maxWidth),a&&c&&(t.top=h-e.minHeight),n&&c&&(t.top=h-e.maxHeight),t.width||t.height||t.left||!t.top?t.width||t.height||t.top||!t.left||(t.left=null):t.top=null,t},_getPaddingPlusBorderDimensions:function(t){for(var e=0,i=[],s=[t.css("borderTopWidth"),t.css("borderRightWidth"),t.css("borderBottomWidth"),t.css("borderLeftWidth")],n=[t.css("paddingTop"),t.css("paddingRight"),t.css("paddingBottom"),t.css("paddingLeft")];4>e;e++)i[e]=parseFloat(s[e])||0,i[e]+=parseFloat(n[e])||0;return{height:i[0]+i[2],width:i[1]+i[3]}},_proportionallyResize:function(){if(this._proportionallyResizeElements.length)for(var t,e=0,i=this.helper||this.element;this._proportionallyResizeElements.length>e;e++)t=this._proportionallyResizeElements[e],this.outerDimensions||(this.outerDimensions=this._getPaddingPlusBorderDimensions(t)),t.css({height:i.height()-this.outerDimensions.height||0,width:i.width()-this.outerDimensions.width||0})},_renderProxy:function(){var e=this.element,i=this.options;this.elementOffset=e.offset(),this._helper?(this.helper=this.helper||t("
    "),this._addClass(this.helper,this._helper),this.helper.css({width:this.element.outerWidth(),height:this.element.outerHeight(),position:"absolute",left:this.elementOffset.left+"px",top:this.elementOffset.top+"px",zIndex:++i.zIndex}),this.helper.appendTo("body").disableSelection()):this.helper=this.element},_change:{e:function(t,e){return{width:this.originalSize.width+e}},w:function(t,e){var i=this.originalSize,s=this.originalPosition;return{left:s.left+e,width:i.width-e}},n:function(t,e,i){var s=this.originalSize,n=this.originalPosition;return{top:n.top+i,height:s.height-i}},s:function(t,e,i){return{height:this.originalSize.height+i}},se:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},sw:function(e,i,s){return t.extend(this._change.s.apply(this,arguments),this._change.w.apply(this,[e,i,s]))},ne:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.e.apply(this,[e,i,s]))},nw:function(e,i,s){return t.extend(this._change.n.apply(this,arguments),this._change.w.apply(this,[e,i,s]))}},_propagate:function(e,i){t.ui.plugin.call(this,e,[i,this.ui()]),"resize"!==e&&this._trigger(e,i,this.ui())},plugins:{},ui:function(){return{originalElement:this.originalElement,element:this.element,helper:this.helper,position:this.position,size:this.size,originalSize:this.originalSize,originalPosition:this.originalPosition}}}),t.ui.plugin.add("resizable","animate",{stop:function(e){var i=t(this).resizable("instance"),s=i.options,n=i._proportionallyResizeElements,o=n.length&&/textarea/i.test(n[0].nodeName),a=o&&i._hasScroll(n[0],"left")?0:i.sizeDiff.height,r=o?0:i.sizeDiff.width,h={width:i.size.width-r,height:i.size.height-a},l=parseFloat(i.element.css("left"))+(i.position.left-i.originalPosition.left)||null,c=parseFloat(i.element.css("top"))+(i.position.top-i.originalPosition.top)||null;i.element.animate(t.extend(h,c&&l?{top:c,left:l}:{}),{duration:s.animateDuration,easing:s.animateEasing,step:function(){var s={width:parseFloat(i.element.css("width")),height:parseFloat(i.element.css("height")),top:parseFloat(i.element.css("top")),left:parseFloat(i.element.css("left"))};n&&n.length&&t(n[0]).css({width:s.width,height:s.height}),i._updateCache(s),i._propagate("resize",e)}})}}),t.ui.plugin.add("resizable","containment",{start:function(){var e,i,s,n,o,a,r,h=t(this).resizable("instance"),l=h.options,c=h.element,u=l.containment,d=u instanceof t?u.get(0):/parent/.test(u)?c.parent().get(0):u;d&&(h.containerElement=t(d),/document/.test(u)||u===document?(h.containerOffset={left:0,top:0},h.containerPosition={left:0,top:0},h.parentData={element:t(document),left:0,top:0,width:t(document).width(),height:t(document).height()||document.body.parentNode.scrollHeight}):(e=t(d),i=[],t(["Top","Right","Left","Bottom"]).each(function(t,s){i[t]=h._num(e.css("padding"+s))}),h.containerOffset=e.offset(),h.containerPosition=e.position(),h.containerSize={height:e.innerHeight()-i[3],width:e.innerWidth()-i[1]},s=h.containerOffset,n=h.containerSize.height,o=h.containerSize.width,a=h._hasScroll(d,"left")?d.scrollWidth:o,r=h._hasScroll(d)?d.scrollHeight:n,h.parentData={element:d,left:s.left,top:s.top,width:a,height:r}))},resize:function(e){var i,s,n,o,a=t(this).resizable("instance"),r=a.options,h=a.containerOffset,l=a.position,c=a._aspectRatio||e.shiftKey,u={top:0,left:0},d=a.containerElement,p=!0;d[0]!==document&&/static/.test(d.css("position"))&&(u=h),l.left<(a._helper?h.left:0)&&(a.size.width=a.size.width+(a._helper?a.position.left-h.left:a.position.left-u.left),c&&(a.size.height=a.size.width/a.aspectRatio,p=!1),a.position.left=r.helper?h.left:0),l.top<(a._helper?h.top:0)&&(a.size.height=a.size.height+(a._helper?a.position.top-h.top:a.position.top),c&&(a.size.width=a.size.height*a.aspectRatio,p=!1),a.position.top=a._helper?h.top:0),n=a.containerElement.get(0)===a.element.parent().get(0),o=/relative|absolute/.test(a.containerElement.css("position")),n&&o?(a.offset.left=a.parentData.left+a.position.left,a.offset.top=a.parentData.top+a.position.top):(a.offset.left=a.element.offset().left,a.offset.top=a.element.offset().top),i=Math.abs(a.sizeDiff.width+(a._helper?a.offset.left-u.left:a.offset.left-h.left)),s=Math.abs(a.sizeDiff.height+(a._helper?a.offset.top-u.top:a.offset.top-h.top)),i+a.size.width>=a.parentData.width&&(a.size.width=a.parentData.width-i,c&&(a.size.height=a.size.width/a.aspectRatio,p=!1)),s+a.size.height>=a.parentData.height&&(a.size.height=a.parentData.height-s,c&&(a.size.width=a.size.height*a.aspectRatio,p=!1)),p||(a.position.left=a.prevPosition.left,a.position.top=a.prevPosition.top,a.size.width=a.prevSize.width,a.size.height=a.prevSize.height)},stop:function(){var e=t(this).resizable("instance"),i=e.options,s=e.containerOffset,n=e.containerPosition,o=e.containerElement,a=t(e.helper),r=a.offset(),h=a.outerWidth()-e.sizeDiff.width,l=a.outerHeight()-e.sizeDiff.height;e._helper&&!i.animate&&/relative/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l}),e._helper&&!i.animate&&/static/.test(o.css("position"))&&t(this).css({left:r.left-n.left-s.left,width:h,height:l})}}),t.ui.plugin.add("resizable","alsoResize",{start:function(){var e=t(this).resizable("instance"),i=e.options;t(i.alsoResize).each(function(){var e=t(this);e.data("ui-resizable-alsoresize",{width:parseFloat(e.width()),height:parseFloat(e.height()),left:parseFloat(e.css("left")),top:parseFloat(e.css("top"))})})},resize:function(e,i){var s=t(this).resizable("instance"),n=s.options,o=s.originalSize,a=s.originalPosition,r={height:s.size.height-o.height||0,width:s.size.width-o.width||0,top:s.position.top-a.top||0,left:s.position.left-a.left||0};t(n.alsoResize).each(function(){var e=t(this),s=t(this).data("ui-resizable-alsoresize"),n={},o=e.parents(i.originalElement[0]).length?["width","height"]:["width","height","top","left"];t.each(o,function(t,e){var i=(s[e]||0)+(r[e]||0);i&&i>=0&&(n[e]=i||null)}),e.css(n)})},stop:function(){t(this).removeData("ui-resizable-alsoresize")}}),t.ui.plugin.add("resizable","ghost",{start:function(){var e=t(this).resizable("instance"),i=e.size;e.ghost=e.originalElement.clone(),e.ghost.css({opacity:.25,display:"block",position:"relative",height:i.height,width:i.width,margin:0,left:0,top:0}),e._addClass(e.ghost,"ui-resizable-ghost"),t.uiBackCompat!==!1&&"string"==typeof e.options.ghost&&e.ghost.addClass(this.options.ghost),e.ghost.appendTo(e.helper)},resize:function(){var e=t(this).resizable("instance");e.ghost&&e.ghost.css({position:"relative",height:e.size.height,width:e.size.width})},stop:function(){var e=t(this).resizable("instance");e.ghost&&e.helper&&e.helper.get(0).removeChild(e.ghost.get(0))}}),t.ui.plugin.add("resizable","grid",{resize:function(){var e,i=t(this).resizable("instance"),s=i.options,n=i.size,o=i.originalSize,a=i.originalPosition,r=i.axis,h="number"==typeof s.grid?[s.grid,s.grid]:s.grid,l=h[0]||1,c=h[1]||1,u=Math.round((n.width-o.width)/l)*l,d=Math.round((n.height-o.height)/c)*c,p=o.width+u,f=o.height+d,g=s.maxWidth&&p>s.maxWidth,m=s.maxHeight&&f>s.maxHeight,_=s.minWidth&&s.minWidth>p,v=s.minHeight&&s.minHeight>f;s.grid=h,_&&(p+=l),v&&(f+=c),g&&(p-=l),m&&(f-=c),/^(se|s|e)$/.test(r)?(i.size.width=p,i.size.height=f):/^(ne)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.top=a.top-d):/^(sw)$/.test(r)?(i.size.width=p,i.size.height=f,i.position.left=a.left-u):((0>=f-c||0>=p-l)&&(e=i._getPaddingPlusBorderDimensions(this)),f-c>0?(i.size.height=f,i.position.top=a.top-d):(f=c-e.height,i.size.height=f,i.position.top=a.top+o.height-f),p-l>0?(i.size.width=p,i.position.left=a.left-u):(p=l-e.width,i.size.width=p,i.position.left=a.left+o.width-p))}}),t.ui.resizable,t.widget("ui.dialog",{version:"1.12.1",options:{appendTo:"body",autoOpen:!0,buttons:[],classes:{"ui-dialog":"ui-corner-all","ui-dialog-titlebar":"ui-corner-all"},closeOnEscape:!0,closeText:"Close",draggable:!0,hide:null,height:"auto",maxHeight:null,maxWidth:null,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(e){var i=t(this).css(e).offset().top;0>i&&t(this).css("top",e.top-i)}},resizable:!0,show:null,title:null,width:300,beforeClose:null,close:null,drag:null,dragStart:null,dragStop:null,focus:null,open:null,resize:null,resizeStart:null,resizeStop:null},sizeRelatedOptions:{buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},resizableRelatedOptions:{maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0},_create:function(){this.originalCss={display:this.element[0].style.display,width:this.element[0].style.width,minHeight:this.element[0].style.minHeight,maxHeight:this.element[0].style.maxHeight,height:this.element[0].style.height},this.originalPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.originalTitle=this.element.attr("title"),null==this.options.title&&null!=this.originalTitle&&(this.options.title=this.originalTitle),this.options.disabled&&(this.options.disabled=!1),this._createWrapper(),this.element.show().removeAttr("title").appendTo(this.uiDialog),this._addClass("ui-dialog-content","ui-widget-content"),this._createTitlebar(),this._createButtonPane(),this.options.draggable&&t.fn.draggable&&this._makeDraggable(),this.options.resizable&&t.fn.resizable&&this._makeResizable(),this._isOpen=!1,this._trackFocus()},_init:function(){this.options.autoOpen&&this.open()},_appendTo:function(){var e=this.options.appendTo;return e&&(e.jquery||e.nodeType)?t(e):this.document.find(e||"body").eq(0)},_destroy:function(){var t,e=this.originalPosition;this._untrackInstance(),this._destroyOverlay(),this.element.removeUniqueId().css(this.originalCss).detach(),this.uiDialog.remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),t=e.parent.children().eq(e.index),t.length&&t[0]!==this.element[0]?t.before(this.element):e.parent.append(this.element)},widget:function(){return this.uiDialog +},disable:t.noop,enable:t.noop,close:function(e){var i=this;this._isOpen&&this._trigger("beforeClose",e)!==!1&&(this._isOpen=!1,this._focusedElement=null,this._destroyOverlay(),this._untrackInstance(),this.opener.filter(":focusable").trigger("focus").length||t.ui.safeBlur(t.ui.safeActiveElement(this.document[0])),this._hide(this.uiDialog,this.options.hide,function(){i._trigger("close",e)}))},isOpen:function(){return this._isOpen},moveToTop:function(){this._moveToTop()},_moveToTop:function(e,i){var s=!1,n=this.uiDialog.siblings(".ui-front:visible").map(function(){return+t(this).css("z-index")}).get(),o=Math.max.apply(null,n);return o>=+this.uiDialog.css("z-index")&&(this.uiDialog.css("z-index",o+1),s=!0),s&&!i&&this._trigger("focus",e),s},open:function(){var e=this;return this._isOpen?(this._moveToTop()&&this._focusTabbable(),void 0):(this._isOpen=!0,this.opener=t(t.ui.safeActiveElement(this.document[0])),this._size(),this._position(),this._createOverlay(),this._moveToTop(null,!0),this.overlay&&this.overlay.css("z-index",this.uiDialog.css("z-index")-1),this._show(this.uiDialog,this.options.show,function(){e._focusTabbable(),e._trigger("focus")}),this._makeFocusTarget(),this._trigger("open"),void 0)},_focusTabbable:function(){var t=this._focusedElement;t||(t=this.element.find("[autofocus]")),t.length||(t=this.element.find(":tabbable")),t.length||(t=this.uiDialogButtonPane.find(":tabbable")),t.length||(t=this.uiDialogTitlebarClose.filter(":tabbable")),t.length||(t=this.uiDialog),t.eq(0).trigger("focus")},_keepFocus:function(e){function i(){var e=t.ui.safeActiveElement(this.document[0]),i=this.uiDialog[0]===e||t.contains(this.uiDialog[0],e);i||this._focusTabbable()}e.preventDefault(),i.call(this),this._delay(i)},_createWrapper:function(){this.uiDialog=t("
    ").hide().attr({tabIndex:-1,role:"dialog"}).appendTo(this._appendTo()),this._addClass(this.uiDialog,"ui-dialog","ui-widget ui-widget-content ui-front"),this._on(this.uiDialog,{keydown:function(e){if(this.options.closeOnEscape&&!e.isDefaultPrevented()&&e.keyCode&&e.keyCode===t.ui.keyCode.ESCAPE)return e.preventDefault(),this.close(e),void 0;if(e.keyCode===t.ui.keyCode.TAB&&!e.isDefaultPrevented()){var i=this.uiDialog.find(":tabbable"),s=i.filter(":first"),n=i.filter(":last");e.target!==n[0]&&e.target!==this.uiDialog[0]||e.shiftKey?e.target!==s[0]&&e.target!==this.uiDialog[0]||!e.shiftKey||(this._delay(function(){n.trigger("focus")}),e.preventDefault()):(this._delay(function(){s.trigger("focus")}),e.preventDefault())}},mousedown:function(t){this._moveToTop(t)&&this._focusTabbable()}}),this.element.find("[aria-describedby]").length||this.uiDialog.attr({"aria-describedby":this.element.uniqueId().attr("id")})},_createTitlebar:function(){var e;this.uiDialogTitlebar=t("
    "),this._addClass(this.uiDialogTitlebar,"ui-dialog-titlebar","ui-widget-header ui-helper-clearfix"),this._on(this.uiDialogTitlebar,{mousedown:function(e){t(e.target).closest(".ui-dialog-titlebar-close")||this.uiDialog.trigger("focus")}}),this.uiDialogTitlebarClose=t("").button({label:t("").text(this.options.closeText).html(),icon:"ui-icon-closethick",showLabel:!1}).appendTo(this.uiDialogTitlebar),this._addClass(this.uiDialogTitlebarClose,"ui-dialog-titlebar-close"),this._on(this.uiDialogTitlebarClose,{click:function(t){t.preventDefault(),this.close(t)}}),e=t("").uniqueId().prependTo(this.uiDialogTitlebar),this._addClass(e,"ui-dialog-title"),this._title(e),this.uiDialogTitlebar.prependTo(this.uiDialog),this.uiDialog.attr({"aria-labelledby":e.attr("id")})},_title:function(t){this.options.title?t.text(this.options.title):t.html(" ")},_createButtonPane:function(){this.uiDialogButtonPane=t("
    "),this._addClass(this.uiDialogButtonPane,"ui-dialog-buttonpane","ui-widget-content ui-helper-clearfix"),this.uiButtonSet=t("
    ").appendTo(this.uiDialogButtonPane),this._addClass(this.uiButtonSet,"ui-dialog-buttonset"),this._createButtons()},_createButtons:function(){var e=this,i=this.options.buttons;return this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),t.isEmptyObject(i)||t.isArray(i)&&!i.length?(this._removeClass(this.uiDialog,"ui-dialog-buttons"),void 0):(t.each(i,function(i,s){var n,o;s=t.isFunction(s)?{click:s,text:i}:s,s=t.extend({type:"button"},s),n=s.click,o={icon:s.icon,iconPosition:s.iconPosition,showLabel:s.showLabel,icons:s.icons,text:s.text},delete s.click,delete s.icon,delete s.iconPosition,delete s.showLabel,delete s.icons,"boolean"==typeof s.text&&delete s.text,t("",s).button(o).appendTo(e.uiButtonSet).on("click",function(){n.apply(e.element[0],arguments)})}),this._addClass(this.uiDialog,"ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog),void 0)},_makeDraggable:function(){function e(t){return{position:t.position,offset:t.offset}}var i=this,s=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(s,n){i._addClass(t(this),"ui-dialog-dragging"),i._blockFrames(),i._trigger("dragStart",s,e(n))},drag:function(t,s){i._trigger("drag",t,e(s))},stop:function(n,o){var a=o.offset.left-i.document.scrollLeft(),r=o.offset.top-i.document.scrollTop();s.position={my:"left top",at:"left"+(a>=0?"+":"")+a+" "+"top"+(r>=0?"+":"")+r,of:i.window},i._removeClass(t(this),"ui-dialog-dragging"),i._unblockFrames(),i._trigger("dragStop",n,e(o))}})},_makeResizable:function(){function e(t){return{originalPosition:t.originalPosition,originalSize:t.originalSize,position:t.position,size:t.size}}var i=this,s=this.options,n=s.resizable,o=this.uiDialog.css("position"),a="string"==typeof n?n:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:s.maxWidth,maxHeight:s.maxHeight,minWidth:s.minWidth,minHeight:this._minHeight(),handles:a,start:function(s,n){i._addClass(t(this),"ui-dialog-resizing"),i._blockFrames(),i._trigger("resizeStart",s,e(n))},resize:function(t,s){i._trigger("resize",t,e(s))},stop:function(n,o){var a=i.uiDialog.offset(),r=a.left-i.document.scrollLeft(),h=a.top-i.document.scrollTop();s.height=i.uiDialog.height(),s.width=i.uiDialog.width(),s.position={my:"left top",at:"left"+(r>=0?"+":"")+r+" "+"top"+(h>=0?"+":"")+h,of:i.window},i._removeClass(t(this),"ui-dialog-resizing"),i._unblockFrames(),i._trigger("resizeStop",n,e(o))}}).css("position",o)},_trackFocus:function(){this._on(this.widget(),{focusin:function(e){this._makeFocusTarget(),this._focusedElement=t(e.target)}})},_makeFocusTarget:function(){this._untrackInstance(),this._trackingInstances().unshift(this)},_untrackInstance:function(){var e=this._trackingInstances(),i=t.inArray(this,e);-1!==i&&e.splice(i,1)},_trackingInstances:function(){var t=this.document.data("ui-dialog-instances");return t||(t=[],this.document.data("ui-dialog-instances",t)),t},_minHeight:function(){var t=this.options;return"auto"===t.height?t.minHeight:Math.min(t.minHeight,t.height)},_position:function(){var t=this.uiDialog.is(":visible");t||this.uiDialog.show(),this.uiDialog.position(this.options.position),t||this.uiDialog.hide()},_setOptions:function(e){var i=this,s=!1,n={};t.each(e,function(t,e){i._setOption(t,e),t in i.sizeRelatedOptions&&(s=!0),t in i.resizableRelatedOptions&&(n[t]=e)}),s&&(this._size(),this._position()),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option",n)},_setOption:function(e,i){var s,n,o=this.uiDialog;"disabled"!==e&&(this._super(e,i),"appendTo"===e&&this.uiDialog.appendTo(this._appendTo()),"buttons"===e&&this._createButtons(),"closeText"===e&&this.uiDialogTitlebarClose.button({label:t("").text(""+this.options.closeText).html()}),"draggable"===e&&(s=o.is(":data(ui-draggable)"),s&&!i&&o.draggable("destroy"),!s&&i&&this._makeDraggable()),"position"===e&&this._position(),"resizable"===e&&(n=o.is(":data(ui-resizable)"),n&&!i&&o.resizable("destroy"),n&&"string"==typeof i&&o.resizable("option","handles",i),n||i===!1||this._makeResizable()),"title"===e&&this._title(this.uiDialogTitlebar.find(".ui-dialog-title")))},_size:function(){var t,e,i,s=this.options;this.element.show().css({width:"auto",minHeight:0,maxHeight:"none",height:0}),s.minWidth>s.width&&(s.width=s.minWidth),t=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),e=Math.max(0,s.minHeight-t),i="number"==typeof s.maxHeight?Math.max(0,s.maxHeight-t):"none","auto"===s.height?this.element.css({minHeight:e,maxHeight:i,height:"auto"}):this.element.height(Math.max(0,s.height-t)),this.uiDialog.is(":data(ui-resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())},_blockFrames:function(){this.iframeBlocks=this.document.find("iframe").map(function(){var e=t(this);return t("
    ").css({position:"absolute",width:e.outerWidth(),height:e.outerHeight()}).appendTo(e.parent()).offset(e.offset())[0]})},_unblockFrames:function(){this.iframeBlocks&&(this.iframeBlocks.remove(),delete this.iframeBlocks)},_allowInteraction:function(e){return t(e.target).closest(".ui-dialog").length?!0:!!t(e.target).closest(".ui-datepicker").length},_createOverlay:function(){if(this.options.modal){var e=!0;this._delay(function(){e=!1}),this.document.data("ui-dialog-overlays")||this._on(this.document,{focusin:function(t){e||this._allowInteraction(t)||(t.preventDefault(),this._trackingInstances()[0]._focusTabbable())}}),this.overlay=t("
    ").appendTo(this._appendTo()),this._addClass(this.overlay,null,"ui-widget-overlay ui-front"),this._on(this.overlay,{mousedown:"_keepFocus"}),this.document.data("ui-dialog-overlays",(this.document.data("ui-dialog-overlays")||0)+1)}},_destroyOverlay:function(){if(this.options.modal&&this.overlay){var t=this.document.data("ui-dialog-overlays")-1;t?this.document.data("ui-dialog-overlays",t):(this._off(this.document,"focusin"),this.document.removeData("ui-dialog-overlays")),this.overlay.remove(),this.overlay=null}}}),t.uiBackCompat!==!1&&t.widget("ui.dialog",t.ui.dialog,{options:{dialogClass:""},_createWrapper:function(){this._super(),this.uiDialog.addClass(this.options.dialogClass)},_setOption:function(t,e){"dialogClass"===t&&this.uiDialog.removeClass(this.options.dialogClass).addClass(e),this._superApply(arguments)}}),t.ui.dialog,t.widget("ui.droppable",{version:"1.12.1",widgetEventPrefix:"drop",options:{accept:"*",addClasses:!0,greedy:!1,scope:"default",tolerance:"intersect",activate:null,deactivate:null,drop:null,out:null,over:null},_create:function(){var e,i=this.options,s=i.accept;this.isover=!1,this.isout=!0,this.accept=t.isFunction(s)?s:function(t){return t.is(s)},this.proportions=function(){return arguments.length?(e=arguments[0],void 0):e?e:e={width:this.element[0].offsetWidth,height:this.element[0].offsetHeight}},this._addToManager(i.scope),i.addClasses&&this._addClass("ui-droppable")},_addToManager:function(e){t.ui.ddmanager.droppables[e]=t.ui.ddmanager.droppables[e]||[],t.ui.ddmanager.droppables[e].push(this)},_splice:function(t){for(var e=0;t.length>e;e++)t[e]===this&&t.splice(e,1)},_destroy:function(){var e=t.ui.ddmanager.droppables[this.options.scope];this._splice(e)},_setOption:function(e,i){if("accept"===e)this.accept=t.isFunction(i)?i:function(t){return t.is(i)};else if("scope"===e){var s=t.ui.ddmanager.droppables[this.options.scope];this._splice(s),this._addToManager(i)}this._super(e,i)},_activate:function(e){var i=t.ui.ddmanager.current;this._addActiveClass(),i&&this._trigger("activate",e,this.ui(i))},_deactivate:function(e){var i=t.ui.ddmanager.current;this._removeActiveClass(),i&&this._trigger("deactivate",e,this.ui(i))},_over:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._addHoverClass(),this._trigger("over",e,this.ui(i)))},_out:function(e){var i=t.ui.ddmanager.current;i&&(i.currentItem||i.element)[0]!==this.element[0]&&this.accept.call(this.element[0],i.currentItem||i.element)&&(this._removeHoverClass(),this._trigger("out",e,this.ui(i)))},_drop:function(e,i){var s=i||t.ui.ddmanager.current,n=!1;return s&&(s.currentItem||s.element)[0]!==this.element[0]?(this.element.find(":data(ui-droppable)").not(".ui-draggable-dragging").each(function(){var i=t(this).droppable("instance");return i.options.greedy&&!i.options.disabled&&i.options.scope===s.options.scope&&i.accept.call(i.element[0],s.currentItem||s.element)&&v(s,t.extend(i,{offset:i.element.offset()}),i.options.tolerance,e)?(n=!0,!1):void 0}),n?!1:this.accept.call(this.element[0],s.currentItem||s.element)?(this._removeActiveClass(),this._removeHoverClass(),this._trigger("drop",e,this.ui(s)),this.element):!1):!1},ui:function(t){return{draggable:t.currentItem||t.element,helper:t.helper,position:t.position,offset:t.positionAbs}},_addHoverClass:function(){this._addClass("ui-droppable-hover")},_removeHoverClass:function(){this._removeClass("ui-droppable-hover")},_addActiveClass:function(){this._addClass("ui-droppable-active")},_removeActiveClass:function(){this._removeClass("ui-droppable-active")}});var v=t.ui.intersect=function(){function t(t,e,i){return t>=e&&e+i>t}return function(e,i,s,n){if(!i.offset)return!1;var o=(e.positionAbs||e.position.absolute).left+e.margins.left,a=(e.positionAbs||e.position.absolute).top+e.margins.top,r=o+e.helperProportions.width,h=a+e.helperProportions.height,l=i.offset.left,c=i.offset.top,u=l+i.proportions().width,d=c+i.proportions().height;switch(s){case"fit":return o>=l&&u>=r&&a>=c&&d>=h;case"intersect":return o+e.helperProportions.width/2>l&&u>r-e.helperProportions.width/2&&a+e.helperProportions.height/2>c&&d>h-e.helperProportions.height/2;case"pointer":return t(n.pageY,c,i.proportions().height)&&t(n.pageX,l,i.proportions().width);case"touch":return(a>=c&&d>=a||h>=c&&d>=h||c>a&&h>d)&&(o>=l&&u>=o||r>=l&&u>=r||l>o&&r>u);default:return!1}}}();t.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(e,i){var s,n,o=t.ui.ddmanager.droppables[e.options.scope]||[],a=i?i.type:null,r=(e.currentItem||e.element).find(":data(ui-droppable)").addBack();t:for(s=0;o.length>s;s++)if(!(o[s].options.disabled||e&&!o[s].accept.call(o[s].element[0],e.currentItem||e.element))){for(n=0;r.length>n;n++)if(r[n]===o[s].element[0]){o[s].proportions().height=0;continue t}o[s].visible="none"!==o[s].element.css("display"),o[s].visible&&("mousedown"===a&&o[s]._activate.call(o[s],i),o[s].offset=o[s].element.offset(),o[s].proportions({width:o[s].element[0].offsetWidth,height:o[s].element[0].offsetHeight}))}},drop:function(e,i){var s=!1;return t.each((t.ui.ddmanager.droppables[e.options.scope]||[]).slice(),function(){this.options&&(!this.options.disabled&&this.visible&&v(e,this,this.options.tolerance,i)&&(s=this._drop.call(this,i)||s),!this.options.disabled&&this.visible&&this.accept.call(this.element[0],e.currentItem||e.element)&&(this.isout=!0,this.isover=!1,this._deactivate.call(this,i)))}),s},dragStart:function(e,i){e.element.parentsUntil("body").on("scroll.droppable",function(){e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)})},drag:function(e,i){e.options.refreshPositions&&t.ui.ddmanager.prepareOffsets(e,i),t.each(t.ui.ddmanager.droppables[e.options.scope]||[],function(){if(!this.options.disabled&&!this.greedyChild&&this.visible){var s,n,o,a=v(e,this,this.options.tolerance,i),r=!a&&this.isover?"isout":a&&!this.isover?"isover":null;r&&(this.options.greedy&&(n=this.options.scope,o=this.element.parents(":data(ui-droppable)").filter(function(){return t(this).droppable("instance").options.scope===n}),o.length&&(s=t(o[0]).droppable("instance"),s.greedyChild="isover"===r)),s&&"isover"===r&&(s.isover=!1,s.isout=!0,s._out.call(s,i)),this[r]=!0,this["isout"===r?"isover":"isout"]=!1,this["isover"===r?"_over":"_out"].call(this,i),s&&"isout"===r&&(s.isout=!1,s.isover=!0,s._over.call(s,i)))}})},dragStop:function(e,i){e.element.parentsUntil("body").off("scroll.droppable"),e.options.refreshPositions||t.ui.ddmanager.prepareOffsets(e,i)}},t.uiBackCompat!==!1&&t.widget("ui.droppable",t.ui.droppable,{options:{hoverClass:!1,activeClass:!1},_addActiveClass:function(){this._super(),this.options.activeClass&&this.element.addClass(this.options.activeClass)},_removeActiveClass:function(){this._super(),this.options.activeClass&&this.element.removeClass(this.options.activeClass)},_addHoverClass:function(){this._super(),this.options.hoverClass&&this.element.addClass(this.options.hoverClass)},_removeHoverClass:function(){this._super(),this.options.hoverClass&&this.element.removeClass(this.options.hoverClass)}}),t.ui.droppable,t.widget("ui.progressbar",{version:"1.12.1",options:{classes:{"ui-progressbar":"ui-corner-all","ui-progressbar-value":"ui-corner-left","ui-progressbar-complete":"ui-corner-right"},max:100,value:0,change:null,complete:null},min:0,_create:function(){this.oldValue=this.options.value=this._constrainedValue(),this.element.attr({role:"progressbar","aria-valuemin":this.min}),this._addClass("ui-progressbar","ui-widget ui-widget-content"),this.valueDiv=t("
    ").appendTo(this.element),this._addClass(this.valueDiv,"ui-progressbar-value","ui-widget-header"),this._refreshValue()},_destroy:function(){this.element.removeAttr("role aria-valuemin aria-valuemax aria-valuenow"),this.valueDiv.remove()},value:function(t){return void 0===t?this.options.value:(this.options.value=this._constrainedValue(t),this._refreshValue(),void 0)},_constrainedValue:function(t){return void 0===t&&(t=this.options.value),this.indeterminate=t===!1,"number"!=typeof t&&(t=0),this.indeterminate?!1:Math.min(this.options.max,Math.max(this.min,t))},_setOptions:function(t){var e=t.value;delete t.value,this._super(t),this.options.value=this._constrainedValue(e),this._refreshValue()},_setOption:function(t,e){"max"===t&&(e=Math.max(this.min,e)),this._super(t,e)},_setOptionDisabled:function(t){this._super(t),this.element.attr("aria-disabled",t),this._toggleClass(null,"ui-state-disabled",!!t)},_percentage:function(){return this.indeterminate?100:100*(this.options.value-this.min)/(this.options.max-this.min)},_refreshValue:function(){var e=this.options.value,i=this._percentage();this.valueDiv.toggle(this.indeterminate||e>this.min).width(i.toFixed(0)+"%"),this._toggleClass(this.valueDiv,"ui-progressbar-complete",null,e===this.options.max)._toggleClass("ui-progressbar-indeterminate",null,this.indeterminate),this.indeterminate?(this.element.removeAttr("aria-valuenow"),this.overlayDiv||(this.overlayDiv=t("
    ").appendTo(this.valueDiv),this._addClass(this.overlayDiv,"ui-progressbar-overlay"))):(this.element.attr({"aria-valuemax":this.options.max,"aria-valuenow":e}),this.overlayDiv&&(this.overlayDiv.remove(),this.overlayDiv=null)),this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),e===this.options.max&&this._trigger("complete")}}),t.widget("ui.selectable",t.ui.mouse,{version:"1.12.1",options:{appendTo:"body",autoRefresh:!0,distance:0,filter:"*",tolerance:"touch",selected:null,selecting:null,start:null,stop:null,unselected:null,unselecting:null},_create:function(){var e=this;this._addClass("ui-selectable"),this.dragged=!1,this.refresh=function(){e.elementPos=t(e.element[0]).offset(),e.selectees=t(e.options.filter,e.element[0]),e._addClass(e.selectees,"ui-selectee"),e.selectees.each(function(){var i=t(this),s=i.offset(),n={left:s.left-e.elementPos.left,top:s.top-e.elementPos.top};t.data(this,"selectable-item",{element:this,$element:i,left:n.left,top:n.top,right:n.left+i.outerWidth(),bottom:n.top+i.outerHeight(),startselected:!1,selected:i.hasClass("ui-selected"),selecting:i.hasClass("ui-selecting"),unselecting:i.hasClass("ui-unselecting")})})},this.refresh(),this._mouseInit(),this.helper=t("
    "),this._addClass(this.helper,"ui-selectable-helper")},_destroy:function(){this.selectees.removeData("selectable-item"),this._mouseDestroy()},_mouseStart:function(e){var i=this,s=this.options;this.opos=[e.pageX,e.pageY],this.elementPos=t(this.element[0]).offset(),this.options.disabled||(this.selectees=t(s.filter,this.element[0]),this._trigger("start",e),t(s.appendTo).append(this.helper),this.helper.css({left:e.pageX,top:e.pageY,width:0,height:0}),s.autoRefresh&&this.refresh(),this.selectees.filter(".ui-selected").each(function(){var s=t.data(this,"selectable-item");s.startselected=!0,e.metaKey||e.ctrlKey||(i._removeClass(s.$element,"ui-selected"),s.selected=!1,i._addClass(s.$element,"ui-unselecting"),s.unselecting=!0,i._trigger("unselecting",e,{unselecting:s.element}))}),t(e.target).parents().addBack().each(function(){var s,n=t.data(this,"selectable-item");return n?(s=!e.metaKey&&!e.ctrlKey||!n.$element.hasClass("ui-selected"),i._removeClass(n.$element,s?"ui-unselecting":"ui-selected")._addClass(n.$element,s?"ui-selecting":"ui-unselecting"),n.unselecting=!s,n.selecting=s,n.selected=s,s?i._trigger("selecting",e,{selecting:n.element}):i._trigger("unselecting",e,{unselecting:n.element}),!1):void 0}))},_mouseDrag:function(e){if(this.dragged=!0,!this.options.disabled){var i,s=this,n=this.options,o=this.opos[0],a=this.opos[1],r=e.pageX,h=e.pageY;return o>r&&(i=r,r=o,o=i),a>h&&(i=h,h=a,a=i),this.helper.css({left:o,top:a,width:r-o,height:h-a}),this.selectees.each(function(){var i=t.data(this,"selectable-item"),l=!1,c={};i&&i.element!==s.element[0]&&(c.left=i.left+s.elementPos.left,c.right=i.right+s.elementPos.left,c.top=i.top+s.elementPos.top,c.bottom=i.bottom+s.elementPos.top,"touch"===n.tolerance?l=!(c.left>r||o>c.right||c.top>h||a>c.bottom):"fit"===n.tolerance&&(l=c.left>o&&r>c.right&&c.top>a&&h>c.bottom),l?(i.selected&&(s._removeClass(i.$element,"ui-selected"),i.selected=!1),i.unselecting&&(s._removeClass(i.$element,"ui-unselecting"),i.unselecting=!1),i.selecting||(s._addClass(i.$element,"ui-selecting"),i.selecting=!0,s._trigger("selecting",e,{selecting:i.element}))):(i.selecting&&((e.metaKey||e.ctrlKey)&&i.startselected?(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,s._addClass(i.$element,"ui-selected"),i.selected=!0):(s._removeClass(i.$element,"ui-selecting"),i.selecting=!1,i.startselected&&(s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0),s._trigger("unselecting",e,{unselecting:i.element}))),i.selected&&(e.metaKey||e.ctrlKey||i.startselected||(s._removeClass(i.$element,"ui-selected"),i.selected=!1,s._addClass(i.$element,"ui-unselecting"),i.unselecting=!0,s._trigger("unselecting",e,{unselecting:i.element})))))}),!1}},_mouseStop:function(e){var i=this;return this.dragged=!1,t(".ui-unselecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-unselecting"),s.unselecting=!1,s.startselected=!1,i._trigger("unselected",e,{unselected:s.element})}),t(".ui-selecting",this.element[0]).each(function(){var s=t.data(this,"selectable-item");i._removeClass(s.$element,"ui-selecting")._addClass(s.$element,"ui-selected"),s.selecting=!1,s.selected=!0,s.startselected=!0,i._trigger("selected",e,{selected:s.element})}),this._trigger("stop",e),this.helper.remove(),!1}}),t.widget("ui.selectmenu",[t.ui.formResetMixin,{version:"1.12.1",defaultElement:"",widgetEventPrefix:"spin",options:{classes:{"ui-spinner":"ui-corner-all","ui-spinner-down":"ui-corner-br","ui-spinner-up":"ui-corner-tr"},culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),""!==this.value()&&this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var e=this._super(),i=this.element;return t.each(["min","max","step"],function(t,s){var n=i.attr(s);null!=n&&n.length&&(e[s]=n)}),e},_events:{keydown:function(t){this._start(t)&&this._keydown(t)&&t.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(t){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._stop(),this._refresh(),this.previous!==this.element.val()&&this._trigger("change",t),void 0)},mousewheel:function(t,e){if(e){if(!this.spinning&&!this._start(t))return!1;this._spin((e>0?1:-1)*this.options.step,t),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(t)},100),t.preventDefault()}},"mousedown .ui-spinner-button":function(e){function i(){var e=this.element[0]===t.ui.safeActiveElement(this.document[0]);e||(this.element.trigger("focus"),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===t.ui.safeActiveElement(this.document[0])?this.previous:this.element.val(),e.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(e)!==!1&&this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(e){return t(e.currentTarget).hasClass("ui-state-active")?this._start(e)===!1?!1:(this._repeat(null,t(e.currentTarget).hasClass("ui-spinner-up")?1:-1,e),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_enhance:function(){this.uiSpinner=this.element.attr("autocomplete","off").wrap("").parent().append("")},_draw:function(){this._enhance(),this._addClass(this.uiSpinner,"ui-spinner","ui-widget ui-widget-content"),this._addClass("ui-spinner-input"),this.element.attr("role","spinbutton"),this.buttons=this.uiSpinner.children("a").attr("tabIndex",-1).attr("aria-hidden",!0).button({classes:{"ui-button":""}}),this._removeClass(this.buttons,"ui-corner-all"),this._addClass(this.buttons.first(),"ui-spinner-button ui-spinner-up"),this._addClass(this.buttons.last(),"ui-spinner-button ui-spinner-down"),this.buttons.first().button({icon:this.options.icons.up,showLabel:!1}),this.buttons.last().button({icon:this.options.icons.down,showLabel:!1}),this.buttons.height()>Math.ceil(.5*this.uiSpinner.height())&&this.uiSpinner.height()>0&&this.uiSpinner.height(this.uiSpinner.height())},_keydown:function(e){var i=this.options,s=t.ui.keyCode;switch(e.keyCode){case s.UP:return this._repeat(null,1,e),!0;case s.DOWN:return this._repeat(null,-1,e),!0;case s.PAGE_UP:return this._repeat(null,i.page,e),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,e),!0}return!1},_start:function(t){return this.spinning||this._trigger("start",t)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(t,e,i){t=t||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,e,i)},t),this._spin(e*this.options.step,i)},_spin:function(t,e){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+t*this._increment(this.counter)),this.spinning&&this._trigger("spin",e,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(e){var i=this.options.incremental;return i?t.isFunction(i)?i(e):Math.floor(e*e*e/5e4-e*e/500+17*e/200+1):1},_precision:function(){var t=this._precisionOf(this.options.step);return null!==this.options.min&&(t=Math.max(t,this._precisionOf(this.options.min))),t},_precisionOf:function(t){var e=""+t,i=e.indexOf(".");return-1===i?0:e.length-i-1},_adjustValue:function(t){var e,i,s=this.options;return e=null!==s.min?s.min:0,i=t-e,i=Math.round(i/s.step)*s.step,t=e+i,t=parseFloat(t.toFixed(this._precision())),null!==s.max&&t>s.max?s.max:null!==s.min&&s.min>t?s.min:t},_stop:function(t){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",t))},_setOption:function(t,e){var i,s,n;return"culture"===t||"numberFormat"===t?(i=this._parse(this.element.val()),this.options[t]=e,this.element.val(this._format(i)),void 0):(("max"===t||"min"===t||"step"===t)&&"string"==typeof e&&(e=this._parse(e)),"icons"===t&&(s=this.buttons.first().find(".ui-icon"),this._removeClass(s,null,this.options.icons.up),this._addClass(s,null,e.up),n=this.buttons.last().find(".ui-icon"),this._removeClass(n,null,this.options.icons.down),this._addClass(n,null,e.down)),this._super(t,e),void 0)},_setOptionDisabled:function(t){this._super(t),this._toggleClass(this.uiSpinner,null,"ui-state-disabled",!!t),this.element.prop("disabled",!!t),this.buttons.button(t?"disable":"enable")},_setOptions:r(function(t){this._super(t)}),_parse:function(t){return"string"==typeof t&&""!==t&&(t=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(t,10,this.options.culture):+t),""===t||isNaN(t)?null:t},_format:function(t){return""===t?"":window.Globalize&&this.options.numberFormat?Globalize.format(t,this.options.numberFormat,this.options.culture):t},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},isValid:function(){var t=this.value();return null===t?!1:t===this._adjustValue(t)},_value:function(t,e){var i;""!==t&&(i=this._parse(t),null!==i&&(e||(i=this._adjustValue(i)),t=this._format(i))),this.element.val(t),this._refresh()},_destroy:function(){this.element.prop("disabled",!1).removeAttr("autocomplete role aria-valuemin aria-valuemax aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:r(function(t){this._stepUp(t)}),_stepUp:function(t){this._start()&&(this._spin((t||1)*this.options.step),this._stop())},stepDown:r(function(t){this._stepDown(t)}),_stepDown:function(t){this._start()&&(this._spin((t||1)*-this.options.step),this._stop())},pageUp:r(function(t){this._stepUp((t||1)*this.options.page)}),pageDown:r(function(t){this._stepDown((t||1)*this.options.page)}),value:function(t){return arguments.length?(r(this._value).call(this,t),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}}),t.uiBackCompat!==!1&&t.widget("ui.spinner",t.ui.spinner,{_enhance:function(){this.uiSpinner=this.element.attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml())},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""}}),t.ui.spinner,t.widget("ui.tabs",{version:"1.12.1",delay:300,options:{active:null,classes:{"ui-tabs":"ui-corner-all","ui-tabs-nav":"ui-corner-all","ui-tabs-panel":"ui-corner-bottom","ui-tabs-tab":"ui-corner-top"},collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_isLocal:function(){var t=/#.*$/;return function(e){var i,s;i=e.href.replace(t,""),s=location.href.replace(t,"");try{i=decodeURIComponent(i)}catch(n){}try{s=decodeURIComponent(s)}catch(n){}return e.hash.length>1&&i===s}}(),_create:function(){var e=this,i=this.options;this.running=!1,this._addClass("ui-tabs","ui-widget ui-widget-content"),this._toggleClass("ui-tabs-collapsible",null,i.collapsible),this._processTabs(),i.active=this._initialActive(),t.isArray(i.disabled)&&(i.disabled=t.unique(i.disabled.concat(t.map(this.tabs.filter(".ui-state-disabled"),function(t){return e.tabs.index(t)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(i.active):t(),this._refresh(),this.active.length&&this.load(i.active)},_initialActive:function(){var e=this.options.active,i=this.options.collapsible,s=location.hash.substring(1);return null===e&&(s&&this.tabs.each(function(i,n){return t(n).attr("aria-controls")===s?(e=i,!1):void 0}),null===e&&(e=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===e||-1===e)&&(e=this.tabs.length?0:!1)),e!==!1&&(e=this.tabs.index(this.tabs.eq(e)),-1===e&&(e=i?!1:0)),!i&&e===!1&&this.anchors.length&&(e=0),e},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):t()}},_tabKeydown:function(e){var i=t(t.ui.safeActiveElement(this.document[0])).closest("li"),s=this.tabs.index(i),n=!0;if(!this._handlePageNav(e)){switch(e.keyCode){case t.ui.keyCode.RIGHT:case t.ui.keyCode.DOWN:s++;break;case t.ui.keyCode.UP:case t.ui.keyCode.LEFT:n=!1,s--;break;case t.ui.keyCode.END:s=this.anchors.length-1;break;case t.ui.keyCode.HOME:s=0;break;case t.ui.keyCode.SPACE:return e.preventDefault(),clearTimeout(this.activating),this._activate(s),void 0;case t.ui.keyCode.ENTER:return e.preventDefault(),clearTimeout(this.activating),this._activate(s===this.options.active?!1:s),void 0;default:return}e.preventDefault(),clearTimeout(this.activating),s=this._focusNextTab(s,n),e.ctrlKey||e.metaKey||(i.attr("aria-selected","false"),this.tabs.eq(s).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",s)},this.delay))}},_panelKeydown:function(e){this._handlePageNav(e)||e.ctrlKey&&e.keyCode===t.ui.keyCode.UP&&(e.preventDefault(),this.active.trigger("focus"))},_handlePageNav:function(e){return e.altKey&&e.keyCode===t.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):e.altKey&&e.keyCode===t.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):void 0},_findNextTab:function(e,i){function s(){return e>n&&(e=0),0>e&&(e=n),e}for(var n=this.tabs.length-1;-1!==t.inArray(s(),this.options.disabled);)e=i?e+1:e-1;return e},_focusNextTab:function(t,e){return t=this._findNextTab(t,e),this.tabs.eq(t).trigger("focus"),t},_setOption:function(t,e){return"active"===t?(this._activate(e),void 0):(this._super(t,e),"collapsible"===t&&(this._toggleClass("ui-tabs-collapsible",null,e),e||this.options.active!==!1||this._activate(0)),"event"===t&&this._setupEvents(e),"heightStyle"===t&&this._setupHeightStyle(e),void 0)},_sanitizeSelector:function(t){return t?t.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var e=this.options,i=this.tablist.children(":has(a[href])");e.disabled=t.map(i.filter(".ui-state-disabled"),function(t){return i.index(t)}),this._processTabs(),e.active!==!1&&this.anchors.length?this.active.length&&!t.contains(this.tablist[0],this.active[0])?this.tabs.length===e.disabled.length?(e.active=!1,this.active=t()):this._activate(this._findNextTab(Math.max(0,e.active-1),!1)):e.active=this.tabs.index(this.active):(e.active=!1,this.active=t()),this._refresh()},_refresh:function(){this._setOptionDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-hidden":"true"}),this.active.length?(this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}),this._addClass(this.active,"ui-tabs-active","ui-state-active"),this._getPanelForTab(this.active).show().attr({"aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var e=this,i=this.tabs,s=this.anchors,n=this.panels;this.tablist=this._getList().attr("role","tablist"),this._addClass(this.tablist,"ui-tabs-nav","ui-helper-reset ui-helper-clearfix ui-widget-header"),this.tablist.on("mousedown"+this.eventNamespace,"> li",function(e){t(this).is(".ui-state-disabled")&&e.preventDefault()}).on("focus"+this.eventNamespace,".ui-tabs-anchor",function(){t(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this.tabs=this.tablist.find("> li:has(a[href])").attr({role:"tab",tabIndex:-1}),this._addClass(this.tabs,"ui-tabs-tab","ui-state-default"),this.anchors=this.tabs.map(function(){return t("a",this)[0]}).attr({role:"presentation",tabIndex:-1}),this._addClass(this.anchors,"ui-tabs-anchor"),this.panels=t(),this.anchors.each(function(i,s){var n,o,a,r=t(s).uniqueId().attr("id"),h=t(s).closest("li"),l=h.attr("aria-controls");e._isLocal(s)?(n=s.hash,a=n.substring(1),o=e.element.find(e._sanitizeSelector(n))):(a=h.attr("aria-controls")||t({}).uniqueId()[0].id,n="#"+a,o=e.element.find(n),o.length||(o=e._createPanel(a),o.insertAfter(e.panels[i-1]||e.tablist)),o.attr("aria-live","polite")),o.length&&(e.panels=e.panels.add(o)),l&&h.data("ui-tabs-aria-controls",l),h.attr({"aria-controls":a,"aria-labelledby":r}),o.attr("aria-labelledby",r)}),this.panels.attr("role","tabpanel"),this._addClass(this.panels,"ui-tabs-panel","ui-widget-content"),i&&(this._off(i.not(this.tabs)),this._off(s.not(this.anchors)),this._off(n.not(this.panels)))},_getList:function(){return this.tablist||this.element.find("ol, ul").eq(0)},_createPanel:function(e){return t("
    ").attr("id",e).data("ui-tabs-destroy",!0)},_setOptionDisabled:function(e){var i,s,n;for(t.isArray(e)&&(e.length?e.length===this.anchors.length&&(e=!0):e=!1),n=0;s=this.tabs[n];n++)i=t(s),e===!0||-1!==t.inArray(n,e)?(i.attr("aria-disabled","true"),this._addClass(i,null,"ui-state-disabled")):(i.removeAttr("aria-disabled"),this._removeClass(i,null,"ui-state-disabled"));this.options.disabled=e,this._toggleClass(this.widget(),this.widgetFullName+"-disabled",null,e===!0)},_setupEvents:function(e){var i={};e&&t.each(e.split(" "),function(t,e){i[e]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(!0,this.anchors,{click:function(t){t.preventDefault()}}),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(e){var i,s=this.element.parent();"fill"===e?(i=s.height(),i-=this.element.outerHeight()-this.element.height(),this.element.siblings(":visible").each(function(){var e=t(this),s=e.css("position");"absolute"!==s&&"fixed"!==s&&(i-=e.outerHeight(!0))}),this.element.children().not(this.panels).each(function(){i-=t(this).outerHeight(!0)}),this.panels.each(function(){t(this).height(Math.max(0,i-t(this).innerHeight()+t(this).height()))}).css("overflow","auto")):"auto"===e&&(i=0,this.panels.each(function(){i=Math.max(i,t(this).height("").height())}).height(i))},_eventHandler:function(e){var i=this.options,s=this.active,n=t(e.currentTarget),o=n.closest("li"),a=o[0]===s[0],r=a&&i.collapsible,h=r?t():this._getPanelForTab(o),l=s.length?this._getPanelForTab(s):t(),c={oldTab:s,oldPanel:l,newTab:r?t():o,newPanel:h};e.preventDefault(),o.hasClass("ui-state-disabled")||o.hasClass("ui-tabs-loading")||this.running||a&&!i.collapsible||this._trigger("beforeActivate",e,c)===!1||(i.active=r?!1:this.tabs.index(o),this.active=a?t():o,this.xhr&&this.xhr.abort(),l.length||h.length||t.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(o),e),this._toggle(e,c))},_toggle:function(e,i){function s(){o.running=!1,o._trigger("activate",e,i)}function n(){o._addClass(i.newTab.closest("li"),"ui-tabs-active","ui-state-active"),a.length&&o.options.show?o._show(a,o.options.show,s):(a.show(),s())}var o=this,a=i.newPanel,r=i.oldPanel;this.running=!0,r.length&&this.options.hide?this._hide(r,this.options.hide,function(){o._removeClass(i.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),n()}):(this._removeClass(i.oldTab.closest("li"),"ui-tabs-active","ui-state-active"),r.hide(),n()),r.attr("aria-hidden","true"),i.oldTab.attr({"aria-selected":"false","aria-expanded":"false"}),a.length&&r.length?i.oldTab.attr("tabIndex",-1):a.length&&this.tabs.filter(function(){return 0===t(this).attr("tabIndex")}).attr("tabIndex",-1),a.attr("aria-hidden","false"),i.newTab.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0})},_activate:function(e){var i,s=this._findActive(e);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:t.noop}))},_findActive:function(e){return e===!1?t():this.tabs.eq(e)},_getIndex:function(e){return"string"==typeof e&&(e=this.anchors.index(this.anchors.filter("[href$='"+t.ui.escapeSelector(e)+"']"))),e},_destroy:function(){this.xhr&&this.xhr.abort(),this.tablist.removeAttr("role").off(this.eventNamespace),this.anchors.removeAttr("role tabIndex").removeUniqueId(),this.tabs.add(this.panels).each(function(){t.data(this,"ui-tabs-destroy")?t(this).remove():t(this).removeAttr("role tabIndex aria-live aria-busy aria-selected aria-labelledby aria-hidden aria-expanded")}),this.tabs.each(function(){var e=t(this),i=e.data("ui-tabs-aria-controls");i?e.attr("aria-controls",i).removeData("ui-tabs-aria-controls"):e.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(e){var i=this.options.disabled;i!==!1&&(void 0===e?i=!1:(e=this._getIndex(e),i=t.isArray(i)?t.map(i,function(t){return t!==e?t:null}):t.map(this.tabs,function(t,i){return i!==e?i:null})),this._setOptionDisabled(i))},disable:function(e){var i=this.options.disabled;if(i!==!0){if(void 0===e)i=!0;else{if(e=this._getIndex(e),-1!==t.inArray(e,i))return;i=t.isArray(i)?t.merge([e],i).sort():[e]}this._setOptionDisabled(i)}},load:function(e,i){e=this._getIndex(e);var s=this,n=this.tabs.eq(e),o=n.find(".ui-tabs-anchor"),a=this._getPanelForTab(n),r={tab:n,panel:a},h=function(t,e){"abort"===e&&s.panels.stop(!1,!0),s._removeClass(n,"ui-tabs-loading"),a.removeAttr("aria-busy"),t===s.xhr&&delete s.xhr};this._isLocal(o[0])||(this.xhr=t.ajax(this._ajaxSettings(o,i,r)),this.xhr&&"canceled"!==this.xhr.statusText&&(this._addClass(n,"ui-tabs-loading"),a.attr("aria-busy","true"),this.xhr.done(function(t,e,n){setTimeout(function(){a.html(t),s._trigger("load",i,r),h(n,e)},1)}).fail(function(t,e){setTimeout(function(){h(t,e)},1)})))},_ajaxSettings:function(e,i,s){var n=this;return{url:e.attr("href").replace(/#.*$/,""),beforeSend:function(e,o){return n._trigger("beforeLoad",i,t.extend({jqXHR:e,ajaxSettings:o},s))}}},_getPanelForTab:function(e){var i=t(e).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}}),t.uiBackCompat!==!1&&t.widget("ui.tabs",t.ui.tabs,{_processTabs:function(){this._superApply(arguments),this._addClass(this.tabs,"ui-tab")}}),t.ui.tabs,t.widget("ui.tooltip",{version:"1.12.1",options:{classes:{"ui-tooltip":"ui-corner-all ui-widget-shadow"},content:function(){var e=t(this).attr("title")||"";return t("").text(e).html()},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,track:!1,close:null,open:null},_addDescribedBy:function(e,i){var s=(e.attr("aria-describedby")||"").split(/\s+/);s.push(i),e.data("ui-tooltip-id",i).attr("aria-describedby",t.trim(s.join(" ")))},_removeDescribedBy:function(e){var i=e.data("ui-tooltip-id"),s=(e.attr("aria-describedby")||"").split(/\s+/),n=t.inArray(i,s);-1!==n&&s.splice(n,1),e.removeData("ui-tooltip-id"),s=t.trim(s.join(" ")),s?e.attr("aria-describedby",s):e.removeAttr("aria-describedby")},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.liveRegion=t("
    ").attr({role:"log","aria-live":"assertive","aria-relevant":"additions"}).appendTo(this.document[0].body),this._addClass(this.liveRegion,null,"ui-helper-hidden-accessible"),this.disabledTitles=t([])},_setOption:function(e,i){var s=this;this._super(e,i),"content"===e&&t.each(this.tooltips,function(t,e){s._updateContent(e.element)})},_setOptionDisabled:function(t){this[t?"_disable":"_enable"]()},_disable:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur");n.target=n.currentTarget=s.element[0],e.close(n,!0)}),this.disabledTitles=this.disabledTitles.add(this.element.find(this.options.items).addBack().filter(function(){var e=t(this);return e.is("[title]")?e.data("ui-tooltip-title",e.attr("title")).removeAttr("title"):void 0}))},_enable:function(){this.disabledTitles.each(function(){var e=t(this);e.data("ui-tooltip-title")&&e.attr("title",e.data("ui-tooltip-title"))}),this.disabledTitles=t([])},open:function(e){var i=this,s=t(e?e.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),e&&"mouseover"===e.type&&s.parents().each(function(){var e,s=t(this);s.data("ui-tooltip-open")&&(e=t.Event("blur"),e.target=e.currentTarget=this,i.close(e,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._registerCloseHandlers(e,s),this._updateContent(s,e))},_updateContent:function(t,e){var i,s=this.options.content,n=this,o=e?e.type:null;return"string"==typeof s||s.nodeType||s.jquery?this._open(e,t,s):(i=s.call(t[0],function(i){n._delay(function(){t.data("ui-tooltip-open")&&(e&&(e.type=o),this._open(e,t,i))})}),i&&this._open(e,t,i),void 0)},_open:function(e,i,s){function n(t){l.of=t,a.is(":hidden")||a.position(l)}var o,a,r,h,l=t.extend({},this.options.position);if(s){if(o=this._find(i))return o.tooltip.find(".ui-tooltip-content").html(s),void 0;i.is("[title]")&&(e&&"mouseover"===e.type?i.attr("title",""):i.removeAttr("title")),o=this._tooltip(i),a=o.tooltip,this._addDescribedBy(i,a.attr("id")),a.find(".ui-tooltip-content").html(s),this.liveRegion.children().hide(),h=t("
    ").html(a.find(".ui-tooltip-content").html()),h.removeAttr("name").find("[name]").removeAttr("name"),h.removeAttr("id").find("[id]").removeAttr("id"),h.appendTo(this.liveRegion),this.options.track&&e&&/^mouse/.test(e.type)?(this._on(this.document,{mousemove:n}),n(e)):a.position(t.extend({of:i},this.options.position)),a.hide(),this._show(a,this.options.show),this.options.track&&this.options.show&&this.options.show.delay&&(r=this.delayedShow=setInterval(function(){a.is(":visible")&&(n(l.of),clearInterval(r))},t.fx.interval)),this._trigger("open",e,{tooltip:a})}},_registerCloseHandlers:function(e,i){var s={keyup:function(e){if(e.keyCode===t.ui.keyCode.ESCAPE){var s=t.Event(e);s.currentTarget=i[0],this.close(s,!0)}}};i[0]!==this.element[0]&&(s.remove=function(){this._removeTooltip(this._find(i).tooltip)}),e&&"mouseover"!==e.type||(s.mouseleave="close"),e&&"focusin"!==e.type||(s.focusout="close"),this._on(!0,i,s)},close:function(e){var i,s=this,n=t(e?e.currentTarget:this.element),o=this._find(n);return o?(i=o.tooltip,o.closing||(clearInterval(this.delayedShow),n.data("ui-tooltip-title")&&!n.attr("title")&&n.attr("title",n.data("ui-tooltip-title")),this._removeDescribedBy(n),o.hiding=!0,i.stop(!0),this._hide(i,this.options.hide,function(){s._removeTooltip(t(this))}),n.removeData("ui-tooltip-open"),this._off(n,"mouseleave focusout keyup"),n[0]!==this.element[0]&&this._off(n,"remove"),this._off(this.document,"mousemove"),e&&"mouseleave"===e.type&&t.each(this.parents,function(e,i){t(i.element).attr("title",i.title),delete s.parents[e]}),o.closing=!0,this._trigger("close",e,{tooltip:i}),o.hiding||(o.closing=!1)),void 0):(n.removeData("ui-tooltip-open"),void 0)},_tooltip:function(e){var i=t("
    ").attr("role","tooltip"),s=t("
    ").appendTo(i),n=i.uniqueId().attr("id");return this._addClass(s,"ui-tooltip-content"),this._addClass(i,"ui-tooltip","ui-widget ui-widget-content"),i.appendTo(this._appendTo(e)),this.tooltips[n]={element:e,tooltip:i}},_find:function(t){var e=t.data("ui-tooltip-id");return e?this.tooltips[e]:null},_removeTooltip:function(t){t.remove(),delete this.tooltips[t.attr("id")]},_appendTo:function(t){var e=t.closest(".ui-front, dialog");return e.length||(e=this.document[0].body),e},_destroy:function(){var e=this;t.each(this.tooltips,function(i,s){var n=t.Event("blur"),o=s.element;n.target=n.currentTarget=o[0],e.close(n,!0),t("#"+i).remove(),o.data("ui-tooltip-title")&&(o.attr("title")||o.attr("title",o.data("ui-tooltip-title")),o.removeData("ui-tooltip-title"))}),this.liveRegion.remove()}}),t.uiBackCompat!==!1&&t.widget("ui.tooltip",t.ui.tooltip,{options:{tooltipClass:null},_tooltip:function(){var t=this._superApply(arguments);return this.options.tooltipClass&&t.tooltip.addClass(this.options.tooltipClass),t}}),t.ui.tooltip}); \ No newline at end of file diff --git a/public_html/js/jquery.min.js b/public_html/js/jquery.min.js new file mode 100644 index 0000000..4c5be4c --- /dev/null +++ b/public_html/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), +a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
    "],col:[2,"","
    "],tr:[2,"","
    "],td:[3,"","
    "],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), +void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" - - diff --git a/public_html/jsolait/jsolait.ywe.js b/public_html/jsolait/jsolait.ywe.js deleted file mode 100644 index 795b45e..0000000 --- a/public_html/jsolait/jsolait.ywe.js +++ /dev/null @@ -1,41 +0,0 @@ - -include(typeof JsolaitInstallPath=='undefined'?"./jsolait/jsolait.js":JsolaitInstallPath); -jsolait.__imprt__=function(name){ -if(jsolait.modules[name]){ -return jsolait.modules[name]; -}else{ -var src,modPath; -var searchURIs=[]; -if(jsolait.knownModuleURIs[name]!=undefined){ -searchURIs.push(jsolait.knownModuleURIs[name].format(jsolait)); -}else{ -name=name.split('.'); -if(name.length>1){ -if(jsolait.knownModuleURIs[name[0]]!=undefined){ -var uri=jsolait.knownModuleURIs[name[0]].format(jsolait); -searchURIs.push("%s/%s.js".format(uri,name.slice(1).join('/'))); -} -searchURIs.push("%s/%s.js".format(jsolait.packagesURI.format(jsolait),name.join('/'))); -} -for(var i=0;i0){ -this.queue.shift().call(null); -if((new Date()).getTime()-startTime>=maxTimeToRun){ -mod.appendTask(this); -break; -} -} -}; -publ.insertSubtasks=function(){ -for(var i=arguments.length-1;i>=0;i--){ -this.queue.unshift(arguments[i]); -} -}; -}); -mod.tasks=[]; -mod.appendTask=function(t){ -mod.tasks.push(t); -}; -mod.currentTask=null; -mod.runTasks=function(){ -while(mod.tasks.length>0&&mod.currentTask==null){ -mod.currentTask=mod.tasks.shift(); -mod.currentTask.run(mod.currentTask.timeToRun); -mod.currentTask=null; -if(typeof(setTimeout)!='undefined'){ -setTimeout('imprt("async").runTasks()',0); -break; -} -} -}; -mod.insertSubtasks=function(t){ -mod.currentTask.insertSubtasks.apply(mod.currentTask,arguments); -}; -mod.IterTask=Class(function(publ,priv,supr){ -publ.__init__=function(t){ -this.t=t; -this.p=0; -}; -publ.step=function(){ -if((this.p++)<100){ -mod.insertSubtasks(this.t,bind(this,this.step)); -} -}; -}); -var iter=function(t){ -var i=new mod.IterTask(t); -return mod.insertSubtasks(bind(i,i.step)); -};var seq=function(){ -return mod.insertSubtasks.apply(mod,arguments); -}; -var runTask=function(f){ -var x=(new mod.Task(f)).start(); -return mod.runTasks(); -}; -mod.__main__=function(){ -var i=0; -var k=0; -var t1=new mod.Task(function(){ -print("sdfasdfasdfasdfasdfasdfasdfasdfasdfasd"); -iter(function(){ -print('a',i++); -iter(function(){ -for(var i=0;i<1000;i++){ -} -}); -}); -}); -var t2=new mod.Task(function(){ -seq(function(){ -iter(function(){ -print('b',k++); -iter(function(){ -}); -}); -},function(){ -print("done ..................... b"); -}); -}); -runTask(function(){ -t1.start(); -t2.start(); -}); -}; -}); diff --git a/public_html/jsolait/lib/codecs.js b/public_html/jsolait/lib/codecs.js deleted file mode 100644 index c2fd062..0000000 --- a/public_html/jsolait/lib/codecs.js +++ /dev/null @@ -1,151 +0,0 @@ - -Module("codecs","$Revision: 44 $",function(mod){ -mod.listEncoders=function(){ -var c=[]; -for(var attr in String.prototype){ -if(attr.slice(0,7)=="encode_"){ -c.push(attr.slice(7)); -} -} -return c; -}; -mod.listDecoders=function(){ -var c=[]; -for(var attr in String.prototype){ -if(attr.slice(0,7)=="decode_"){ -c.push(attr.slice(7)); -} -} -return c; -}; -String.prototype.decode=function(codec){ -var n="decode_"+codec; -if(String.prototype[n]){ -var args=[]; -for(var i=1;i>16,(nBits&0xff00)>>8,nBits&0xff); -} -sDecoded[sDecoded.length-1]=sDecoded[sDecoded.length-1].substring(0,3-((this.charCodeAt(i-2)==61)?2:(this.charCodeAt(i-1)==61?1:0))); -return sDecoded.join(""); -} -}else{ -throw new mod.Exception("String length must be divisible by 4."); -} -}; -String.prototype.encode_base64=function(){ -if(typeof(btoa)!="undefined"){ -return btoa(this); -}else{ -var base64=['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', -'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', -'0','1','2','3','4','5','6','7','8','9','+','/']; -var sbin; -var pad=0; -var s=""+this; -if((s.length%3)==1){ -s+=String.fromCharCode(0); -s+=String.fromCharCode(0); -pad=2; -}else if((s.length%3)==2){ -s+=String.fromCharCode(0); -pad=1; -} -var rslt=new Array(s.length/3); -var ri=0; -for(var i=0;i>18)&0x3f]+base64[(sbin>>12)&0x3f]+base64[(sbin>>6)&0x3f]+base64[sbin&0x3f]); -ri++; -} -if(pad>0){ -rslt[rslt.length-1]=rslt[rslt.length-1].substr(0,4-pad)+((pad==2)?"==":(pad==1)?"=":""); -} -return rslt.join(""); -} -}; -String.prototype.decode_uri=function(){ -return decodeURIComponent(this); -}; -String.prototype.encode_uri=function(){ -return encodeURIComponent(this); -}; -String.prototype.encode_lzw=function(){ -var dict={}; -var data=(this+"").split(""); -var out=[]; -var currChar; -var phrase=data[0]; -var code=256; -for(var i=1;i1?dict[phrase]:phrase.charCodeAt(0)); -dict[phrase+currChar]=code; -code++; -phrase=currChar; -} -} -out.push(phrase.length>1?dict[phrase]:phrase.charCodeAt(0)); -for(var i=0;i?"); -print(fm.encode()); -try{ -print(fm.submitNoReload().responseText); -}catch(e){ -print(e); -} -fm.method="post"; -print(fm.submitNoReload().responseText); -}; -}); diff --git a/public_html/jsolait/lib/iter.js b/public_html/jsolait/lib/iter.js deleted file mode 100644 index bcfa167..0000000 --- a/public_html/jsolait/lib/iter.js +++ /dev/null @@ -1,248 +0,0 @@ - -Module("iter","$Revision: 64 $",function(mod){ -mod.Iterator=Class(function(publ,supr){ -publ.next=function(){ -return undefined; -}; -publ.__iter__=function(){ -return this; -}; -publ.__iterate__=function(thisObj,cb){ -var result; -thisObj=thisObj==null?this:thisObj; -var item; -while(((item=this.next())!==undefined)&&result===undefined){ -if(item.__tupleResult__){ -item.push(this); -result=cb.apply(thisObj,item); -}else{ -result=cb.call(thisObj,item,this); -} -} -return result; -}; -publ.__filter__=function(thisObj,cb){ -var result=[]; -thisObj=thisObj==null?this:thisObj; -var item,doKeep; -while((item=this.next())!==undefined){ -if(item.__tupleResult__){ -item.push(this); -doKeep=cb.apply(thisObj,item); -}else{ -doKeep=cb.call(thisObj,item,this); -} -if(doKeep){ -result.push(item); -} -} -return result; -}; -publ.__map__=function(thisObj,cb){ -var result=[]; -thisObj=thisObj==null?this:thisObj; -var item,mapedItem; -while((item=this.next())!==undefined){ -if(item.__tupleResult__){ -item.push(this); -mapedItem=cb.apply(thisObj,item); -}else{ -mapedItem=cb.call(thisObj,item,this); -} -result.push(mapedItem); -} -return result; -}; -publ.__list__=function(){ -var list=[]; -var item; -while((item=this.next())!==undefined){ -list.push(item); -} -return list; -}; -publ.replace=function(item){ -throw new mod.Exception("Iterator::replace() not implemented"); -}; -}); -mod.Range=Class(mod.Iterator,function(publ,supr){ -publ.__init__=function(start,end,step){ -switch(arguments.length){ -case 1: -this.start=0; -this.end=start; -this.step=1; -break; -case 2: -this.start=start; -this.end=end; -this.step=1; -break; -default: -this.start=start; -this.end=end; -this.step=step; -break; -} -this.current=this.start-this.step; -}; -publ.next=function(){ -var n=this.current+this.step; -if(n>this.end){ -this.current=this.start; -return undefined; -}else{ -this.current=n; -return this.current; -} -}; -publ.__iterate__=function(thisObj,cb){ -var result=undefined; -for(this.current+=this.step;this.current<=this.end&&result===undefined;this.current+=this.step){ -result=cb.call(thisObj,this.current,this); -} -return result; -}; -}); -mod.range=function(start,end,step){ -var r=new mod.Range(Class); -r.__init__.apply(r,arguments); -return r; -}; -mod.ArrayItereator=Class(mod.Iterator,function(publ,supr){ -publ.__init__=function(array){ -this.array=array; -this.index=-1; -}; -publ.next=function(){ -this.index+=1; -if(this.index>=this.array.length){ -return undefined; -}else{ -return this.array[this.index]; -} -}; -publ.__iterate__=function(thisObj,cb){ -var result=undefined; -thisObj=thisObj==null?this:thisObj; -var args=[null,this]; -for(this.index++;this.index=this.keys.length){ -return undefined; -}else{ -var key=this.keys[this.index]; -var rslt={key:key}; -try{ -rslt.value=this.obj[key]; -}catch(e){ -} -return rslt; -} -}; -}); -mod.iter=function(iterable,thisObj,cb){ -var iterator; -if(iterable.__iter__!==undefined){ -iterator=iterable.__iter__(); -}else if(iterable.length!=null){ -iterator=new mod.ArrayItereator(iterable); -}else if(iterable.constructor==Object){ -iterator=new mod.ObjectIterator(iterable); -}else{ -throw new mod.Exception("Iterable object does not provide __iter__ method or no Iterator found."); -} -if(arguments.length==1){ -return iterator; -}else{ -if(cb==null){ -cb=thisObj; -thisObj=null; -} -return iterator.__iterate__(thisObj,cb); -} -}; -mod.IterationCallback=function(item,iteration){}; -mod.filter=function(iterable,thisObj,cb){ -var iterator=mod.iter(iterable); -if(cb==null){ -cb=thisObj; -thisObj=null; -} -return iterator.__filter__(thisObj,cb); -}; -mod.map=function(iterable,thisObj,cb){ -var iterator=mod.iter(iterable); -if(cb==null){ -cb=thisObj; -thisObj=null; -} -return iterator.__map__(thisObj,cb); -}; -mod.list=function(iterable){ -return mod.iter(iterable).__list__(); -}; -mod.Zipper=Class(mod.Iterator,function(publ,priv,supr){ -publ.__init__=function(iterators){ -this.iterators=iterators; -}; -publ.next=function(){ -var r=[]; -r.__tupleResult__=true; -var item; -for(var i=0;i=0){if(s.charAt(p-1)=="\\"){ -rs+=s.slice(0,p+1);s=s.slice(p+1);}else{ -return rs+s.slice(0,p+1); -} -p=s.indexOf(startEndChar); -} -throw new mod.Exception(startEndChar+" expected."); -}; -var extractSLComment=function(s){ -var p=s.search(/\n/); -if(p>=0){ -return s.slice(0,p); -}else{ -return s; -} -}; -var extractMLComment=function(s){ -var p=s.search(/\*\//); -if(p>=0){ -return s.slice(0,p+2); -}else{ -throw new mod.Exception("End of comment expected."); -} -}; -mod.Token=Class(function(publ,supr){ -publ.__init__=function(value,pos,err){ -this.value=value; -this.pos=pos; -this.err=err; -}; -publ.toString=function(){ -return "["+this.constructor.__name__+" "+this.value+"]"; -}; -}); -mod.TokenWhiteSpace=Class(mod.Token,function(publ,supr){}); -mod.TokenPunctuator=Class(mod.Token,function(publ,supr){}); -mod.TokenNewLine=Class(mod.Token,function(publ,supr){}); -mod.TokenNumber=Class(mod.Token,function(publ,supr){}); -mod.TokenKeyword=Class(mod.Token,function(publ,supr){}); -mod.TokenString=Class(mod.Token,function(publ,supr){}); -mod.TokenRegExp=Class(mod.Token,function(publ,supr){}); -mod.TokenIdentifier=Class(mod.Token,function(publ,supr){}); -mod.TokenComment=Class(mod.Token,function(publ,supr){}); -mod.TokenDocComment=Class(mod.TokenComment,function(publ,supr){}); -var arithmaticOperators=new sets.Set(['/','+','-','*','%']); -var relationalOperators=new sets.Set(['<','>','<=','>=','instanceof']); -var equalityOperators=new sets.Set(['===','!==','==','!=']);var unaryPrefixOperators=new sets.Set(['!','++','--','-','~','typeof']); -var unaryPostfixOperators=new sets.Set(['++','--']); -var unaryOperators=unaryPrefixOperators; -var bitwiseShiftOperators=new sets.Set(['>>','<<','>>>']); -var binaryBitwiseOperaters=new sets.Set(['&','|','^']); -var binaryLogicalOperators=new sets.Set(['||','^^','&&']); -var conditionalOperators=new sets.Set(['?']); -var propertyOperators=new sets.Set(['.']); -var assignmentOperators=new sets.Set(['=','+=','-=','*=','%=','&=','|=','^=','/=','<<=','>>=','>>>=']); -var operators=(new sets.Set()).unionUpdate( -arithmaticOperators).unionUpdate( -relationalOperators).unionUpdate( -equalityOperators).unionUpdate( -unaryOperators).unionUpdate( -bitwiseShiftOperators).unionUpdate( -binaryBitwiseOperaters).unionUpdate( -binaryLogicalOperators).unionUpdate( -propertyOperators).unionUpdate( -conditionalOperators).unionUpdate( -assignmentOperators); -var punctuators=(new sets.Set(['{','}','(',')','[',']',';',',',':'])).unionUpdate(operators); -var valueKeywords=new sets.Set(['null','undefined','true','false','this']); -var operatorKeywords=new sets.Set(['instanceof','typeof','new']); -var jsolaitStartStatementKeywords=new sets.Set(['Module','mod','publ']); -var startStatementKeywords=new sets.Set(['var','return','for','switch','while','continue','break','with','if','throw','delete','try','this','function']); -var subStatementKeywords=new sets.Set(['else','var','catch','case','default']); -var startStatementToken=startStatementKeywords.union(new sets.Set(['('])); -var keywords=(new sets.Set()).unionUpdate( -valueKeywords).unionUpdate( -operatorKeywords).unionUpdate( -startStatementKeywords).unionUpdate( -subStatementKeywords).unionUpdate( -startStatementToken); -var whiteSpace=/^[\s\t\f]+/; -var stringSQ=/^'((\\[^\x00-\x1f]|[^\x00-\x1f'\\])*)'/; -var stringDQ=/^"((\\[^\x00-\x1f]|[^\x00-\x1f"\\])*)"/; -var regExp=/^\/(\\[^\x00-\x1f]|\[(\\[^\x00-\x1f]|[^\x00-\x1f\\\/])*\]|[^\x00-\x1f\\\/\[])+\/[gim]*/;var identifiers=/^[a-zA-Z_$][\w_$]*\b/; -var intNumber=/^-?[1-9]\d*|0\b/; -var floatNumber=/^-?([1-9]\d*|0)\.\d+/; -var expNumber=/^-?([1-9]\d*|0)\.\d+e-?[1-9]\d*/; -var hexNumber=/^-?0x[0-9a-fA-F]+/; -mod.Tokenizer=Class(function(publ,supr){ -publ.__init__=function(s){ -this._working=s; -this.source=s; -}; -publ.next=function(){ -if(this._working==""){ -return undefined; -}var s1=this._working.charAt(0); -var s2=s1+this._working.charAt(1); -var s3=s2+this._working.charAt(2); -var isWS=false; -if(s1==" "||s1=="\t"||s1=="\f"){ -tkn=new mod.TokenWhiteSpace(whiteSpace.exec(this._working)[0]); -isWS=true; -}else if(s1=="\n"||s1=="\r"){ -tkn=new mod.TokenNewLine(s1); -}else if(s1=='"'||s1=="'"){ -if(tkn=(s1=="'"?stringSQ:stringDQ).exec(this._working)){ -tkn=new mod.TokenString(tkn[0]); -}else{ -throw "String expected"; -} -}else if(s3=="///"){ -tkn=new mod.TokenDocComment(extractSLComment(this._working),this.source.length-this._working.length); -}else if(s3=="/**"){ -tkn=new mod.TokenDocComment(extractMLComment(this._working),this.source.length-this._working.length); -}else if(s2=="//"){ -tkn=new mod.TokenComment(extractSLComment(this._working),this.source.length-this._working.length); -}else if(s2=="/*"){ -tkn=new mod.TokenComment(extractMLComment(this._working),this.source.length-this._working.length);}else if(punctuators.contains(s3)){ -tkn=new mod.TokenPunctuator(s3); -}else if(punctuators.contains(s2)){ -tkn=new mod.TokenPunctuator(s2); -}else if(punctuators.contains(s1)){ -if(s1=="/"&&(",(=+[{".indexOf(this._lastNonWSTkn.value)>-1)){ -if(tkn=regExp.exec(this._working)){ -tkn=new mod.TokenRegExp(tkn[0]); -}else{ -tkn=new mod.TokenPunctuator(s1);} -}else{ -tkn=new mod.TokenPunctuator(s1);} -}else if(tkn=identifiers.exec(this._working)){ -tkn=tkn[0]; -if(keywords.contains(tkn)){ -tkn=new mod.TokenKeyword(tkn); -}else{ -tkn=new mod.TokenIdentifier(tkn); -} -}else if(tkn=hexNumber.exec(this._working)){ -tkn=new mod.TokenNumber(tkn[0],this.source.length-this._working.length); -}else if(tkn=expNumber.exec(this._working)){ -tkn=new mod.TokenNumber(tkn[0],this.source.length-this._working.length); -}else if(tkn=floatNumber.exec(this._working)){ -tkn=new mod.TokenNumber(tkn[0],this.source.length-this._working.length); -}else if(tkn=intNumber.exec(this._working)){ -tkn=new mod.TokenNumber(tkn[0],this.source.length-this._working.length); -}else{ -throw "Unrecognized token at char %s, near:\n%s".format(this.source.length-this._working.length,this._working.slice(0,50));} -if(!isWS){ -this._lastNonWSTkn=tkn; -} -this._working=this._working.slice(tkn.value.length); -return tkn;}; -publ.nextNonWhiteSpace=function(newLineIsWS){ -while(tkn=this.next()){ -if(!(tkn instanceof mod.TokenWhiteSpace)){ -if(!(newLineIsWS&&(tkn instanceof mod.TokenNewLine))){ -break; -} -} -} -return tkn; -}; -publ.__iter__=function(){ -return new mod.Tokenizer(this.source); -}; -publ.getPosition=function(){ -var a=this.source.split("\n"); -var p=this.source.length-this._working.length; -for(var i=0;i0){ -this.pprintIndent+=indent; -} -}; -publ.pprintIndent=0; -publ.printGlobalNode=function(n){ -this.pprint('',4); -this.pprint('',4); -for(var i=0;i',-4); -this.pprint('',-4); -};publ.printModuleNode=function(n){ -this.pprint('',4); -this.pprint(''+n.name+''); -this.pprint('',4); -this.pprint(n.description); -this.pprint('',-4); -this.pprint(''+n.dependencies+''); -this.printPublics(n); -this.pprint('',-4); -}; -publ.printClassNode=function(n){ -this.pprint('',4); -this.pprint(''+n.name+''); -this.pprint('',4); -this.pprint(n.description); -this.pprint('',-4); -this.printPublics(n); -this.pprint('',-4); -}; -publ.printPublics=function(n){ -var classes=[]; -var props=[]; -var methods=[]; -for(var i=0;i0){ -this.pprint('',4); -if(classes.length>0){ -this.pprint('',4); -for(var i=0;i',-4); -} -if(methods.length>0){ -this.pprint('',4); -for(var i=0;i',-4); -} -if(props.length>0){ -this.pprint('',4); -for(var i=0;i',-4); -} -this.pprint('',-4); -} -}; -publ.printPropertyNode=function(n){ -this.pprint('',4); -this.pprint(''+n.name+''); -this.pprint('',4); -this.pprint(n.description); -this.pprint('',-4); -this.pprint('',-4); -}; -publ.printMethodNode=function(n){ -this.pprint('',4); -this.pprint(''+n.name+'('+n.parameters.join(', ')+')'); -this.pprint('',4); -this.pprint(n.description); -this.pprint('',-4); -this.pprint('',-4); -}; -}); -mod.__main__=function(){ -var it=imprt('iter'); -var c=imprt('codecs'); -var filenames=['jsolait.js','lib/codecs.js','lib/crypto.js', 'lib/dom.js', 'lib/forms.js', 'lib/iter.js', 'lib/jsonrpc.js', 'lib/lang.js', 'lib/sets.js', 'lib/testing.js', 'lib/urllib.js', 'lib/xml.js', 'lib/xmlrpc.js']; -var gn=new mod.GlobalNode(); -iter(filenames,function(fname){ -fname=jsolait.baseURI+'/'+fname; -var s=jsolait.loadURI(fname); -var p=new mod.Parser(s,gn); -try{ -p.parseStatements(p.next()); -}catch(e){ -var l=p.getPosition(); -throw new mod.Exception(fname.slice('file://'.length)+'('+(l[0])+','+l[1]+') '+e+' near:\n'+p._working.slice(0,200)); -}}); -}; -}); diff --git a/public_html/jsolait/lib/net/SocketProvider.as b/public_html/jsolait/lib/net/SocketProvider.as deleted file mode 100644 index 3748a9f..0000000 --- a/public_html/jsolait/lib/net/SocketProvider.as +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright (c) 2004-2006 Jan-Klaas Kollhof - - This file is part of the JavaScript O Lait library(jsolait). - - jsolait is free software; you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation; either version 2.1 of the License, or - (at your option) any later version. - - This software is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with this software; if not, write to the Free Software - Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - -import flash.external.ExternalInterface; - -class SocketProvider { - - static var app : SocketProvider; - static var sockets = []; - - public function SocketProvider() { - ExternalInterface.addCallback("newSocket", null, newSocket); - ExternalInterface.addCallback("connect", null, connect); - ExternalInterface.addCallback("send", null, send); - var x:Boolean=ExternalInterface.addCallback("close", null, close); - _root.createTextField("tf",0,0,0,100,30); - _root.tf.text = "jsolait SocketProvider " + (x==true?'OK': 'initialization failed'); - ExternalInterface.call("imprt('net.sockets').handleFlashMessage", "", "socketProviderLoaded" ); - } - - public function newSocket() : String { - var id =0; - while(sockets['s' + id] != null){ - id ++ - } - id = 's' + id; - - var s:XMLSocket = new XMLSocket(); - sockets[id] = s; - - s.onData = function(data){ - data=data.split('\r').join('\\r'); - data=data.split('\n').join('\\n'); - ExternalInterface.call("imprt('net.sockets').handleFlashMessage", id, "onData", data); - } - s.onConnect = function(succsess){ - ExternalInterface.call("imprt('net.sockets').handleFlashMessage", id, "onConnect", succsess); - } - s.onClose = function(){ - ExternalInterface.call("imprt('net.sockets').handleFlashMessage", id, "onClose"); - } - return id; - } - - public function connect(id : String, host : String, port : String){ - sockets[id].connect(host, port); - } - - public function send(id : String, data : String){ - sockets[id].send(data); - } - - public function close(id:String){ - sockets[id].close(); - delete sockets[id]; - } - - // entry point - static function main(mc) { - app = new SocketProvider(); - } -} - - diff --git a/public_html/jsolait/lib/net/SocketProvider.swf b/public_html/jsolait/lib/net/SocketProvider.swf deleted file mode 100644 index 5d03bc1..0000000 Binary files a/public_html/jsolait/lib/net/SocketProvider.swf and /dev/null differ diff --git a/public_html/jsolait/lib/net/sockets.js b/public_html/jsolait/lib/net/sockets.js deleted file mode 100644 index 4c6bf83..0000000 --- a/public_html/jsolait/lib/net/sockets.js +++ /dev/null @@ -1,78 +0,0 @@ - -Module("net.sockets","0.0.1",function(mod){ -var flashSocketProvider; -var flashSocketWrappers={}; -mod.prepare=function(){ -addFlashToPage(); -}; -mod.createSocket=function(){ -return new mod.FlashSocket(); -}; -mod.Socket=Class(function(publ,priv,supr){ -publ.connect=function(host,port){}; -publ.send=function(data){}; -publ.close=function(){}; -publ.onConnect=function(success){}; -publ.onData=function(data){}; -publ.onClose=function(){}; -}); -mod.FlashSocket=Class(mod.Socket,function(publ,priv,supr){ -publ.connect=function(host,port){ -if(this.id!=null){ -this.close(); -}else{ -this.id=flashSocketProvider.newSocket(); -flashSocketWrappers[this.id]=this; -} -flashSocketProvider.connect(this.id,host,port); -}; -publ.send=function(data){ -if(this.id!=null){ -flashSocketProvider.send(this.id,data); -}else{ -throw new mod.Exception("Socket not connected"); -} -}; -publ.close=function(){ -flashSocketProvider.close(this.id); -delete this['id']; -}; -}); -var ie=''; -var moz=''; -var addFlashToPage=function(){ -var url=jsolait.baseURI+"/lib/net/SocketProvider.swf"; -if(navigator.appName.indexOf("Microsoft")!=-1){ -document.getElementsByTagName('body')[0].innerHTML+=ie.format(url); -}else{ -var d=document.createElement('embed'); -d.setAttribute("src",url); -d.setAttribute("width","0"); -d.setAttribute("height","0"); -d.setAttribute("style","visibility:hidden;"); -d.setAttribute("type","application/x-shockwave-flash"); -d.setAttribute("allowScriptAccess","sameDomain"); -d.setAttribute("id","__SocketProvider__"); -document.documentElement.appendChild(d); -flashSocketProvider=d; -} -}; -mod.handleFlashMessage=function(id,type,data,data2){ -if(type=="socketProviderLoaded"){ -if(flashSocketProvider==null){ -flashSocketProvider=document.getElementById("__SocketProvider__"); -} -}else{ -var s=flashSocketWrappers[id]; -if(s[type]!=null){ -s[type].call(s,data,data2); -} -if(type=="onClose"){ -delete flashSocketWrappers[id]; -} -} -}; -mod.isReady=function(){ -return flashSocketProvider!=null; -}; -}); diff --git a/public_html/jsolait/lib/operators.js b/public_html/jsolait/lib/operators.js deleted file mode 100644 index c1489c6..0000000 --- a/public_html/jsolait/lib/operators.js +++ /dev/null @@ -1,103 +0,0 @@ - -Module("operators","$Revision: 20 $",function(mod){ -mod.lt=function(a,b){ -if((a!=null)&&(a.__lt__!==undefined)){ -return a.__lt__(b); -}else if((b!=null)&&(b.__lt__!==undefined)){ -return b.__lt__(a); -}else{ -return a=b; -} -}; mod.gt=function(a,b){ -if((a!=null)&&(a.__gt__!==undefined)){ -return a.__gt__(b); -}else if((b!=null)&&(b.__gt__!==undefined)){ -return b.__gt__(a); -}else{ -return a>b; -} -}; mod.not=function(a){ -if((a!=null)&&(a.__not__!==undefined)){ -return a.__not__(); -}else{ -return!a; -} -}; -Array.prototype.__eq__=function(a){ -if(this.length!=a.length){ -return false; -}else{ -for(var i=0;i1){ -for(var i=0;iy){ -return 1; -}else if(x==null&&y==null){ -return 0; -} -} -}; -mod.WritableString=Class(Array,function(publ,supr){ -publ.__init__=function(value){ -value=value==null?"":value; -if(value!=""){ -this.write(value); -} -}; -publ.write=Array.prototype.push; -publ.__str__=function(){ -return this.join(""); -}; -publ.__repr__=function(){ -return repr(this.join("")); -}; -}); -mod.templateCodeStartDelimiter=""; -String.prototype.exec=function(locals,codeStartDelimiter,codeEndDelimiter){ -codeStartDelimiter=codeStartDelimiter==null?mod.templateCodeStartDelimiter:codeStartDelimiter; -codeEndDelimiter=codeEndDelimiter==null?mod.templateCodeEndDelimiter:codeEndDelimiter; -var s=this+""; -var code=[]; -var p,text; -while(s.length>0){ -var p=s.indexOf(codeStartDelimiter); -if(p>=0){ -text=s.slice(0,p); -code.push(';out.write("'+text.replace(/\\/g,"\\\\").replace(/\"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r")+'");'); -s=s.slice(p+codeStartDelimiter.length); -p=s.indexOf(codeEndDelimiter); -if(p>=0){ -text=s.slice(0,p); -s=s.slice(p+codeEndDelimiter.length); -if(text.slice(0,1)=="="){ -code.push(';out.write('+text.slice(1)+');'); -}else{ -code.push(text); -} -}else{ -throw mod.Exception("No code end dilimiter: '%s' found".format(codeEndDelimiter)); -} -}else{ -code.push(';out.write("'+s.replace(/\\/g,"\\\\").replace(/\"/g,"\\\"").replace(/\n/g,"\\n").replace(/\r/g,"\\r")+'");'); -s=""; -} -} -var sout=new mod.WritableString(); -var params=[sout]; -var paramNames=["out"]; -for(var key in locals){ -paramNames.push(key); -params.push(locals[key]); -} -try{ -var f=new Function(paramNames.join(","),code.join("")); -}catch(e){ -throw new mod.Exception("Error compiling template:\n\n%s".format(code.join("")),e); -} -f.apply(sout,params); -return str(sout); -}; -}); diff --git a/public_html/jsolait/lib/testing.js b/public_html/jsolait/lib/testing.js deleted file mode 100644 index 89d8d40..0000000 --- a/public_html/jsolait/lib/testing.js +++ /dev/null @@ -1,191 +0,0 @@ - -Module("testing","$Revision: 51 $",function(mod){ -var ops=imprt('operators'); -mod.minProfileTime=500; -mod.timeExec=function(repeat,fn){ -var args=[]; -for(var i=2;i\n"; -} break; case PROCESSING_INSTRUCTION_NODE: s+=""; break; case TEXT_NODE: s+=node.nodeValue; break; case CDATA_SECTION_NODE: s+="<"+"![CDATA["+node.nodeValue+"]"+"]>"; break; case COMMENT_NODE: s+=""; break; -case ENTITY_REFERENCE_NODE: case DOCUMENT_FRAGMENT_NODE: case DOCUMENT_TYPE_NODE: case NOTATION_NODE: case ENTITY_NODE: throw new mod.Exception("Nodetype(%s) not supported.".format(node.nodeType)); break; } return s; }; -}); diff --git a/public_html/jsolait/lib/xmlrpc.js b/public_html/jsolait/lib/xmlrpc.js deleted file mode 100644 index 4b6c8aa..0000000 --- a/public_html/jsolait/lib/xmlrpc.js +++ /dev/null @@ -1,530 +0,0 @@ - -Module("xmlrpc","$Revision: 56 $",function(mod){ -var xmlext=imprt("xml"); -var urllib=imprt("urllib"); -mod.InvalidServerResponse=Class(mod.Exception,function(publ,supr){ -publ.__init__=function(status){ -supr.__init__.call(this,"The server did not respond with a status 200 (OK) but with: "+status); -this.status=status; -}; -publ.status; -}); -mod.MalformedXmlRpc=Class(mod.Exception,function(publ,supr){ -publ.__init__=function(msg,xml,trace){ -supr.__init__.call(this,msg,trace); -this.xml=xml; -}; -publ.xml; -}); -mod.Fault=Class(mod.Exception,function(publ,supr){ -publ.__init__=function(faultCode,faultString){ -supr.__init__.call(this,"XML-RPC Fault: "+faultCode+"\n\n"+faultString); -this.faultCode=faultCode; -this.faultString=faultString; -}; -publ.faultCode; -publ.faultString; -}); -mod.marshall=function(obj){ -if(obj.toXmlRpc!=null){ -return obj.toXmlRpc(); -}else{ -var s=""; for(var attr in obj){ -if(typeof obj[attr]!="function"){ -s+=""+attr+""+mod.marshall(obj[attr])+""; -} } s+=""; -return s; -} -}; -mod.unmarshall=function(xml){ -try{ -var doc=xmlext.parseXML(xml); -}catch(e){ -throw new mod.MalformedXmlRpc("The server's response could not be parsed.",xml,e); -} -var rslt=mod.unmarshallDoc(doc,xml); -doc=null; -return rslt; -}; -mod.unmarshallDoc=function(doc,xml){ -try{ -var node=doc.documentElement; -if(node==null){ -throw new mod.MalformedXmlRpc("No documentElement found.",xml); -} -switch(node.tagName){ -case "methodResponse": -return parseMethodResponse(node); -case "methodCall": -return parseMethodCall(node); -default: -throw new mod.MalformedXmlRpc("'methodCall' or 'methodResponse' element expected.\nFound: '"+node.tagName+"'",xml); -} -}catch(e){ -if(e.constructor==mod.Fault){ -throw e; -}else{ -throw new mod.MalformedXmlRpc("Unmarshalling of XML failed.",xml,e); -} -} -}; -var parseMethodResponse=function(node){ -try{ -for(var i=0;i'; -if(args.length>0){ -data+=""; -for(var i=0;i'; } -data+=''; -} data+=''; -return data; -}; -publ.__init__=function(url,methodName,user,pass){ -this.methodName=methodName; -this.url=url; -this.user=user; -this.password=pass; -}; -publ.__call__=function(){ -if(typeof arguments[arguments.length-1]!='function'){ -var data=getXML(this.methodName,arguments); -var resp=postData(this.url,this.user,this.password,data); -return handleResponse(resp); -}else{ -var args=new Array(); -for(var i=0;i0){ -var tryIntrospection=false; -}else{ -var tryIntrospection=true; -} -}else{ -pass=user; -user=methodNames; -methodNames=[]; -var tryIntrospection=true; -} -this._url=url; -this._user=user; -this._password=pass; -this._addMethodNames(methodNames); -if(tryIntrospection){ -try{ -this._introspect(); -}catch(e){ -} -} -}; -publ._addMethodNames=function(methodNames){ -for(var i=0;i"+this.replace(/&/g,"&").replace(/"; }; -Number.prototype.toXmlRpc=function(){ if(this==parseInt(this)){ -return ""+this+""; }else if(this==parseFloat(this)){ return ""+this+""; }else{ return false.toXmlRpc(); } }; Boolean.prototype.toXmlRpc=function(){ if(this==true){ -return "1"; -}else{ -return "0"; -} }; Date.prototype.toXmlRpc=function(){ -var padd=function(s,p){ -s=p+s; -return s.substring(s.length-p.length); -}; -var y=padd(this.getUTCFullYear(),"0000"); -var m=padd(this.getUTCMonth()+1,"00"); -var d=padd(this.getUTCDate(),"00"); var h=padd(this.getUTCHours(),"00"); var min=padd(this.getUTCMinutes(),"00"); var s=padd(this.getUTCSeconds(),"00"); -var ms=padd(this.getUTCMilliseconds(),"000"); var isodate=y+m+d+"T"+h+":"+min+":"+s+":"+ms; -return ""+isodate+""; }; Array.prototype.toXmlRpc=function(){ var retstr=""; for(var i=0;i"; } return retstr+""; }; mod.__main__=function(){ -var s=new mod.ServiceProxy("http://jsolait.net/test.py",['echo']); -print("creating ServiceProxy object using introspection for method construction...\n"); -print("%s created\n".format(s)); -print("creating and marshalling test data:\n"); -var o=[1.234,5,{a:"Hello & < ",b:new Date()}]; -print(mod.marshall(o)); -print("\ncalling echo() on remote service...\n"); -var r=s.echo(o); -print("service returned data(marshalled again):\n"); -print(mod.marshall(r)); -}; -}); diff --git a/public_html/iphone.html b/public_html/mobile.html similarity index 100% rename from public_html/iphone.html rename to public_html/mobile.html diff --git a/qarnot/__init__.py b/qarnot/__init__.py new file mode 100644 index 0000000..5e6e58e --- /dev/null +++ b/qarnot/__init__.py @@ -0,0 +1,67 @@ +"""Rest API for submitting qarnot jobs in Python.""" + + +# Copyright 2016 Qarnot computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from qarnot.exceptions import QarnotGenericException + + +__all__ = ["task", "connection", "disk"] + + +def raise_on_error(response): + if response.status_code == 503: + raise QarnotGenericException("Service Unavailable") + if response.status_code != 200: + try: + raise QarnotGenericException(response.json()['message']) + except ValueError: + raise QarnotGenericException(response.text) + + +def get_url(key, **kwargs): + """Get and format the url for the given key. + """ + urls = { + 'disk folder': '/disks', # GET -> list; POST -> add + 'disk force': '/disks/force', # POST -> force add + 'disk info': '/disks/{name}', # DELETE -> remove; PUT -> update + 'get disk': '/disks/archive/{name}.{ext}', # GET-> disk archive + 'tree disk': '/disks/tree/{name}', # GET -> ls on the disk + 'link disk': '/disks/link/{name}', # POST -> create links + 'move disk': '/disks/move/{name}', # POST -> create links + 'ls disk': '/disks/list/{name}/{path}', # GET -> ls on the dir {path} + 'update file': '/disks/{name}/{path}', # POST -> add file; GET -> download file; DELETE -> remove file; PUT -> update file settings + 'tasks': '/tasks', # GET -> running tasks; POST -> submit task + 'task force': '/tasks/force', # POST -> force add + 'task update': '/tasks/{uuid}', # GET->result; DELETE -> abort, PATCH -> update resources + 'task snapshot': '/tasks/{uuid}/snapshot/periodic', # POST -> snapshots + 'task instant': '/tasks/{uuid}/snapshot', # POST -> get a snapshot + 'task stdout': '/tasks/{uuid}/stdout', # GET -> task stdout + 'task stderr': '/tasks/{uuid}/stderr', # GET -> task stderr + 'task abort': '/tasks/{uuid}/abort', # GET -> task stderr + 'user': '/info', # GET -> user info + 'profiles': '/profiles', # GET -> profiles list + 'profile details': '/profiles/{profile}' + # GET -> profile details + } + return urls[key].format(**kwargs) + +from qarnot.connection import Connection # noqa + +from ._version import get_versions # noqa +__version__ = get_versions()['version'] +del get_versions diff --git a/qarnot/_version.py b/qarnot/_version.py new file mode 100644 index 0000000..deb7ffc --- /dev/null +++ b/qarnot/_version.py @@ -0,0 +1,484 @@ + +# This file helps to compute a version number in source trees obtained from +# git-archive tarball (such as those provided by githubs download-from-tag +# feature). Distribution tarballs (built by setup.py sdist) and build +# directories (produced by setup.py build) will contain a much shorter file +# that just contains the computed version number. + +# This file is released into the public domain. Generated by +# versioneer-0.16 (https://github.com/warner/python-versioneer) + +"""Git implementation of _version.py.""" + +import errno +import os +import re +import subprocess +import sys + + +def get_keywords(): + """Get the keywords needed to look up the version information.""" + # these strings will be replaced by git during git-archive. + # setup.py/versioneer.py will grep for the variable names, so they must + # each be defined on a line of their own. _version.py will just call + # get_keywords(). + git_refnames = " (HEAD -> master)" + git_full = "2475844799bf97b90bf0a60e38345b1a1fcd3ce3" + keywords = {"refnames": git_refnames, "full": git_full} + return keywords + + +class VersioneerConfig: + """Container for Versioneer configuration parameters.""" + + +def get_config(): + """Create, populate and return the VersioneerConfig() object.""" + # these strings are filled in when 'setup.py versioneer' creates + # _version.py + cfg = VersioneerConfig() + cfg.VCS = "git" + cfg.style = "pep440" + cfg.tag_prefix = "" + cfg.parentdir_prefix = "qarnot-" + cfg.versionfile_source = "qarnot/_version.py" + cfg.verbose = False + return cfg + + +class NotThisMethod(Exception): + """Exception raised if a method is not valid for the current scenario.""" + + +LONG_VERSION_PY = {} +HANDLERS = {} + + +def register_vcs_handler(vcs, method): # decorator + """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): + """Store f in HANDLERS[vcs][method].""" + if vcs not in HANDLERS: + HANDLERS[vcs] = {} + HANDLERS[vcs][method] = f + return f + return decorate + + +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): + """Call the given command(s).""" + assert isinstance(commands, list) + p = None + for c in commands: + try: + dispcmd = str([c] + args) + # remember shell=False, so use git.cmd on windows, not just git + p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None)) + break + except EnvironmentError: + e = sys.exc_info()[1] + if e.errno == errno.ENOENT: + continue + if verbose: + print("unable to run %s" % dispcmd) + print(e) + return None + else: + if verbose: + print("unable to find command, tried %s" % (commands,)) + return None + stdout = p.communicate()[0].strip() + if sys.version_info[0] >= 3: + stdout = stdout.decode() + if p.returncode != 0: + if verbose: + print("unable to run %s (error)" % dispcmd) + return None + return stdout + + +def versions_from_parentdir(parentdir_prefix, root, verbose): + """Try to determine the version from the parent directory name. + + Source tarballs conventionally unpack into a directory that includes + both the project name and a version string. + """ + dirname = os.path.basename(root) + if not dirname.startswith(parentdir_prefix): + if verbose: + print("guessing rootdir is '%s', but '%s' doesn't start with " + "prefix '%s'" % (root, dirname, parentdir_prefix)) + raise NotThisMethod("rootdir doesn't start with parentdir_prefix") + return {"version": dirname[len(parentdir_prefix):], + "full-revisionid": None, + "dirty": False, "error": None} + + +@register_vcs_handler("git", "get_keywords") +def git_get_keywords(versionfile_abs): + """Extract version information from the given file.""" + # the code embedded in _version.py can just fetch the value of these + # keywords. When used from setup.py, we don't want to import _version.py, + # so we do it with a regexp instead. This function is not used from + # _version.py. + keywords = {} + try: + f = open(versionfile_abs, "r") + for line in f.readlines(): + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + f.close() + except EnvironmentError: + pass + return keywords + + +@register_vcs_handler("git", "keywords") +def git_versions_from_keywords(keywords, tag_prefix, verbose): + """Get version information from git keywords.""" + if not keywords: + raise NotThisMethod("no keywords at all, weird") + refnames = keywords["refnames"].strip() + if refnames.startswith("$Format"): + if verbose: + print("keywords are unexpanded, not using") + raise NotThisMethod("unexpanded keywords, not a git-archive tarball") + refs = set([r.strip() for r in refnames.strip("()").split(",")]) + # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of + # just "foo-1.0". If we see a "tag: " prefix, prefer those. + TAG = "tag: " + tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + if not tags: + # Either we're using git < 1.8.3, or there really are no tags. We use + # a heuristic: assume all version tags have a digit. The old git %d + # expansion behaves like git log --decorate=short and strips out the + # refs/heads/ and refs/tags/ prefixes that would let us distinguish + # between branches and tags. By ignoring refnames without digits, we + # filter out many common branch names like "release" and + # "stabilization", as well as "HEAD" and "master". + tags = set([r for r in refs if re.search(r'\d', r)]) + if verbose: + print("discarding '%s', no digits" % ",".join(refs-tags)) + if verbose: + print("likely tags: %s" % ",".join(sorted(tags))) + for ref in sorted(tags): + # sorting will prefer e.g. "2.0" over "2.0rc1" + if ref.startswith(tag_prefix): + r = ref[len(tag_prefix):] + if verbose: + print("picking %s" % r) + return {"version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": None + } + # no suitable tags, so version is "0+unknown", but full hex is still there + if verbose: + print("no suitable tags, using unknown + full revision id") + return {"version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, "error": "no suitable tags"} + + +@register_vcs_handler("git", "pieces_from_vcs") +def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): + """Get version from 'git describe' in the root of the source tree. + + This only gets called if the git-archive 'subst' keywords were *not* + expanded, and _version.py hasn't already been rewritten with a short + version string, meaning we're inside a checked out source tree. + """ + if not os.path.exists(os.path.join(root, ".git")): + if verbose: + print("no .git in %s" % root) + raise NotThisMethod("no .git directory") + + GITS = ["git"] + if sys.platform == "win32": + GITS = ["git.cmd", "git.exe"] + # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] + # if there isn't one, this yields HEX[-dirty] (no NUM) + describe_out = run_command(GITS, ["describe", "--tags", "--dirty", + "--always", "--long", + "--match", "%s*" % tag_prefix], + cwd=root) + # --long was added in git-1.5.5 + if describe_out is None: + raise NotThisMethod("'git describe' failed") + describe_out = describe_out.strip() + full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + if full_out is None: + raise NotThisMethod("'git rev-parse' failed") + full_out = full_out.strip() + + pieces = {} + pieces["long"] = full_out + pieces["short"] = full_out[:7] # maybe improved later + pieces["error"] = None + + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] + # TAG might have hyphens. + git_describe = describe_out + + # look for -dirty suffix + dirty = git_describe.endswith("-dirty") + pieces["dirty"] = dirty + if dirty: + git_describe = git_describe[:git_describe.rindex("-dirty")] + + # now we have TAG-NUM-gHEX or HEX + + if "-" in git_describe: + # TAG-NUM-gHEX + mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + if not mo: + # unparseable. Maybe git-describe is misbehaving? + pieces["error"] = ("unable to parse git-describe output: '%s'" + % describe_out) + return pieces + + # tag + full_tag = mo.group(1) + if not full_tag.startswith(tag_prefix): + if verbose: + fmt = "tag '%s' doesn't start with prefix '%s'" + print(fmt % (full_tag, tag_prefix)) + pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" + % (full_tag, tag_prefix)) + return pieces + pieces["closest-tag"] = full_tag[len(tag_prefix):] + + # distance: number of commits since tag + pieces["distance"] = int(mo.group(2)) + + # commit: short hex revision ID + pieces["short"] = mo.group(3) + + else: + # HEX: no tags + pieces["closest-tag"] = None + count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], + cwd=root) + pieces["distance"] = int(count_out) # total number of commits + + return pieces + + +def plus_or_dot(pieces): + """Return a + if we don't already have one, else return a .""" + if "+" in pieces.get("closest-tag", ""): + return "." + return "+" + + +def render_pep440(pieces): + """Build up version string, with post-release "local version identifier". + + Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you + get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty + + Exceptions: + 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0+untagged.%d.g%s" % (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_pre(pieces): + """TAG[.post.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post.devDISTANCE + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += ".post.dev%d" % pieces["distance"] + else: + # exception #1 + rendered = "0.post.dev%d" % pieces["distance"] + return rendered + + +def render_pep440_post(pieces): + """TAG[.postDISTANCE[.dev0]+gHEX] . + + The ".dev0" means dirty. Note that .dev0 sorts backwards + (a dirty tree will appear "older" than the corresponding clean one), + but you shouldn't be releasing software with -dirty anyways. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + return rendered + + +def render_pep440_old(pieces): + """TAG[.postDISTANCE[.dev0]] . + + The ".dev0" means dirty. + + Eexceptions: + 1: no tags. 0.postDISTANCE[.dev0] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["dirty"]: + rendered += ".dev0" + return rendered + + +def render_git_describe(pieces): + """TAG[-DISTANCE-gHEX][-dirty]. + + Like 'git describe --tags --dirty --always'. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"]: + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render_git_describe_long(pieces): + """TAG-DISTANCE-gHEX[-dirty]. + + Like 'git describe --tags --dirty --always -long'. + The distance/hash is unconditional. + + Exceptions: + 1: no tags. HEX[-dirty] (note: no 'g' prefix) + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) + else: + # exception #1 + rendered = pieces["short"] + if pieces["dirty"]: + rendered += "-dirty" + return rendered + + +def render(pieces, style): + """Render the given version pieces into the requested style.""" + if pieces["error"]: + return {"version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"]} + + if not style or style == "default": + style = "pep440" # the default + + if style == "pep440": + rendered = render_pep440(pieces) + elif style == "pep440-pre": + rendered = render_pep440_pre(pieces) + elif style == "pep440-post": + rendered = render_pep440_post(pieces) + elif style == "pep440-old": + rendered = render_pep440_old(pieces) + elif style == "git-describe": + rendered = render_git_describe(pieces) + elif style == "git-describe-long": + rendered = render_git_describe_long(pieces) + else: + raise ValueError("unknown style '%s'" % style) + + return {"version": rendered, "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], "error": None} + + +def get_versions(): + """Get version information or return default if unable to do so.""" + # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have + # __file__, we can work backwards from there to the root. Some + # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which + # case we can only use expanded keywords. + + cfg = get_config() + verbose = cfg.verbose + + try: + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, + verbose) + except NotThisMethod: + pass + + try: + root = os.path.realpath(__file__) + # versionfile_source is the relative path from the top of the source + # tree (where the .git directory might live) to this file. Invert + # this to find the root from __file__. + for i in cfg.versionfile_source.split('/'): + root = os.path.dirname(root) + except NameError: + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree"} + + try: + pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) + return render(pieces, cfg.style) + except NotThisMethod: + pass + + try: + if cfg.parentdir_prefix: + return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) + except NotThisMethod: + pass + + return {"version": "0+unknown", "full-revisionid": None, + "dirty": None, + "error": "unable to compute version"} diff --git a/qarnot/connection.py b/qarnot/connection.py new file mode 100644 index 0000000..bb65fec --- /dev/null +++ b/qarnot/connection.py @@ -0,0 +1,512 @@ +"""Module describing a connection.""" + + +# Copyright 2016 Qarnot computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from qarnot import get_url, raise_on_error +from qarnot.disk import Disk +from qarnot.task import Task +from qarnot.exceptions import * +import requests +import sys +import warnings +import os +from json import dumps as json_dumps +from requests.exceptions import ConnectionError +if sys.version_info[0] >= 3: # module renamed in py3 + import configparser as config # pylint: disable=import-error +else: + import ConfigParser as config # pylint: disable=import-error + + +######### +# class # +######### + +class Connection(object): + """Represents the couple cluster/user to which submit tasks. + """ + def __init__(self, fileconf=None, client_token=None, cluster_url=None, cluster_unsafe=False, cluster_timeout=None): + """Create a connection to a cluster with given config file, options or environment variables. + Available environment variable are + `QARNOT_CLUSTER_URL`, `QARNOT_CLUSTER_UNSAFE`, `QARNOT_CLUSTER_TIMEOUT` and `QARNOT_CLIENT_TOKEN`. + + :param fileconf: path to a qarnot configuration file or a corresponding dict + :type fileconf: str or dict + :param str client_token: API Token + :param str cluster_url: (optional) Cluster url. + :param bool cluster_unsafe: (optional) Disable certificate check + :param int cluster_timeout: (optional) Timeout value for every request + + Configuration sample: + + .. code-block:: ini + + [cluster] + # url of the REST API + url=https://localhost + # No SSL verification ? + unsafe=False + [client] + # auth string of the client + token=login + + """ + self._http = requests.session() + + if fileconf is not None: + if isinstance(fileconf, dict): + warnings.warn("Dict config should be replaced by constructor explicit arguments.") + self.cluster = None + if fileconf.get('cluster_url'): + self.cluster = fileconf.get('cluster_url') + auth = fileconf.get('client_auth') + self.timeout = fileconf.get('cluster_timeout') + if fileconf.get('cluster_unsafe'): + self._http.verify = False + else: + cfg = config.ConfigParser() + with open(fileconf) as cfgfile: + cfg.readfp(cfgfile) + + self.cluster = None + if cfg.has_option('cluster', 'url'): + self.cluster = cfg.get('cluster', 'url') + + if cfg.has_option('client', 'token'): + auth = cfg.get('client', 'token') + elif cfg.has_option('client', 'auth'): + warnings.warn('auth is deprecated, use token instead.') + auth = cfg.get('client', 'auth') + else: + auth = None + self.timeout = None + if cfg.has_option('cluster', 'timeout'): + self.timeout = cfg.getint('cluster', 'timeout') + if cfg.has_option('cluster', 'unsafe') \ + and cfg.getboolean('cluster', 'unsafe'): + self._http.verify = False + else: + self.cluster = cluster_url + self.timeout = cluster_timeout + self._http.verify = not cluster_unsafe + auth = client_token + + if self.cluster is None: + self.cluster = os.getenv("QARNOT_CLUSTER_URL") + + if auth is None: + auth = os.getenv("QARNOT_CLIENT_TOKEN") + + if os.getenv("QARNOT_CLUSTER_UNSAFE") is not None: + self._http.verify = not os.getenv("QARNOT_CLUSTER_UNSAFE") in ["true", "True", "1"] + + if os.getenv("QARNOT_CLUSTER_TIMEOUT") is not None: + self.timeout = int(os.getenv("QARNOT_CLUSTER_TIMEOUT")) + + if auth is None: + raise QarnotGenericException("Token is mandatory.") + self._http.headers.update({"Authorization": auth}) + + if self.cluster is None: + self.cluster = "https://api.qarnot.com" + resp = self._get('/') + raise_on_error(resp) + + def _get(self, url, **kwargs): + """Perform a GET request on the cluster. + + :param str url: + relative url of the file (according to the cluster url) + + :rtype: :class:`requests.Response` + :returns: The response to the given request. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: Additional keyword arguments are passed to the underlying + :func:`requests.Session.get`. + """ + while True: + try: + ret = self._http.get(self.cluster + url, timeout=self.timeout, + **kwargs) + if ret.status_code == 401: + raise UnauthorizedException() + return ret + except ConnectionError as exception: + + if str(exception) == "('Connection aborted.', BadStatusLine(\"\'\'\",))": + pass + else: + raise + + def _patch(self, url, json=None, **kwargs): + """perform a PATCH request on the cluster + + :param url: :class:`str`, + relative url of the file (according to the cluster url) + :param json: the data to json serialize and post + + :rtype: :class:`requests.Response` + :returns: The response to the given request. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: Additional keyword arguments are passed to the underlying + :attr:`requests.Session.post()`. + """ + while True: + try: + if json is not None: + if 'headers' not in kwargs: + kwargs['headers'] = dict() + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json_dumps(json) + ret = self._http.patch(self.cluster + url, + timeout=self.timeout, **kwargs) + if ret.status_code == 401: + raise UnauthorizedException() + return ret + except ConnectionError as exception: + if str(exception) == "('Connection aborted.', BadStatusLine(\"\'\'\",))": + pass + else: + raise + + def _post(self, url, json=None, *args, **kwargs): + """perform a POST request on the cluster + + :param url: :class:`str`, + relative url of the file (according to the cluster url) + :param json: the data to json serialize and post + + :rtype: :class:`requests.Response` + :returns: The response to the given request. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: Additional keyword arguments are passed to the underlying + :attr:`requests.Session.post()`. + """ + while True: + try: + if json is not None: + if 'headers' not in kwargs: + kwargs['headers'] = dict() + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json_dumps(json) + ret = self._http.post(self.cluster + url, + timeout=self.timeout, *args, **kwargs) + if ret.status_code == 401: + raise UnauthorizedException() + return ret + except ConnectionError as exception: + if str(exception) == "('Connection aborted.', BadStatusLine(\"\'\'\",))": + pass + else: + raise + + def _delete(self, url, **kwargs): + """Perform a DELETE request on the cluster. + + :param url: :class:`str`, + relative url of the file (according to the cluster url) + + :rtype: :class:`requests.Response` + :returns: The response to the given request. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: Additional keyword arguments are passed to the underlying + :attr:`requests.Session.delete()`. + """ + + while True: + try: + ret = self._http.delete(self.cluster + url, + timeout=self.timeout, **kwargs) + if ret.status_code == 401: + raise UnauthorizedException() + return ret + except ConnectionError as exception: + if str(exception) == "('Connection aborted.', BadStatusLine(\"\'\'\",))": + pass + else: + raise + + def _put(self, url, json=None, **kwargs): + """Performs a PUT on the cluster.""" + while True: + try: + if json is not None: + if 'headers' not in kwargs: + kwargs['headers'] = dict() + kwargs['headers']['Content-Type'] = 'application/json' + kwargs['data'] = json_dumps(json) + ret = self._http.put(self.cluster + url, + timeout=self.timeout, **kwargs) + if ret.status_code == 401: + raise UnauthorizedException() + return ret + except ConnectionError as exception: + if str(exception) == "('Connection aborted.', BadStatusLine(\"\'\'\",))": + pass + else: + raise + + @property + def user_info(self): + """Get information of the current user on the cluster. + + :rtype: :class:`UserInfo` + :returns: Requested information. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + resp = self._get(get_url('user')) + raise_on_error(resp) + ret = resp.json() + return UserInfo(ret) + + def disks(self): + """Get the list of disks on this cluster for this user. + + :rtype: List of :class:`~qarnot.disk.Disk`. + :returns: Disks on the cluster owned by the user. + + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + response = self._get(get_url('disk folder')) + raise_on_error(response) + disks = [Disk.from_json(self, data) for data in response.json()] + return disks + + def tasks(self): + """Get the list of tasks stored on this cluster for this user. + + :rtype: List of :class:`~qarnot.task.Task`. + :returns: Tasks stored on the cluster owned by the user. + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + response = self._get(get_url('tasks')) + raise_on_error(response) + return [Task.from_json(self, task) for task in response.json()] + + def retrieve_task(self, uuid): + """Retrieve a :class:`qarnot.task.Task` from its uuid + + :param str uuid: Desired task uuid + :rtype: :class:`~qarnot.task.Task` + :returns: Existing task defined by the given uuid + :raises qarnot.exceptions.MissingTaskException: task does not exist + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + + response = self._get(get_url('task update', uuid=uuid)) + if response.status_code == 404: + raise MissingTaskException(response.json()['message']) + raise_on_error(response) + return Task.from_json(self, response.json()) + + def retrieve_or_create_disk(self, description): + """Retrieve a :class:`~qarnot.disk.Disk` from its description, or create a new one. + + .. note:: Description are not unique, if multiple description match, an exception will be raised + + + :param str description: a short description of the disk + :rtype: :class:`~qarnot.disk.Disk` + :returns: Existing or newly created disk defined by the given description + :raises ValueError: no such disk + :raises qarnot.exceptions.MaxDiskException: disk quota reached + :raises qarnot.exceptions.MissingDiskException: disk does not exist + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + + disks = self.disks() + + matches = [d for d in disks if d.description == description] + matchcount = len(matches) + if matchcount == 0: + return self.create_disk(description) + elif matchcount == 1: + return matches[0] + else: + raise QarnotGenericException("No unique match for given description.") + + def retrieve_disk(self, uuid): + """Retrieve a :class:`~qarnot.disk.Disk` from its uuid + + :param str uuid: Desired disk uuid + :rtype: :class:`~qarnot.disk.Disk` + :returns: Existing disk defined by the given uuid + :raises ValueError: no such disk + :raises qarnot.exceptions.MissingDiskException: disk does not exist + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + + response = self._get(get_url('disk info', name=uuid)) + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + return Disk.from_json(self, response.json()) + + def create_disk(self, description, lock=False, tags=None): + """Create a new :class:`~qarnot.disk.Disk`. + + :param str description: a short description of the disk + :param bool lock: prevents the disk to be removed accidentally + :param list(str) tags: custom tags + + :rtype: :class:`qarnot.disk.Disk` + :returns: The created :class:`~qarnot.disk.Disk`. + + :raises qarnot.exceptions.MaxDiskException: disk quota reached + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + disk = Disk(self, description, lock=lock, tags=tags) + disk.create() + return disk + + def create_task(self, name, profile, instancecount_or_range=1): + """Create a new :class:`~qarnot.task.Task`. + + :param str name: given name of the task + :param str profile: which profile to use with this task + :param instancecount_or_range: number of instances, or ranges on which to run task. Defaults to 1. + :type instancecount_or_range: int or str + :rtype: :class:`~qarnot.task.Task` + :returns: The created :class:`~qarnot.task.Task`. + + .. note:: See available profiles with :meth:`profiles`. + """ + return Task(self, name, profile, instancecount_or_range) + + def profiles(self): + """Get list of profiles available on the cluster. + + :rtype: List of :class:`Profile` + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + + url = get_url('profiles') + response = self._get(url) + raise_on_error(response) + profiles_list = [] + for p in response.json(): + url = get_url('profile details', profile=p) + response2 = self._get(url) + if response2.status_code == 404: + continue + profiles_list.append(Profile(response2.json())) + return profiles_list + + def retrieve_profile(self, name): + """Get details of a profile from its name. + + :rtype: :class:`Profile` + + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + """ + + url = get_url('profile details', profile=name) + response = self._get(url) + raise_on_error(response) + if response.status_code == 404: + raise QarnotGenericException(response.json()['message']) + return Profile(response.json()) + + +################### +# utility Classes # +################### + +class UserInfo(object): + """Information about a qarnot user.""" + + def __init__(self, info): + self.email = info.get('email', '') + """:type: :class:`str` + + User email address.""" + + self.disk_count = info['diskCount'] + """:type: :class:`int` + + Number of disks owned by the user.""" + self.max_disk = info['maxDisk'] + """:type: :class:`int` + + Maximum number of disks allowed (resource and result disks).""" + self.quota_bytes = info['quotaBytes'] + """:type: :class:`int` + + Total storage space allowed for the user's disks (in Bytes).""" + self.used_quota_bytes = info['usedQuotaBytes'] + """:type: :class:`int` + + Total storage space used by the user's disks (in Bytes).""" + self.task_count = info['taskCount'] + """:type: :class:`int` + + Total number of tasks belonging to the user.""" + self.max_task = info['maxTask'] + """:type: :class:`int` + + Maximum number of tasks the user is allowed to create.""" + self.running_task_count = info['runningTaskCount'] + """:type: :class:`int` + + Number of tasks currently in 'Submitted' state.""" + self.max_running_task = info['maxRunningTask'] + """:type: :class:`int` + + Maximum number of running tasks.""" + self.max_instances = info['maxInstances'] + """:type: :class:`int` + + Maximum number of instances.""" + + +class Profile(object): + """Information about a profile.""" + def __init__(self, info): + self.name = info['name'] + """:type: :class:`str` + + Name of the profile.""" + self.constants = tuple((cst['name'], cst['value']) + for cst in info['constants']) + """:type: List of (:class:`str`, :class:`str`) + + List of couples (name, value) representing constants for this profile + and their default values.""" + + def __repr__(self): + return 'Profile(name=%s, constants=%r}' % (self.name, self.constants) diff --git a/qarnot/disk.py b/qarnot/disk.py new file mode 100644 index 0000000..6bc45fc --- /dev/null +++ b/qarnot/disk.py @@ -0,0 +1,1125 @@ +"""Module for disk object.""" + +# Copyright 2016 Qarnot computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import print_function + +from qarnot import get_url, raise_on_error +from qarnot.exceptions import * +import posixpath +import os +import os.path +import time +import hashlib +import datetime +import threading +import itertools + +try: + from progressbar import AnimatedMarker, Bar, ETA, Percentage, AdaptiveETA, ProgressBar, AdaptiveTransferSpeed +except: + pass + + +class Disk(object): + """Represents a resource/result disk on the cluster. + + This class is the interface to manage resources or results from a + :class:`qarnot.task.Task`. + + .. note:: + A :class:`Disk` must be created with + :meth:`qarnot.connection.Connection.create_disk` + or retrieved with :meth:`qarnot.connection.Connection.disks` or `qarnot.connection.Connection.retrieve_disk`. + + .. note:: + Paths given as 'remote' arguments, + (or as path arguments for :func:`Disk.directory`) + **must** be valid unix-like paths. + """ + + # Creation + def __init__(self, connection, description, lock=False, + tags=None): + """ + Create a disk on a cluster. + + :param :class:`qarnot.connection.Connection` connection: represents the cluster on which to create the disk + :param str description: a short description of the disk + :param bool lock: prevents the disk to be removed accidentally + :param list(str) tags: Custom tags + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + self._uuid = None + self._description = description + self._file_count = 0 + self._used_space_bytes = 0 + self._locked = lock + + self._connection = connection + self._filethreads = {} # A dictionary containing key:value where key is + # the remote destination on disk, and value a running thread. + self._filecache = {} # A dictionary containing key:value where key is + # the remote destination on disk, and value an opened Python File. + self._add_mode = UploadMode.blocking + self._tags = tags + self._auto_update = True + self._last_auto_update_state = self._auto_update + self._update_cache_time = 5 + self._last_cache = time.time() + + def create(self): + """Create the Disk on the REST API. + .. note:: This method should not be used unless if the object was created with the constructor. + """ + data = { + "description": self._description, + "locked": self._locked + } + if self._tags is not None: + data["tags"] = self._tags + response = self._connection._post(get_url('disk folder'), json=data) + if response.status_code == 403: + raise MaxDiskException(response.json()['message']) + else: + raise_on_error(response) + + self._uuid = response.json()['uuid'] + self.update() + + @classmethod + def _retrieve(cls, connection, disk_uuid): + """Retrieve information of a disk on a cluster. + + :param :class:`qarnot.connection.Connection` connection: the cluster + to get the disk from + :param str disk_uuid: the UUID of the disk to retrieve + + :rtype: :class:`qarnot.disk.Disk` + :returns: The retrieved disk. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + response = connection._get(get_url('disk info', name=disk_uuid)) + + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + + return cls.from_json(connection, response.json()) + + @classmethod + def from_json(cls, connection, json_disk): + """Create a Disk object from a json disk + + :param qarnot.connection.Connection connection: the cluster connection + :param dict json_disk: Dictionary representing the disk + """ + disk = cls(connection, + json_disk['description'], + lock=json_disk['locked'], + tags=json_disk.get('tags')) + disk._update(json_disk) + return disk + + # Disk Management + def update(self, flushcache=False): + """ + Update the disk object from the REST Api. + The flushcache parameter can be used to force the update, otherwise a cached version of the object + will be served when accessing properties of the object. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + if self._uuid is None: + return + + now = time.time() + if (now - self._last_cache) < self._update_cache_time and not flushcache: + return + + response = self._connection._get(get_url('disk info', name=self._uuid)) + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + + self._update(response.json()) + self._last_cache = time.time() + + def _update(self, json_disk): + """ Update local disk object from json + :type json_disk: dict + """ + self._uuid = json_disk["uuid"] + self._description = json_disk["description"] + self._file_count = json_disk["fileCount"] + self._used_space_bytes = json_disk["usedSpaceBytes"] + self._locked = json_disk["locked"] + self._file_count = json_disk["fileCount"] + self._used_space_bytes = json_disk["usedSpaceBytes"] + self._tags = json_disk.get("tags", None) + + def delete(self): + """Delete the disk represented by this :class:`Disk`. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + response = self._connection._delete( + get_url('disk info', name=self._uuid)) + + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + if response.status_code == 403: + raise LockedDiskException(response.json()['message']) + raise_on_error(response) + + def get_archive(self, extension='zip', local=None): + """Get an archive of this disk's content. + + :param str extension: in {'tar', 'tgz', 'zip'}, + format of the archive to get + :param str local: name of the file to output to + + :rtype: :class:`str` + :returns: + The filename of the retrieved archive. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises ValueError: invalid extension format + """ + response = self._connection._get( + get_url('get disk', name=self._uuid, ext=extension), + stream=True) + + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + elif response.status_code == 400: + raise ValueError('invalid file format : {0}', extension) + else: + raise_on_error(response) + + local = local or ".".join([self._uuid, extension]) + if os.path.isdir(local): + local = os.path.join(local, ".".join([self._uuid, extension])) + + with open(local, 'wb') as f_local: + for elt in response.iter_content(): + f_local.write(elt) + return local + + def list_files(self): + """List files on the whole disk. + + :rtype: List of :class:`FileInfo`. + :returns: List of the files on the disk. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + + self.flush() + response = self._connection._get( + get_url('tree disk', name=self._uuid)) + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + return [FileInfo(**f) for f in response.json()] + + def directory(self, directory=''): + """List files in a directory of the disk. Doesn't go through + subdirectories. + + :param str directory: path of the directory to inspect. + Must be unix-like. + + :rtype: List of :class:`FileInfo`. + :returns: Files in the given directory on the :class:`Disk`. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: + Paths in results are relative to the *directory* argument. + """ + + self.flush() + + response = self._connection._get( + get_url('ls disk', name=self._uuid, path=directory)) + if response.status_code == 404: + if response.json()['message'] == 'no such disk': + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + return [FileInfo(**f) for f in response.json()] + + def sync_directory(self, directory, verbose=False): + """Synchronize a local directory with the remote disks. + + :param str directory: The local directory to use for synchronization + :param bool verbose: Print information about synchronization operations + + .. warning:: + Local changes are reflected on the server, a file present on the + disk but not in the local directory will be deleted from the disk. + + A file present in the directory but not in the disk will be uploaded. + + .. note:: + The following parameters are used to determine whether + synchronization is required : + + * name + * size + * sha1sum + """ + if not directory.endswith('/'): + directory = directory + '/' + + filesdict = {} + for root, dirs, files in os.walk(directory): + for file_ in files: + filepath = os.path.join(root, file_) + name = filepath[len(directory) - 1:] + filesdict[name] = filepath + for dir_ in dirs: + filepath = os.path.join(root, dir_) + name = filepath[len(directory) - 1:] + if not name.endswith('/'): + name += '/' + filesdict[name] = filepath + + self.sync_files(filesdict, verbose) + + def sync_files(self, files, verbose=False, ignore_directories=False): + """Synchronize files with the remote disks. + + :param dict files: Dictionary of synchronized files + :param bool verbose: Print information about synchronization operations + :param bool ignore_directories: Ignore directories when looking for changes + + Dictionary key is the remote file path while value is the local file + path. + + .. warning:: + Local changes are reflected on the server, a file present on the + disk but + not in the local directory will be deleted from the disk. + + A file present in the directory but not in the disk will be uploaded. + + .. note:: + The following parameters are used to determine whether + synchronization is required : + + * name + * size + * sha1sum + """ + def generate_file_sha1(filepath, blocksize=2**20): + """Generate SHA1 from file""" + sha1 = hashlib.sha1() + with open(filepath, "rb") as file_: + while True: + buf = file_.read(blocksize) + if not buf: + break + sha1.update(buf) + return sha1.hexdigest() + + def create_qfi(name, filepath): + """Create a QFI from a file""" + if not name.startswith('/'): + name = '/' + name + mtime = os.path.getmtime(filepath) + dtutc = datetime.datetime.utcfromtimestamp(mtime) + dtutc = dtutc.replace(microsecond=0) + + type = 'directory' if os.path.isdir(filepath) else 'file' + sha1 = generate_file_sha1(filepath) if type is 'file' else 'N/A' + size = os.stat(filepath).st_size if type is 'file' else 0 + qfi = FileInfo(dtutc, name, size, type, sha1) + qfi.filepath = filepath + return qfi + + localfiles = [] + for name, filepath in files.items(): + qfi = create_qfi(name, filepath) + localfiles.append(qfi) + + if ignore_directories: + local = set([x for x in localfiles if not x.directory]) + remote = set([x for x in self.list_files() if not x.directory]) + else: + local = set(localfiles) + remote = set(self.list_files()) + adds = local - remote + removes = remote - local + + sadds = sorted(adds, key=lambda x: x.sha1sum) + groupedadds = [list(g) for _, g in itertools.groupby( + sadds, lambda x: x.sha1sum)] + + for file_ in removes: + renames = [x for x in adds if x.sha1sum == file_.sha1sum and not x.directory and not file_.directory] + if len(renames) > 0: + for dup in renames: + if verbose: + print("Copy", file_.name, "to", dup.name) + self.add_link(file_.name, dup.name) + if verbose: + print("remove ", file_.name) + self.delete_file(file_.name, force=True) + + remote = self.list_files() + + for entry in groupedadds: + try: + rem = next(x for x in remote if x.sha1sum == entry[0].sha1sum and not x.directory and not entry[0].directory) + if rem.name == entry[0].name: + continue + if verbose: + print("Link:", rem.name, "<-", entry[0].name) + self.add_link(rem.name, entry[0].name) + except StopIteration: + if verbose: + print("Upload:", entry[0].name) + self.add_file(entry[0].filepath, entry[0].name) + if len(entry) > 1: # duplicate files + for link in entry[1:]: + if not link.directory: + if verbose: + print("Link:", entry[0].name, "<-", link.name) + self.add_link(entry[0].name, link.name) + else: + if verbose: + print("Add dir" + link.filepath + " " + str(link.name)) + self.add_file(link.filepath, link.name) + + def flush(self): + """Ensure all files added through :meth:`add_file`/:meth:`add_directory` + are on the disk. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises TypeError: trying to write on a R/O disk + :raises IOError: user space quota reached + """ + for thread in self._filethreads.values(): + thread.join() + + self._filethreads.clear() + + for remote, file_ in self._filecache.items(): + self._add_file(file_, remote) + + self._filecache.clear() + + def move(self, source, dest): + """Move a file or a directory inside a disk. + Missing destination path directories can be created. + Trailing '/' for directories affect behavior. + + :param str source: name of the source file + :param str dest: name of the destination file + + .. warning:: + No clobber on move. If dest exist move will fail. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + + data = [ + { + "source": source, + "dest": dest + } + ] + url = get_url('move disk', name=self._uuid) + response = self._connection._post(url, json=data) + + raise_on_error(response) + self.update(True) + + def add_link(self, target, linkname): + """Create link between files on the disk + + :param str target: name of the existing file to duplicate + :param str linkname: name of the created file + + .. warning:: + File size is counted twice, this method is meant to save upload + time, not space. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + data = [ + { + "target": target, + "linkName": linkname + } + ] + url = get_url('link disk', name=self._uuid) + response = self._connection._post(url, json=data) + + raise_on_error(response) + self.update(True) + + def _is_executable(self, file): + try: + return os.access(file.name, os.X_OK) + except IOError: + return False + + def add_file(self, local_or_file, remote=None, mode=None, **kwargs): + """Add a local file or a Python File on the disk. + + .. note:: + You can also use **disk[remote] = local** + + .. warning:: + In non blocking mode, you may receive an exception during an other + operation (like :meth:`flush`). + + :param local_or_file: path of the local file or an opened Python File + :type local_or_file: str or File + :param str remote: name of the remote file + (defaults to *local_or_file*) + :param mode: mode with which to add the file + (defaults to :attr:`~UploadMode.blocking` if not set by + :attr:`Disk.add_mode`) + :type mode: :class:`UploadMode` + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises TypeError: trying to write on a R/O disk + :raises IOError: user space quota reached + :raises ValueError: file could not be created + """ + mode = mode or self._add_mode + + if isinstance(local_or_file, str): + if os.path.isdir(local_or_file): + dest = remote or os.path.basename(local_or_file) + url = get_url('update file', name=self._uuid, path=os.path.dirname(dest)) + response = self._connection._post( + url, + ) + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + return + else: + file_ = open(local_or_file, 'rb') + else: + file_ = local_or_file + + dest = remote or os.path.basename(file_.name) + if isinstance(dest, FileInfo): + dest = dest.name + + # Ensure 2 threads do not write on the same file + previous = self._filethreads.get(dest) + if previous is not None: + previous.join() + del self._filethreads[dest] + + # Do not delay a file added differently + if dest in self._filecache: + self._filecache[dest].close() + del self._filecache[dest] + + if mode is UploadMode.blocking: + return self._add_file(file_, dest, **kwargs) + elif mode is UploadMode.lazy: + self._filecache[dest] = file_ + else: + thread = threading.Thread(None, self._add_file, dest, (file_, dest), **kwargs) + thread.start() + self._filethreads[dest] = thread + + def _add_file(self, file_, dest, **kwargs): + """Add a file on the disk. + + :param File file_: an opened Python File + :param str dest: name of the remote file (defaults to filename) + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + """ + + try: + file_.seek(0) + except AttributeError: + pass + + if dest.endswith('/'): + dest = os.path.join(dest, os.path.basename(file_.name)) + url = get_url('update file', name=self._uuid, path=os.path.dirname(dest)) + + try: + # If requests_toolbelt is installed, we can use its + # MultipartEncoder to stream the upload and save memory overuse + from requests_toolbelt import MultipartEncoder # noqa + m = MultipartEncoder( + fields={'filedata': (os.path.basename(dest), file_)}) + response = self._connection._post( + url, + data=m, + headers={'Content-Type': m.content_type}) + except ImportError: + response = self._connection._post( + url, + files={'filedata': (os.path.basename(dest), file_)}) + + if response.status_code == 404: + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + + # Update file settings + if 'executable' not in kwargs: + kwargs['executable'] = self._is_executable(file_) + self.update_file_settings(dest, **kwargs) + self.update(True) + + def add_directory(self, local, remote="", mode=None): + """ Add a directory to the disk. Does not follow symlinks. + File hierarchy is preserved. + + .. note:: + You can also use **disk[remote] = local** + + .. warning:: + In non blocking mode, you may receive an exception during an other + operation (like :meth:`flush`). + + :param str local: path of the local directory to add + :param str remote: path of the directory on remote node + (defaults to *local*) + :param mode: the mode with which to add the directory + (defaults to :attr:`~Disk.add_mode`) + :type mode: :class:`UploadMode` + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises ValueError: one or more file(s) could not be created + :raises IOError: not a valid directory + """ + + if not os.path.isdir(local): + raise IOError("Not a valid directory") + if not remote.endswith('/'): + remote += '/' + for dirpath, _, files in os.walk(local): + remote_loc = dirpath.replace(local, remote, 1) + for filename in files: + self.add_file(os.path.join(dirpath, filename), + posixpath.join(remote_loc, filename), mode) + + def get_file_iterator(self, remote, chunk_size=4096, progress=None): + """Get a file iterator from the disk. + + .. note:: + This function is a generator, and thus can be used in a for loop + + :param remote: the name of the remote file or a QFileInfo + :type remote: str or FileInfo + :param int chunk_size: Size of chunks to be yield + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises ValueError: no such file + """ + + progressbar = None + + def _cb(c, total, remote): + c = max(0, min(c, 100)) + progressbar.update(c) + + if isinstance(remote, FileInfo): + remote = remote.name + + # Ensure file is done uploading + pending = self._filethreads.get(remote) + if pending is not None: + pending.join() + + if remote in self._filecache: + try: + self._filecache[remote].seek(0) + except AttributeError: + pass + while True: + chunk = self._filecache[remote].read(chunk_size) + if not chunk: + break + yield chunk + else: + response = self._connection._get( + get_url('update file', name=self._uuid, path=remote), + stream=True) + + if response.status_code == 404: + if response.json()['message'] == "No such disk": + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + + total_length = float(response.headers.get('content-length')) + if progress is not None: + if progress is True: + progress = _cb + try: + widgets = [ + remote, + ' ', Percentage(), + ' ', AnimatedMarker(), + ' ', Bar(), + ' ', AdaptiveETA(), + ' ', AdaptiveTransferSpeed(unit='B') + ] + progressbar = ProgressBar(widgets=widgets, max_value=total_length) + except Exception as e: + print(str(e)) + progress = None + elif progress is False: + progress = None + + count = 0 + for chunk in response.iter_content(chunk_size): + count += len(chunk) + if progress is not None: + progress(count, total_length, remote) + yield chunk + if progress: + progressbar.finish() + + def get_all_files(self, output_dir, progress=None): + """Get all files the disk. + + :param str output_dir: local directory for the retrieved files. + :param progress: can be a callback (read,total,filename) or True to display a progress bar + :type progress: bool or function(float, float, str) + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. warning:: Will override *output_dir* content. + + """ + + for file_info in self: + outpath = os.path.normpath(file_info.name.lstrip('/')) + self.get_file(file_info, os.path.join(output_dir, + outpath), progress) + + def get_file(self, remote, local=None, progress=None): + """Get a file from the disk. + + .. note:: + You can also use **disk[file]** + + :param remote: the name of the remote file or a QFileInfo + :type remote: str or FileInfo + :param str local: local name of the retrieved file (defaults to *remote*) + :param progress: can be a callback (read,total,filename) or True to display a progress bar + :type progress: bool or function(float, float, str) + :rtype: :class:`str` + :returns: The name of the output file. + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises ValueError: no such file + (:exc:`KeyError` with disk[file] syntax) + """ + + def make_dirs(_local): + """Make directory if needed""" + directory = os.path.dirname(_local) + if directory != '' and not os.path.exists(directory): + os.makedirs(directory) + + if isinstance(remote, FileInfo): + if remote.directory: + return + remote = remote.name + + if local is None: + local = os.path.basename(remote) + + if os.path.isdir(local): + local = os.path.join(local, os.path.basename(remote)) + + make_dirs(local) + + if os.path.isdir(local): + return + with open(local, 'wb') as f_local: + for chunk in self.get_file_iterator(remote, progress=progress): + f_local.write(chunk) + return local + + def update_file_settings(self, remote_path, **kwargs): + settings = dict(**kwargs) + + if len(settings) < 1: + return + + response = self._connection._put( + get_url('update file', name=self._uuid, path=remote_path), + json=settings) + + if response.status_code == 404: + if response.json()['message'] == "No such disk": + raise MissingDiskException(response.json()['message']) + raise_on_error(response) + + def delete_file(self, remote, force=False): + """Delete a file from the disk. + + .. note:: + You can also use **del disk[file]** + + :param str remote: the name of the remote file + :param bool force: ignore missing files + + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises ValueError: no such file + (:exc:`KeyError` with disk['file'] syntax) + + """ + dest = remote.name if isinstance(remote, FileInfo) else remote + + # Ensure 2 threads do not write on the same file + pending = self._filethreads.get(dest) + if pending is not None: + pending.join() + + # Remove the file from local cache if present + if dest in self._filecache: + self._filecache[dest].close() + del self._filecache[dest] + # The file is not present on the disk so just return + return + + response = self._connection._delete( + get_url('update file', name=self._uuid, path=dest)) + + if response.status_code == 404: + if response.json()['message'] == "No such disk": + raise MissingDiskException(response.json()['message']) + if force and response.status_code == 404: + pass + else: + raise_on_error(response) + self.update(True) + + def commit(self): + """Replicate local changes on the current object instance to the REST API + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: When updating disks' properties, auto update will be disabled until commit is called. + """ + data = { + "description": self._description, + "locked": self._locked + } + if self._tags is not None: + data["tags"] = self._tags + + self._auto_update = self._last_auto_update_state + resp = self._connection._put(get_url('disk info', name=self._uuid), + json=data) + if resp.status_code == 404: + raise MissingDiskException(resp.json()['message']) + raise_on_error(resp) + self.update(True) + + @property + def uuid(self): + """:type: :class:`str` + + :getter: Returns this disk's uuid + + The disk's UUID.""" + return self._uuid + + @property + def tags(self): + """:type: :class:list(`str`) + + :getter: Returns this disk's tags + :setter: Sets this disk's tags + + + Custom tags. + """ + if self._auto_update: + self.update() + + return self._tags + + @tags.setter + def tags(self, value): + self._tags = value + self._auto_update = False + + @property + def add_mode(self): + """:type: :class:`UploadMode` + + :getter: Returns this disk's add mode + :setter: Sets this disk's add mode + + + Default mode for adding files. + """ + return self._add_mode + + @add_mode.setter + def add_mode(self, value): + """Add mode setter""" + self._add_mode = value + + @property + def description(self): + """:type: :class:`str` + + :getter: Returns this disk's description + :setter: Sets this disk's description + + The disk's description. + """ + if self._auto_update: + self.update() + return self._description + + @description.setter + def description(self, value): + """Description setter""" + self._description = value + self._auto_update = False + + @property + def file_count(self): + """:type: :class:`int` + + :getter: Returns this disk's file count + + The number of files on the disk. + """ + if self._auto_update: + self.update() + return self._file_count + + @property + def used_space_bytes(self): + """:type: :class:`int` + + :getter: Returns this disk's used space in bytes + + The total space used on the disk in bytes. + """ + if self._auto_update: + self.update() + return self._used_space_bytes + + @property + def locked(self): + """:type: :class:`bool` + + :getter: Returns this disk's locked state + :setter: Sets this disk's locked state + + + The disk's lock state. If True, prevents the disk to be removed + by a subsequent :meth:`qarnot.connection.Connection.create_task` with *force* + set to True. + """ + if self._auto_update: + self.update() + return self._locked + + @locked.setter + def locked(self, value): + """Change disk's lock state.""" + self._locked = value + self._auto_update = False + + @property + def auto_update(self): + """:type: :class:`bool` + + :getter: Returns this disk's auto update state + :setter: Sets this disk's auto update state + + + Auto update state, default to True + When auto update is disabled properties will always return cached value + for the object and a call to :meth:`update` will be required to get latest values from the REST Api. + """ + return self._auto_update + + @auto_update.setter + def auto_update(self, value): + """Setter for auto_update feature + """ + self._auto_update = value + self._last_auto_update_state = self._auto_update + + def __str__(self): + return ( + ("[LOCKED] - " if self.locked else "[NON LOCKED] - ") + + self.uuid + " - " + self.description + ) + + # Operators + def __getitem__(self, filename): + """x.__getitem__(y) <==> x[y]""" + try: + return self.get_file(filename) + except ValueError: + raise KeyError(filename) + + def __setitem__(self, remote, filename): + """x.__setitem__(i, y) <==> x[i]=y""" + if os.path.isdir(filename): + return self.add_directory(filename, remote) + return self.add_file(filename, remote) + + def __delitem__(self, filename): + """x.__delitem__(y) <==> del x[y]""" + try: + return self.delete_file(filename) + except ValueError: + raise KeyError(filename) + + def __contains__(self, item): + """D.__contains__(k) -> True if D has a key k, else False""" + if isinstance(item, FileInfo): + item = item.name + return item in [f.name for f in self.list_files()] + + def __iter__(self): + """x.__iter__() <==> iter(x)""" + return iter(self.list_files()) + + def __eq__(self, other): + """x.__eq__(y) <==> x == y""" + if isinstance(other, self.__class__): + return self._uuid == other._uuid + return False + + def __ne__(self, other): + """x.__ne__(y) <==> x != y""" + return not self.__eq__(other) + + +# Utility Classes +class FileInfo(object): + """Information about a file.""" + def __init__(self, lastChange, name, size, fileFlags, sha1Sum): + + self.lastchange = None + """:type: :class:`datetime` + + UTC Last change time of the file on the :class:`Disk`.""" + + if isinstance(lastChange, datetime.datetime): + self.lastchange = lastChange + else: + self.lastchange = datetime.datetime.strptime(lastChange, + "%Y-%m-%dT%H:%M:%SZ") + + self.name = name + """:type: :class:`str` + + Path of the file on the :class:`Disk`.""" + self.size = size + """:type: :class:`int` + + Size of the file on the :class:`Disk` (in Bytes).""" + self.directory = fileFlags == 'directory' + """:type: :class:`bool` + + Is the file a directory.""" + + self.sha1sum = sha1Sum + """:type: :class:`str` + + SHA1 Sum of the file.""" + + if not self.directory: + self.executable = fileFlags == 'executableFile' + """:type: :class:`bool` + + Is the file executable.""" + + self.filepath = None # Only for sync + + def __repr__(self): + template = 'FileInfo(lastchange={0}, name={1}, size={2}, '\ + 'directory={3}, sha1sum={4})' + return template.format(self.lastchange, self.name, self.size, + self.directory, self.sha1sum) + + def __eq__(self, other): + return (self.name == other.name and + self.size == other.size and + self.directory == other.directory and + self.sha1sum == other.sha1sum) + + def __hash__(self): + return (hash(self.name) ^ + hash(self.size) ^ + hash(self.directory) ^ + hash(self.sha1sum)) + + +class UploadMode(object): + """How to add files on a :class:`Disk`.""" + blocking = 0 + """Call to :func:`~Disk.add_file` :func:`~Disk.add_directory` + or blocks until file is done uploading.""" + background = 1 + """Launch a background thread for uploading.""" + lazy = 2 + """Actual uploading is made by the :func:`~Disk.flush` method call.""" diff --git a/qarnot/exceptions.py b/qarnot/exceptions.py new file mode 100644 index 0000000..8c406dd --- /dev/null +++ b/qarnot/exceptions.py @@ -0,0 +1,67 @@ +"""Exceptions.""" + + +# Copyright 2016 Qarnot computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +__all__ = ['QarnotGenericException', + 'UnauthorizedException', + 'MissingTaskException', + 'MaxTaskException', + 'MissingDiskException', + 'MaxDiskException', + 'NotEnoughCreditsException', + 'LockedDiskException'] + + +class QarnotGenericException(Exception): + """General Connection exception""" + def __init__(self, msg): + super(QarnotGenericException, self).__init__("Error: {0}".format(msg)) + + +class UnauthorizedException(Exception): + """Invalid token.""" + pass + + +class MissingTaskException(Exception): + """Non existent task.""" + pass + + +class MaxTaskException(Exception): + """Max number of tasks reached.""" + pass + + +class MissingDiskException(Exception): + """Non existing disk.""" + pass + + +class MaxDiskException(Exception): + """Max number of disks reached.""" + pass + + +class NotEnoughCreditsException(Exception): + """Not enough credits exceptions.""" + pass + + +class LockedDiskException(Exception): + """Locked disk.""" + pass diff --git a/qarnot/task.py b/qarnot/task.py new file mode 100644 index 0000000..1e77b1e --- /dev/null +++ b/qarnot/task.py @@ -0,0 +1,1401 @@ +"""Module to handle a task.""" + +# Copyright 2016 Qarnot computing +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from os import makedirs, path +import time +import datetime +import warnings +import sys + +from qarnot import disk +from qarnot import get_url, raise_on_error +from qarnot.exceptions import * +try: + from progressbar import AnimatedMarker, Bar, ETA, Percentage, AdaptiveETA, ProgressBar +except: + pass + +RUNNING_DOWNLOADING_STATES = ['Submitted', 'PartiallyDispatched', + 'FullyDispatched', 'PartiallyExecuting', + 'FullyExecuting', 'DownloadingResults'] + + +class Task(object): + """Represents a Qarnot job. + + .. note:: + A :class:`Task` must be created with + :meth:`qarnot.connection.Connection.create_task` + or retrieved with :meth:`qarnot.connection.Connection.tasks` or :meth:`qarnot.connection.Connection.retrieve_task`. + """ + def __init__(self, connection, name, profile, instancecount_or_range): + """Create a new :class:`Task`. + + :param connection: the cluster on which to send the task + :type connection: :class:`qarnot.connection.Connection` + :param name: given name of the task + :type name: :class:`str` + :param str profile: which profile (payload) to use with this task + + :param instancecount_or_range: number of instances or ranges on which to run task + :type instancecount_or_range: int or str + """ + self._name = name + self._profile = profile + + if isinstance(instancecount_or_range, int): + self._instancecount = instancecount_or_range + self._advanced_range = None + else: + self._advanced_range = instancecount_or_range + self._instancecount = 0 + + self._resource_disks = [] + self._result_disk = None + self._connection = connection + self.constants = {} + """ + :type: :class:`dict(str,str)` + + Constants of the task. + + """ + + self._auto_update = True + self._last_auto_update_state = self._auto_update + self._update_cache_time = 5 + + self._last_cache = time.time() + """ + Dictionary [CST] = val. + + Can be set until :meth:`run` is called + + .. note:: See available constants for a specific profile + with :meth:`qarnot.connection.Connection.profile_info`. + """ + + self.constraints = {} + self._state = 'UnSubmitted' # RO property same for below + self._uuid = None + self._snapshots = False + self._dirty = False + self._rescount = -1 + self._snapshot_whitelist = None + self._snapshot_blacklist = None + self._results_whitelist = None + self._results_blacklist = None + self._status = None + self._tags = [] + self._creation_date = None + self._errors = None + self._resource_disks_uuids = [] + self._result_disk_uuid = None + + @classmethod + def _retrieve(cls, connection, uuid): + """Retrieve a submitted task given its uuid. + + :param qarnot.connection.Connection connection: + the cluster to retrieve the task from + :param str uuid: the uuid of the task to retrieve + + :rtype: Task + :returns: The retrieved task. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: no such task + """ + resp = connection._get(get_url('task update', uuid=uuid)) + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + raise_on_error(resp) + return Task.from_json(connection, resp.json()) + + def run(self, output_dir=None, job_timeout=None, live_progress=False, results_progress=None): + """Submit a task, wait for the results and download them if required. + + :param str output_dir: (optional) path to a directory that will contain the results + :param float job_timeout: (optional) Number of seconds before the task :meth:`abort` if it is not + already finished + :param bool live_progress: (optional) display a live progress + :param results_progress: (optional) can be a callback (read,total,filename) or True to display a progress bar + :type results_progress: bool or function(float, float, str) + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.MaxTaskException: Task quota reached + :raises qarnot.exceptions.NotEnoughCreditsException: Not enough credits + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingDiskException: + resource disk is not a valid disk + + .. note:: Will ensure all added file are on the resource disk + regardless of their uploading mode. + .. note:: If this function is interrupted (script killed for example), + but the task is submitted, the task will still be executed remotely + (results will not be downloaded) + .. warning:: Will override *output_dir* content. + """ + self.submit() + self.wait(timeout=job_timeout, live_progress=live_progress) + if job_timeout is not None: + self.abort() + if output_dir is not None: + self.download_results(output_dir, progress=results_progress) + + def resume(self, output_dir, job_timeout=None, live_progress=False, results_progress=None): + """Resume waiting for this task if it is still in submitted mode. + Equivalent to :meth:`wait` + :meth:`download_results`. + + :param str output_dir: path to a directory that will contain the results + :param float job_timeout: Number of seconds before the task :meth:`abort` if it is not + already finished + :param bool live_progress: display a live progress + :param results_progress: can be a callback (read,total,filename) or True to display a progress bar + :type results_progress: bool or function(float, float, str) + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + :raises qarnot.exceptions.MissingDiskException: + resource disk is not a valid disk + + .. note:: Do nothing if the task has not been submitted. + .. warning:: Will override *output_dir* content. + """ + if self._uuid is None: + return output_dir + self.wait(timeout=job_timeout, live_progress=live_progress) + self.download_results(output_dir, progress=results_progress) + + def submit(self): + """Submit task to the cluster if it is not already submitted. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.MaxTaskException: Task quota reached + :raises qarnot.exceptions.NotEnoughCreditsException: Not enough credits + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingDiskException: + resource disk is not a valid disk + + .. note:: Will ensure all added files are on the resource disk + regardless of their uploading mode. + + .. note:: To get the results, call :meth:`download_results` once the job is done. + """ + if self._uuid is not None: + return self._state + for rdisk in self.resources: + rdisk.flush() + payload = self._to_json() + resp = self._connection._post(get_url('tasks'), json=payload) + + if resp.status_code == 404: + raise disk.MissingDiskException(resp.json()['message']) + elif resp.status_code == 403: + if resp.json()['message'].startswith('Maximum number of disks reached'): + raise MaxDiskException(resp.json()['message']) + else: + raise MaxTaskException(resp.json()['message']) + elif resp.status_code == 402: + raise NotEnoughCreditsException(resp.json()['message']) + raise_on_error(resp) + self._uuid = resp.json()['uuid'] + + if not isinstance(self._snapshots, bool): + self.snapshot(self._snapshots) + + self.update(True) + + def abort(self): + """Abort this task if running. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + """ + self.update(True) + + resp = self._connection._post( + get_url('task abort', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + raise_on_error(resp) + + self.update(True) + + def update_resources(self): + """Update resources for a running task. Be sure to add new resources first. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + """ + + self.update(True) + resp = self._connection._patch( + get_url('task update', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + raise_on_error(resp) + + self.update(True) + + def delete(self, purge_resources=False, purge_results=False): + """Delete this task on the server. + + :param bool purge_resources: parameter value is used to determine if the disk is also deleted. + Defaults to False. + + :param bool purge_results: parameter value is used to determine if the disk is also deleted. + Defaults to False. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + """ + if self._uuid is None: + return + + if purge_resources: + rdisks = [] + for duuid in self._resource_disks_uuids: + try: + d = disk.Disk._retrieve(self._connection, duuid) + rdisks.append(d) + except disk.MissingDiskException as exception: + pass + + if purge_results: + try: + self.results.update() + except disk.MissingDiskException as exception: + purge_results = False + + resp = self._connection._delete( + get_url('task update', uuid=self._uuid)) + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + raise_on_error(resp) + + if purge_resources: + toremove = [] + for rdisk in rdisks: + try: + rdisk.update() + rdisk.delete() + toremove.append(rdisk) + except (disk.MissingDiskException, disk.LockedDiskException) as exception: + warnings.warn(str(exception)) + for tr in toremove: + rdisks.remove(tr) + self.resources = rdisks + + if purge_results: + try: + self._result_disk.delete() + self._result_disk = None + self._result_disk_uuid = None + except (disk.MissingDiskException, disk.LockedDiskException) as exception: + warnings.warn(str(exception)) + + self._state = "Deleted" + self._uuid = None + + def update(self, flushcache=False): + """ + Update the task object from the REST Api. + The flushcache parameter can be used to force the update, otherwise a cached version of the object + will be served when accessing properties of the object. + Some methods will flush the cache, like :meth:`submit`, :meth:`abort`, :meth:`wait` and :meth:`instant`. + Cache behavior is configurable with :attr:`auto_update` and :attr:`update_cache_time`. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not represent a + valid one + """ + if self._uuid is None: + return + + now = time.time() + if (now - self._last_cache) < self._update_cache_time and not flushcache: + return + + resp = self._connection._get( + get_url('task update', uuid=self._uuid)) + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + self._update(resp.json()) + self._last_cache = time.time() + + def _update(self, json_task): + """Update this task from retrieved info.""" + self._name = json_task['name'] + self._profile = json_task['profile'] + self._instancecount = json_task.get('instanceCount') + self._advanced_range = json_task.get('advancedRanges') + self._resource_disks_uuids = json_task['resourceDisks'] + if len(self._resource_disks_uuids) != len(self._resource_disks): + del self._resource_disks[:] + self._result_disk_uuid = json_task['resultDisk'] + if 'status' in json_task: + self._status = json_task['status'] + self._creation_date = datetime.datetime.strptime(json_task['creationDate'], "%Y-%m-%dT%H:%M:%SZ") + if 'errors' in json_task: + self._errors = [Error(d) for d in json_task['errors']] + else: + self._errors = [] + for constant in json_task['constants']: + self.constants[constant.get('key')] = constant.get('value') + self._uuid = json_task['uuid'] + self._state = json_task['state'] + self._tags = json_task.get('tags', None) + if self._rescount < json_task['resultsCount']: + self._dirty = True + self._rescount = json_task['resultsCount'] + if 'resultsBlacklist' in json_task: + self._results_blacklist = json_task['resultsBlacklist'] + if 'resultsWhitelist' in json_task: + self._results_whitelist = json_task['resultsWhitelist'] + if 'snapshotWhitelist' in json_task: + self._snapshot_whitelist = json_task['snapshotWhitelist'] + if 'snapshotBlacklist' in json_task: + self._snapshot_blacklist = json_task['snapshotBlacklist'] + + @classmethod + def from_json(cls, connection, json_task): + """Create a Task object from a json task. + + :param qarnot.connection.Connection connection: the cluster connection + :param dict json_task: Dictionary representing the task + :returns: The created :class:`~qarnot.task.Task`. + """ + if 'instanceCount' in json_task: + instancecount_or_range = json_task['instanceCount'] + else: + instancecount_or_range = json_task['advancedRanges'] + new_task = cls(connection, + json_task['name'], + json_task['profile'], + instancecount_or_range) + new_task._update(json_task) + return new_task + + def commit(self): + """Replicate local changes on the current object instance to the REST API + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. note:: When updating disks' properties, auto update will be disabled until commit is called. + """ + data = self._to_json() + resp = self._connection._put(get_url('task update', uuid=self._uuid), json=data) + self._auto_update = self._last_auto_update_state + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + + def wait(self, timeout=None, live_progress=False): + """Wait for this task until it is completed. + + :param float timeout: maximum time (in seconds) to wait before returning + (None => no timeout) + :param bool live_progress: display a live progress + + :rtype: :class:`bool` + :returns: Is the task finished + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not represent a valid + one + """ + + live_progress = live_progress and sys.stdout.isatty() + + if live_progress: + try: + widgets = [ + Percentage(), + ' ', AnimatedMarker(), + ' ', Bar(), + ' ', AdaptiveETA() + ] + progressbar = ProgressBar(widgets=widgets, max_value=100) + except Exception as e: + live_progress = False + + start = time.time() + if self._uuid is None: + self.update(True) + return False + + nap = min(10, timeout) if timeout is not None else 10 + + self.update(True) + while self._state in RUNNING_DOWNLOADING_STATES: + if live_progress: + n = 0 + progress = 0 + while True: + time.sleep(1) + n += 1 + if n >= nap: + break + progress = self.status.execution_progress if self.status is not None else 0 + progress = max(0, min(progress, 100)) + progressbar.update(progress) + else: + time.sleep(nap) + + self.update(True) + + if timeout is not None: + elapsed = time.time() - start + if timeout <= elapsed: + self.update() + return False + else: + nap = min(10, timeout - elapsed) + self.update(True) + if live_progress: + progressbar.finish() + return True + + def snapshot(self, interval): + """Start snapshooting results. + If called, this task's results will be periodically + updated, instead of only being available at the end. + + Snapshots will be taken every *interval* second from the time + the task is submitted. + + :param int interval: the interval in seconds at which to take snapshots + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not represent a + valid one + + .. note:: To get the temporary results, call :meth:`download_results`. + """ + if self._uuid is None: + self._snapshots = interval + return + resp = self._connection._post(get_url('task snapshot', uuid=self._uuid), + json={"interval": interval}) + + if resp.status_code == 400: + raise ValueError(interval) + elif resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + + self._snapshots = True + + def instant(self): + """Make a snapshot of the current task. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + + .. note:: To get the temporary results, call :meth:`download_results`. + """ + if self._uuid is None: + return + + resp = self._connection._post(get_url('task instant', uuid=self._uuid), + json=None) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + raise_on_error(resp) + + self.update(True) + + @property + def state(self): + """:type: :class:`str` + :getter: return this task's state + + State of the task. + + Value is in + * UnSubmitted + * Submitted + * PartiallyDispatched + * FullyDispatched + * PartiallyExecuting + * FullyExecuting + * DownloadingResults + * Cancelled + * Success + * Failure + + .. warning:: + this is the state of the task when the object was retrieved, + call :meth:`update` for up to date value. + """ + if self._auto_update: + self.update() + return self._state + + @property + def resources(self): + """:type: list(:class:`~qarnot.disk.Disk`) + :getter: Returns this task's resources disks + :setter: Sets this task's resources disks + + Represents resource files. + """ + if self._auto_update: + self.update() + + if not self._resource_disks: + for duuid in self._resource_disks_uuids: + d = disk.Disk._retrieve(self._connection, + duuid) + self._resource_disks.append(d) + + return self._resource_disks + + @resources.setter + def resources(self, value): + """This is a setter.""" + self._resource_disks = value + + @property + def results(self): + """:type: :class:`~qarnot.disk.Disk` + :getter: Returns this task's results disk + :setter: Sets this task's results disk + + Represents results files.""" + if self._result_disk is None: + self._result_disk = disk.Disk._retrieve(self._connection, + self._result_disk_uuid) + + if self._auto_update: + self.update() + + return self._result_disk + + @results.setter + def results(self, value): + """ This is a setter.""" + self._result_disk = value + + def download_results(self, output_dir, progress=None): + """Download results in given *output_dir*. + + :param str output_dir: local directory for the retrieved files. + :param progress: can be a callback (read,total,filename) or True to display a progress bar + :type progress: bool or function(float, float, str) + :raises qarnot.exceptions.MissingDiskException: the disk is not on the server + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + + .. warning:: Will override *output_dir* content. + + """ + + if self._uuid is not None: + self.update() + + if not path.exists(output_dir): + makedirs(output_dir) + + if self._dirty: + self.results.get_all_files(output_dir, progress=progress) + + def stdout(self): + """Get the standard output of the task + since the submission of the task. + + :rtype: :class:`str` + :returns: The standard output. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + + .. note:: The buffer is circular, if stdout is too big, prefer calling + :meth:`fresh_stdout` regularly. + """ + if self._uuid is None: + return "" + resp = self._connection._get( + get_url('task stdout', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + + return resp.text + + def fresh_stdout(self): + """Get what has been written on the standard output since last time + this function was called or since the task has been submitted. + + :rtype: :class:`str` + :returns: The new output since last call. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + """ + if self._uuid is None: + return "" + resp = self._connection._post( + get_url('task stdout', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + return resp.text + + def stderr(self): + """Get the standard error of the task + since the submission of the task. + + :rtype: :class:`str` + :returns: The standard error. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + + .. note:: The buffer is circular, if stderr is too big, prefer calling + :meth:`fresh_stderr` regularly. + """ + if self._uuid is None: + return "" + resp = self._connection._get( + get_url('task stderr', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + return resp.text + + def fresh_stderr(self): + """Get what has been written on the standard error since last time + this function was called or since the task has been submitted. + + :rtype: :class:`str` + :returns: The new error messages since last call. + + :raises qarnot.exceptions.QarnotGenericException: API general error, see message for details + :raises qarnot.exceptions.UnauthorizedException: invalid credentials + :raises qarnot.exceptions.MissingTaskException: task does not exist + """ + if self._uuid is None: + return "" + resp = self._connection._post( + get_url('task stderr', uuid=self._uuid)) + + if resp.status_code == 404: + raise MissingTaskException(resp.json()['message']) + + raise_on_error(resp) + return resp.text + + @property + def uuid(self): + """:type: :class:`str` + :getter: Returns this task's uuid + + The task's uuid. + + Automatically set when a task is submitted. + """ + return self._uuid + + @property + def name(self): + """:type: :class:`str` + :getter: Returns this task's name + :setter: Sets this task's name + + The task's name. + + Can be set until task is submitted. + """ + return self._name + + @name.setter + def name(self, value): + """Setter for name.""" + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + else: + self._name = value + + @property + def tags(self): + """:type: :class:list(`str`) + :getter: Returns this task's tags + :setter: Sets this task's tags + + Custom tags. + """ + if self._auto_update: + self.update() + + return self._tags + + @tags.setter + def tags(self, value): + self._tags = value + self._auto_update = False + + @property + def profile(self): + """:type: :class:`str` + :getter: Returns this task's profile + :setter: Sets this task's profile + + The profile to run the task with. + + Can be set until :meth:`run` is called. + """ + return self._profile + + @profile.setter + def profile(self, value): + """setter for profile""" + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + else: + self._profile = value + + @property + def instancecount(self): + """:type: :class:`int` + :getter: Returns this task's instance count + :setter: Sets this task's instance count + + Number of instances needed for the task. + + Can be set until :meth:`run` is called. + + :raises AttributeError: if :attr:`advanced_range` is not None when setting this property + + .. warning:: This property is mutually exclusive with :attr:`advanced_range` + """ + return self._instancecount + + @instancecount.setter + def instancecount(self, value): + """Setter for instancecount.""" + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + + if self.advanced_range is not None: + raise AttributeError("Can't set instancecount if advanced_range is not None") + self._instancecount = value + + @property + def advanced_range(self): + """:type: :class:`str` + :getter: Returns this task's advanced range + :setter: Sets this task's advanced range + + Advanced instances range selection. + + Allows to select which instances will be computed. + Should be None or match the following extended regular expression + """r"""**"([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))+"** + eg: 1,3-8,9,12-19 + + Can be set until :meth:`run` is called. + + :raises AttributeError: if :attr:`instancecount` is not 0 when setting this property + + .. warning:: This property is mutually exclusive with :attr:`instancecount` + """ + return self._advanced_range + + @advanced_range.setter + def advanced_range(self, value): + """Setter for advanced_range.""" + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + if self.instancecount != 0: + raise AttributeError("Can't set advanced_range if instancecount is not 0") + self._advanced_range = value + + @property + def snapshot_whitelist(self): + """:type: :class:`str` + :getter: Returns this task's snapshot whitelist + :setter: Sets this task's snapshot whitelist + + Snapshot white list (regex) for :meth:`snapshot` and :meth:`instant` + + Can be set until task is submitted. + """ + return self._snapshot_whitelist + + @snapshot_whitelist.setter + def snapshot_whitelist(self, value): + """Setter for snapshot whitelist, this can only be set before tasks submission + """ + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + self._snapshot_whitelist = value + + @property + def snapshot_blacklist(self): + """:type: :class:`str` + :getter: Returns this task's snapshot blacklist + :setter: Sets this task's snapshot blacklist + + Snapshot black list (regex) for :meth:`snapshot` :meth:`instant` + + Can be set until task is submitted. + """ + return self._snapshot_blacklist + + @snapshot_blacklist.setter + def snapshot_blacklist(self, value): + """Setter for snapshot blacklist, this can only be set before tasks submission + """ + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + self._snapshot_blacklist = value + + @property + def results_whitelist(self): + """:type: :class:`str` + :getter: Returns this task's results whitelist + :setter: Sets this task's results whitelist + + Results whitelist (regex) + + Can be set until task is submitted. + """ + return self._results_whitelist + + @results_whitelist.setter + def results_whitelist(self, value): + """Setter for results whitelist, this can only be set before tasks submission + """ + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + self._results_whitelist = value + + @property + def results_blacklist(self): + """:type: :class:`str` + :getter: Returns this task's results blacklist + :setter: Sets this task's results blacklist + + Results blacklist (regex) + + Can be set until task is submitted. + """ + if self._auto_update: + self.update() + + return self._results_blacklist + + @results_blacklist.setter + def results_blacklist(self, value): + """Setter for results blacklist, this can only be set before tasks submission + """ + if self.uuid is not None: + raise AttributeError("can't set attribute on a launched task") + self._results_blacklist = value + + @property + def status(self): + """:type: :class:`TaskStatus` + :getter: Returns this task's status + + Status of the task + """ + if self._auto_update: + self.update() + + if self._status: + return TaskStatus(self._status) + return self._status + + @property + def creation_date(self): + """:type: :class:`str` + + :getter: Returns this task's creation date + + Creation date of the task (UTC Time) + """ + return self._creation_date + + @property + def errors(self): + """:type: list(:class:`Error`) + :getter: Returns this task's errors if any. + + Error reason if any, empty string if none + """ + if self._auto_update: + self.update() + + return self._errors + + @property + def auto_update(self): + """:type: :class:`bool` + + :getter: Returns this task's auto update state + :setter: Sets this task's auto update state + + Auto update state, default to True + When auto update is disabled properties will always return cached value + for the object and a call to :meth:`update` will be required to get latest values from the REST Api. + """ + return self._auto_update + + @auto_update.setter + def auto_update(self, value): + """Setter for auto_update feature + """ + self._auto_update = value + self._last_auto_update_state = self._auto_update + + @property + def update_cache_time(self): + """:type: :class:`int` + + :getter: Returns this task's auto update state + :setter: Sets this task's auto update state + + Cache expiration time, default to 5s + """ + return self._update_cache_time + + @update_cache_time.setter + def update_cache_time(self, value): + """Setter for update_cache_time + """ + self._update_cache_time = value + + def _to_json(self): + """Get a dict ready to be json packed from this task.""" + const_list = [ + {'key': key, 'value': value} + for key, value in self.constants.items() + ] + constr_list = [ + {'key': key, 'value': value} + for key, value in self.constraints.items() + ] + + self._resource_disks_uuids = [x.uuid for x in self._resource_disks] + json_task = { + 'name': self._name, + 'profile': self._profile, + 'resourceDisks': self._resource_disks_uuids, + 'constants': const_list, + 'constraints': constr_list + } + + if self._result_disk is not None: + json_task['resultDisk'] = self._result_disk.uuid + + if self._advanced_range is not None: + json_task['advancedRanges'] = self._advanced_range + else: + json_task['instanceCount'] = self._instancecount + + json_task["tags"] = self._tags + + if self._snapshot_whitelist is not None: + json_task['snapshotWhitelist'] = self._snapshot_whitelist + if self._snapshot_blacklist is not None: + json_task['snapshotBlacklist'] = self._snapshot_blacklist + if self._results_whitelist is not None: + json_task['resultsWhitelist'] = self._results_whitelist + if self._results_blacklist is not None: + json_task['resultsBlacklist'] = self._results_blacklist + return json_task + + def __str__(self): + return '{0} - {1} - {2} - InstanceCount : {3} - {4} - Resources : {5} - Results : {6}'\ + .format(self.name, + self._uuid, + self._profile, + self._instancecount, + self.state, + (self._resource_disks_uuids if self._resource_disks is not None else ""), + (self._result_disk.uuid if self._result_disk is not None else "")) + + # Context manager + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if (exc_type is None) or exc_type != MissingTaskException: + self.delete() + return False + + +class Error(object): + """Task error + + .. note:: Read-only class + """ + def __init__(self, json): + self.code = json['code'] + """:type: :class:`str` + + Error code.""" + + self.message = json['message'] + """:type: :class:`str` + + Error message.""" + + self.debug = json['debug'] + """:type: :class:`str` + + Optional extra debug information""" + + def __str__(self): + if sys.version_info > (3, 0): + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.items()) + else: + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.iteritems()) # pylint: disable=no-member + + +# Status +class TaskStatus(object): + """Task status + + .. note:: Read-only class + """ + def __init__(self, json): + self.download_progress = json['downloadProgress'] + """:type: :class:`float` + + Resources download progress to the instances.""" + + self.execution_progress = json['executionProgress'] + """:type: :class:`float` + + Task execution progress.""" + + self.upload_progress = json['uploadProgress'] + """:type: :class:`float` + + Task results upload progress to the API.""" + + self.instance_count = json['instanceCount'] + """:type: :class:`int` + + Number of running instances.""" + + self.download_time = json['downloadTime'] + """:type: :class:`str` + + Resources download time to the instances.""" + + self.download_time_sec = json['downloadTimeSec'] + """:type: :class:`float` + + Resources download time to the instances in seconds.""" + + self.environment_time = json['environmentTime'] + """:type: :class:`str` + + Environment time to the instances.""" + + self.environment_time_sec = json['environmentTimeSec'] + """:type: :class:`float` + + Environment time to the instances in seconds.""" + + self.execution_time = json['executionTime'] + """:type: :class:`str` + + Task execution time.""" + + self.execution_time_sec = json['executionTimeSec'] + """:type: :class:`float` + + Task execution time in seconds.""" + + self.upload_time = json['uploadTime'] + """:type: :class:`str` + + Task results upload time to the API.""" + + self.upload_time_sec = json['uploadTimeSec'] + """:type: :class:`float` + + Task results upload time to the API in seconds.""" + + self.wall_time = json["wallTime"] + """:type: :class:`str` + + Wall time of the task.""" + + self.wall_time_sec = json["wallTimeSec"] + """:type: :class:`float` + + Wall time of the task in seconds.""" + + self.succeeded_range = json['succeededRange'] + """:type: :class:`str` + + Successful instances range.""" + + self.executed_range = json['executedRange'] + """:type: :class:`str` + + Executed instances range.""" + + self.failed_range = json['failedRange'] + """:type: :class:`str` + + Failed instances range.""" + + self.running_instances_info = None + """:type: :class:`RunningInstancesInfo` + + Running instances information.""" + + if 'runningInstancesInfo' in json and json['runningInstancesInfo'] is not None: + self.running_instances_info = RunningInstancesInfo(json['runningInstancesInfo']) + + def __str__(self): + if sys.version_info > (3, 0): + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.items()) + else: + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.iteritems()) # pylint: disable=no-member + + +class TaskActiveForward(object): + """Task Active Forward + + .. note:: Read-only class + """ + def __init__(self, json): + self.application_port = json['applicationPort'] + """:type: :class:`int` + + Application Port.""" + + self.forwarder_port = json['forwarderPort'] + """:type: :class:`int` + + Forwarder Port.""" + + self.forwarder_host = json['forwarderHost'] + """:type: :class:`str` + + Forwarder Host.""" + + def __str__(self): + if sys.version_info > (3, 0): + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.items()) + else: + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.iteritems()) # pylint: disable=no-member + + +class RunningInstancesInfo(object): + """Running Instances Information + + .. note:: Read-only class + """ + def __init__(self, json): + self.per_running_instance_info = [] + """:type: list(:class:`PerRunningInstanceInfo`) + + Per running instances information.""" + + if 'perRunningInstanceInfo' in json and json['perRunningInstanceInfo'] is not None: + self.per_running_instance_info = [PerRunningInstanceInfo(x) for x in json['perRunningInstanceInfo']] + + self.timestamp = json['timestamp'] + """:type: :class:`str` + + Last information update timestamp.""" + + self.average_frequency_ghz = json['averageFrequencyGHz'] + """:type: :class:`float` + + Average Frequency in GHz.""" + + self.max_frequency_ghz = json['maxFrequencyGHz'] + """:type: :class:`float` + + Maximum Frequency in GHz.""" + + self.min_frequency_ghz = json['minFrequencyGHz'] + """:type: :class:`float` + + Minimum Frequency in GHz.""" + + self.average_max_frequency_ghz = json['averageMaxFrequencyGHz'] + """:type: :class:`float` + + Average Maximum Frequency in GHz.""" + + self.average_cpu_usage = json['averageCpuUsage'] + """:type: :class:`float` + + Average CPU Usage.""" + + self.cluster_power_indicator = json['clusterPowerIndicator'] + """:type: :class:`float` + + Cluster Power Indicator.""" + + self.average_memory_usage = json['averageMemoryUsage'] + """:type: :class:`float` + + Average Memory Usage.""" + + self.average_network_in_kbps = json['averageNetworkInKbps'] + """:type: :class:`float` + + Average Network Input in Kbps.""" + + self.average_network_out_kbps = json['averageNetworkOutKbps'] + """:type: :class:`float` + + Average Network Output in Kbps.""" + + self.total_network_in_kbps = json['totalNetworkInKbps'] + """:type: :class:`float` + + Total Network Input in Kbps.""" + + self.total_network_out_kbps = json['totalNetworkOutKbps'] + """:type: :class:`float` + + Total Network Output in Kbps.""" + + def __str__(self): + if sys.version_info > (3, 0): + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.items()) + else: + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.iteritems()) # pylint: disable=no-member + + +class PerRunningInstanceInfo(object): + """Per Running Instance Information + + .. note:: Read-only class + """ + def __init__(self, json): + self.phase = json['phase'] + """:type: :class:`str` + + Instance phase.""" + + self.instance_id = json['instanceId'] + """:type: :class:`int` + + Instance number.""" + + self.max_frequency_ghz = json['maxFrequencyGHz'] + """:type: :class:`float` + + Maximum CPU frequency in GHz.""" + + self.current_frequency_ghz = json['currentFrequencyGHz'] + """:type: :class:`float` + + Current CPU frequency in GHz.""" + + self.cpu_usage = json['cpuUsage'] + """:type: :class:`float` + + Current CPU usage.""" + + self.max_memory_mb = json['maxMemoryMB'] + """:type: :class:`int` + + Maximum memory size in MB.""" + + self.current_memory_mb = json['currentMemoryMB'] + """:type: :class:`int` + + Current memory size in MB.""" + + self.memory_usage = json['memoryUsage'] + """:type: :class:`float` + + Current memory usage.""" + + self.network_in_kbps = json['networkInKbps'] + """:type: :class:`float` + + Network Input in Kbps.""" + + self.network_out_kbps = json['networkOutKbps'] + """:type: :class:`float` + + Network Output in Kbps.""" + + self.progress = json['progress'] + """:type: :class:`float` + + Instance progress.""" + + self.execution_time_sec = json['executionTimeSec'] + """:type: :class:`float` + + Instance execution time in seconds.""" + + self.execution_time_ghz = json['executionTimeGHz'] + """:type: :class:`float` + + Instance execution time GHz""" + + self.cpu_model = json['cpuModel'] + """:type: :class:`str` + + CPU model""" + + self.active_forward = [] + """type: list(:class:`TaskActiveForward`) + + Active forwards list.""" + + if 'activeForwards' in json: + self.active_forward = [TaskActiveForward(x) for x in json['activeForwards']] + + def __str__(self): + if sys.version_info > (3, 0): + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.items()) + else: + return ', '.join("{0}={1}".format(key, val) for (key, val) in self.__dict__.iteritems()) # pylint: disable=no-member diff --git a/requirements-doc.txt b/requirements-doc.txt new file mode 100644 index 0000000..7f69ef1 --- /dev/null +++ b/requirements-doc.txt @@ -0,0 +1,3 @@ +Sphinx +sphinxcontrib.httpdomain +sphinx_rtd_theme diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..5483c96 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +httplib2 +configparser +Twisted +python-ldap +Sphinx +sphinx_rtd_theme diff --git a/server.py b/server.py old mode 100755 new mode 100644 index 3ac5dc8..0be8495 --- a/server.py +++ b/server.py @@ -1,1882 +1,915 @@ -from twisted.web import xmlrpc, server, static, http -from twisted.internet import defer, reactor -import cPickle, time, os, getopt, sys, base64, re, thread, ConfigParser, random, shutil -import atexit, json -import smtplib -from email.mime.text import MIMEText - -GErr=0 -GOk=0 - -print (time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(time.time ()))) - -# Go to the script directory -global installDir, dataDir -if sys.platform=="win32": - import _winreg - # under windows, uses the registry setup by the installer - try: - hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Mercenaries Engineering\\Coalition", 0, _winreg.KEY_READ) - installDir, _type = _winreg.QueryValueEx (hKey, "Installdir") - dataDir, _type = _winreg.QueryValueEx (hKey, "Datadir") - except OSError: - installDir = "." - dataDir = "." -else: - installDir = "." - dataDir = "." -os.chdir (installDir) - -# Create the logs/ directory -try: - os.mkdir (dataDir + "/logs", 0755); -except OSError: - pass - -global TimeOut, port, verbose, config -config = ConfigParser.SafeConfigParser() -config.read ("coalition.ini") - -def cfgInt (name, defvalue): - global config - if config.has_option('server', name): - try: - return int (config.get('server', name)) - except: - pass - return defvalue - -def cfgBool (name, defvalue): - global config - if config.has_option('server', name): - try: - return int (config.get('server', name)) != 0 - except: - pass - return defvalue - -def cfgStr (name, defvalue): - global config - if config.has_option('server', name): - try: - return str (config.get('server', name)) - except: - pass - return defvalue - -port = cfgInt ('port', 19211) -TimeOut = cfgInt ('timeout', 60) -verbose = cfgBool ('verbose', False) -service = cfgBool ('service', True) -notifyafter = cfgInt ('notifyafter', 10) -decreasepriorityafter = cfgInt ('decreasepriorityafter', 10) -smtpsender = cfgStr ('smtpsender', "") -smtphost = cfgStr ('smtphost', "") -smtpport = cfgInt ('smtpport', 587) -smtptls = cfgBool ('smtptls', True) -smtplogin = cfgStr ('smtplogin', "") -smtppasswd = cfgStr ('smtppasswd', "") - -SaveTime = cfgInt ('savetime', 60*5) # DB save timing in secondes, 5min -BackupTime = cfgInt ('backuptime', 60*60) # Backup timing in secondes, 1H -BackupMax = cfgInt ('backupmax', 24) # Maximum backup files, 24 -BackupLastTime = time.time () # Last backup date - -LDAPServer = cfgStr ('ldaphost', "") -LDAPTemplate = cfgStr ('ldaptemplate', "") - -_TrustedUsers = cfgStr ('trustedusers', "") - -TrustedUsers = {} -for line in _TrustedUsers.splitlines (False): - TrustedUsers[line] = True - -_CmdWhiteList = cfgStr ('commandwhitelist', "") - -GlobalCmdWhiteList = None -UserCmdWhiteList = {} -UserCmdWhiteListUser = None -for line in _CmdWhiteList.splitlines (False): - _re = re.match ("^@(.*)", line) - if _re: - UserCmdWhiteListUser = _re.group(1) - if not UserCmdWhiteListUser in UserCmdWhiteList: - UserCmdWhiteList[UserCmdWhiteListUser] = [] - else: - if UserCmdWhiteListUser: - UserCmdWhiteList[UserCmdWhiteListUser].append (line) - else: - if not GlobalCmdWhiteList: - GlobalCmdWhiteList = [] - GlobalCmdWhiteList.append (line) - -DefaultLocalProgressPattern = "PROGRESS:%percent" -DefaultGlobalProgressPattern = None - -def usage(): - print ("Usage: server [OPTIONS]") - print ("Start a Coalition server.\n") - print ("Options:") - print (" -h, --help\t\tShow this help") - print (" -p, --port=PORT\tPort used by the server (default: "+str(port)+")") - print (" -v, --verbose\t\tIncrease verbosity") - print ("\nExample : server -p 1234") - -# Service only on Windows -service = service and sys.platform == "win32" - -if not service: - # Parse the options - try: - opts, args = getopt.getopt(sys.argv[1:], "hp:v", ["help", "port=", "verbose"]) - if len(args) != 0: - usage() - sys.exit(2) - except getopt.GetoptError, err: - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - for o, a in opts: - if o in ("-h", "--help"): - usage () - sys.exit(2) - elif o in ("-v", "--verbose"): - verbose = True - elif o in ("-p", "--port"): - port = float(a) - else: - assert False, "unhandled option " + o - - if LDAPServer != "": - import ldap - -if not verbose or service: - try: - outfile = open(dataDir + '/server.log', 'a') - sys.stdout = outfile - sys.stderr = outfile - def exit (): - outfile.close () - atexit.register (exit) - except: - pass - - -# Log function -def output (str): - if verbose: - print (str) - -output ("--- Start ------------------------------------------------------------") - -if service: - output ("Running service") -else: - output ("Running standard console") - -def getLogFilename (jobId): - global dataDir - return dataDir + "/logs/" + str(jobId) + ".log" - -# strip all -def strToInt (s): - try: - return int(s) - except: - return 0 - -class LogFilter: - """A log filter object. The log pattern must include a '%percent' or a '%one' key word.""" - - def __init__ (self, pattern): - # 0~100 or 0~1 ? - self.IsPercent = re.match (".*%percent.*", pattern) != None - - # Build the final pattern for the RE - if self.IsPercent: - pattern = re.sub ("%percent", "([0-9.]+)", pattern) - else: - pattern = re.sub ("%one", "([0-9.]+)", pattern) - - # Final progress filter - self.RE = re.compile(pattern) - - # Put it in the cache - global LogFilterCache - LogFilterCache[pattern] = self - - def filterLogs (self, log): - """Return the filtered log and the last progress, if any""" - progress = None - for m in self.RE.finditer (log): - capture = m.group(1) - try: - progress = float(capture) / (self.IsPercent and 100.0 or 1.0) - except ValueError: - pass - return self.RE.sub ("", log), progress - #return log, progress - -LogFilterCache = {} - -def getLogFilter (pattern): - """Get the pattern filter from the cache or add one""" - global LogFilterCache - try: - filter = LogFilterCache[pattern] - except KeyError: - filter = LogFilter (pattern) - LogFilterCache[pattern] = filter - return filter - -class Activity: - """A farm event""" - - def __init__ (self, worker, job, jobTitle, id): - self.Worker = worker - self.JobID = job - self.JobTitle = jobTitle - self.State = "WORKING" - self.Start = time.time () - self.Duration = 0 - self.ID = id - -class Job: - """A farm job""" - - def __init__ (self, title, cmd = "", dir = "", environment = None, priority = 1000, retry = 10, timeout = 0, affinity = "", user = "", dependencies = [], localprogress = None, globalprogress = None): - self.ID = None # Job ID - self.Parent = None # Parent Job ID - self.Children = [] # Children Jobs IDs - self.Title = title # Job title - self.Command = cmd # Job command to execute - self.Dir = dir # Job working directory - self.Environment = environment # Job environment - self.State = "WAITING" # Job state, can be WAITING, WORKING, FINISHED or ERROR - self.Worker = "" # Worker hostname - self.StartTime = time.time() # Start working time - self.Duration = 0 # Duration of the process - self.PingTime = self.StartTime # Last worker ping time - self.Try = 0 # Number of try - self.Retry = strToInt (retry) # Number of try max - self.TimeOut = strToInt (timeout) # Timeout in seconds - self.Priority = strToInt (priority) # Job priority - self.Affinity = affinity # Job affinity - self.User = user # Job user - self.Finished = 0 # Number of finished children - self.Errors = 0 # Number of error children - self.Working = 0 # Number of children working - self.Total = 0 # Total number of (grand)children - self.TotalFinished = 0 # Total number of (grand)children finished - self.TotalErrors = 0 # Total number of (grand)children in error - self.TotalWorking = 0 # Total number of children working - self.Dependencies = dependencies # Job dependencies - self.URL = "" # URL to open - if localprogress != None: - self.LocalProgressPattern = localprogress - if globalprogress != None: - self.GlobalProgressPattern = globalprogress - # self.LocalProgress # Progress of the job - # self.GlobalProgress # Progress of the job - - # Has this job some children - def hasChildren (self): - return len (self.Children) > 0 - -def compareJobs (self, other): - if self.Priority < other.Priority: - return 1 - if self.Priority > other.Priority: - return -1 - if self.ID > other.ID: - return 1 - if self.ID < other.ID: - return -1 - return 0 - -def compareAffinities (jobAffinity, workerAffinity): - # check for job with no affinity -- always success - if jobAffinity == "" : - return True - # check for worker with no affinity -- always failure unless no affinity - if workerAffinity == "" : - return False - jobWords = jobAffinity.split (',') - - workerWords = workerAffinity.split (',') - for jobWord in jobWords: - found = False - for workerWord in workerWords: - if workerWord == jobWord: - found = True - if not found: - return False - return True - -class Worker: - """A farm worker""" - - def __init__ (self, name): - self.Name = name # Worker name - self.IP = "" # Worker IP addr - self.Affinity = "" # Worker affinity - self.State = "WAITING" # Worker state, can be WAITING, WORKING, FINISHED or TIMEOUT - self.PingTime = time.time() # Last worker ping time - self.Finished = 0 # Number of finished - self.Error = 0 # Number of fault - self.LastJob = -1 # Last job done - self.CurrentActivity = -1 # Current activity - self.Load = [] # Load of the worker - self.FreeMemory = 0 # Free memory of the worker system - self.TotalMemory = 0 # Total memory of the worker system - self.Active = True # Is the worker enabled - -def writeJobLog (jobId, log): - logFile = open (getLogFilename (jobId), "a") - logFile.write (log) - logFile.close () - -# State of the master -# Rules for picking a job: -# If the job has children, they must be finished before -# If no child can be ran (exceeded retries count), then the job cannot be ran -# Children are picked according to their priority -DBVersion = 8 -class CState: - - def __init__ (self): - self.clear () - - # Clear the whole database - def clear (self) : - self.Counter = 0 - self.ActivityCounter = 0 - self.Activities = {} - self.Jobs = {} - self.Workers = {} - self._ActiveJobs = set () - self.addJob (0, Job ("Root", priority=1, retry=0)) - self._UpdatedDb = False - self._Affinities = {} # dynamic affinity, job affinity concatened to the children jobs affinity - - # Read the state - def read (self, fo): - _time = time.time () - version = cPickle.load(fo) - if version >= 8 : - self.Counter = cPickle.load (fo) - self.ActivityCounter = cPickle.load (fo) - - count = cPickle.load (fo) - self.Activities = {} - for i in range (0, count): - array = cPickle.load (fo) - self.Activities.update (array) - - count = cPickle.load (fo) - self.Jobs = {} - for i in range (0, count): - array = cPickle.load (fo) - self.Jobs.update (array) - - count = cPickle.load (fo) - self.Workers = {} - for i in range (0, count): - array = cPickle.load (fo) - self.Workers.update (array) - self._refresh () - else : - if version >= 5: - self.Counter = cPickle.load (fo) - if version >= 7 : - self.ActivityCounter = cPickle.load (fo) - self.Activities = cPickle.load (fo) - else : - print ("Translate DB to version 7") - self.ActivityCounter = 0 - self.Activities = {} - self.Jobs = cPickle.load (fo) - self.Workers = cPickle.load (fo) - - # Translate Workers from 6 -> 7 - if version < 7 : - for id, worker in self.Workers.iteritems () : - worker.CurrentActivity = -1 - for id, job in self.Jobs.iteritems () : - job.URL = "" - - self._refresh () - if version <= 5: - # Add Working, TotalWorking - print ("Translate DB to version 6") - for id, job in self.Jobs.iteritems () : - job.Working = 0 - job.TotalWorking = 0 - #self.dump () - else: - raise Exception ("Database too old, erase the master_db file") - self.clear () - output ("Read time :" + str (time.time () - _time) + "s") - - # Write the state - def write (self): - global dataDir - backup () - fo = open(dataDir + "/master_db.part", "wb") - try: - _time = time.time () - version = DBVersion - cPickle.dump (version, fo) - cPickle.dump (self.Counter, fo) - cPickle.dump (self.ActivityCounter, fo) - - blockSize = 10000 - - # Save a block of dict - def saveBlock (blockID, blockSize, keys, _dict, fo): - array = {} - for j in range (blockID*blockSize, min (len (keys), (blockID+1)*blockSize)): - key = keys[j] - value = _dict.get (key) - if value != None: - array[key] = value - cPickle.dump (array, fo) - - # Can't factorize this code here because of the yield - - output ("Write Activities") - keys = self.Activities.keys () - blockCount = (len (keys) + (blockSize-1)) / blockSize - cPickle.dump (blockCount, fo) - for blockID in range(0, blockCount): - saveBlock (blockID, blockSize, keys, self.Activities, fo) - yield True - - output ("Write Jobs") - keys = self.Jobs.keys () - blockCount = (len (keys) + (blockSize-1)) / blockSize - cPickle.dump (blockCount, fo) - for blockID in range(0, blockCount): - saveBlock (blockID, blockSize, keys, self.Jobs, fo) - yield True - - output ("Write Workers") - keys = self.Workers.keys () - blockCount = (len (keys) + (blockSize-1)) / blockSize - cPickle.dump (blockCount, fo) - for blockID in range(0, blockCount): - saveBlock (blockID, blockSize, keys, self.Workers, fo) - yield True - - fo.close() - try: - os.remove (dataDir + '/master_db') - except OSError: - pass - os.rename (dataDir + '/master_db.part', dataDir + '/master_db') - except IOError: - fo.close() - output ("DB saved in " + str (time.time () - _time) + "s") - yield False - - def update (self, forceSaveDb = False) : - global TimeOut - _time = time.time () - refreshActive = False - for id in State._ActiveJobs.copy () : - try: - job = self.Jobs[id] - if job.State == "WORKING" and job.Command != "": # Don't update timeout if it is a folder job - if _time - job.PingTime > TimeOut : - # Job times out, no heartbeat received for too long - output ("Job " + str(job.ID) + " is AWOL") - self.updateJobState (id, "ERROR") - self.updateWorkerState (job.Worker, "TIMEOUT") - writeJobLog (job.ID, "SERVER: Worker "+job.Worker+" doesn't respond, timeout.") - elif job.TimeOut > 0 and _time - job.StartTime > job.TimeOut: - # job exceeded run time - output ("Job " + str(job.ID) + " timeout, exceeded run time") - self.updateJobState (id, "ERROR") - self.updateWorkerState (job.Worker, "ERROR") - writeJobLog (job.ID, "SERVER: Job " + str(job.ID) + " timeout, exceeded run time") - if not job.hasChildren () : - job.Duration = _time - job.StartTime - try: - worker = State.getWorker (job.Worker) - activity = self.Activities[worker.CurrentActivity] - if activity.JobID == job.ID: - activity.Duration = job.Duration - except KeyError: - pass - except KeyError: - refreshActive = True - if refreshActive : - self._refresh () - # Timeout workers - for name, worker in State.Workers.iteritems (): - if worker.State != "TIMEOUT" and _time - worker.PingTime > TimeOut: - self.updateWorkerState (name, "TIMEOUT") - if forceSaveDb: - saveDb () - - # ----------------------------------------------------------------------- - # job handling - - # Is job dependent on another - def doesJobDependOn (self, id0, id1): - if id0 == id1: - return True - try: - job0 = self.Jobs[id0] - for i in job0.Dependencies: - if self.doesJobDependOn (i, id1): - return True - except KeyError: - pass - return False - - # Find a job by its title - def findJobByTitle (self, title): - for id, job in self.Jobs.iteritems (): - if job.Title == title: - return id - - # Find a job by its path, job path atoms are separated by pipe '|' - def findJobByPath (self, path): - job = self.Jobs[0] - atoms = re.findall ('([^|]+)', path) - for atom in atoms : - found = False - for id in job.Children : - try : - child = self.Jobs[id] - if child.Title == atom : - job = child - found = True - break - except KeyError: - pass - if not found : - return None - return job.ID - - # Add a job - def addJob (self, parent, job): - try: - parentJob = None - job.ID = self.Counter - if job.ID != 0: - parentJob = self.Jobs[parent] - self.Counter = job.ID + 1 - self.Jobs[job.ID] = job - self._UpdatedDb = True - if job.ID != 0: - job.Parent = parent - parentJob.Children.append (job.ID) - self._updateAffinity (job.ID) - self._updateParentState (parent) - return job.ID - - except KeyError: - print ("Can't add job to parent " + str (parent) + " type", type (parent)) - - # Remove a job - def removeJob (self, id, updateParentState = True): - if id != 0: - try: - job = self.Jobs[id] - self._UpdatedDb = True - # remove children first - while (len (job.Children) > 0) : - self.removeJob (job.Children[0]) - # remove self from parent - parent = self.Jobs[job.Parent] - for k, childid in enumerate (parent.Children) : - if childid == id : - parent.Children.pop (k) - break - # and unmap - try: - del self.Jobs[id] - except KeyError: - pass - try: - del self._Affinities[id] - except KeyError: - pass - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - # only update parent's state when required (for instance in removeChildren, it is done after removing all children) - if updateParentState: - self._updateParentState (parent.ID) - except KeyError: - pass - - # Remove children jobs - def removeChildren (self, id) : - job = self.Jobs[id] - while (len (job.Children) > 0) : - self.removeJob (job.Children[0], False) - self._updateParentState (id) - - # Change job affinity - def setAffinity (self, id, affinity) : - try: - job = self.Jobs[id] - job.Affinity = affinity - self._UpdatedDb = True - self._updateParentState (id) - except: - pass - - # Reset a job - def resetJob (self, id) : - try: - job = self.Jobs[id] - job.State = "WAITING" - job.Try = 0 - job.Worker = "" - if getattr(job, "LocalProgress", False): - job.LocalProgress = 0 - if getattr(job, "GlobalProgress", False): - job.GlobalProgress = 0 - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - self._UpdatedDb = True - self._updateParentState (job.Parent) - for cid in job.Children : - self.resetJob (cid) - - # Clear the logs - try: - os.unlink (getLogFilename (id)) - except OSError: - pass - - except KeyError: - pass - - # Reset a job and its error children - def resetErrorJob (self, id) : - try: - job = self.Jobs[id] - job.State = "WAITING" - job.Try = 0 - job.Worker = "" - if getattr(job, "LocalProgress", False): - job.LocalProgress = 0 - if getattr(job, "GlobalProgress", False): - job.GlobalProgress = 0 - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - self._UpdatedDb = True - self._updateParentState (job.Parent) - for cid in job.Children : - if self.Jobs[cid].State == "ERROR": - self.resetErrorJob (cid) - except KeyError: - pass - - # Reset a job - def startJob (self, id) : - try: - job = self.Jobs[id] - job.State = "WAITING" - job.Try = 0 - job.Worker = "" - if getattr(job, "LocalProgress", False): - job.LocalProgress = 0 - if getattr(job, "GlobalProgress", False): - job.GlobalProgress = 0 - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - self._UpdatedDb = True - self._updateParentState (id) - except KeyError: - pass - - # Start a paused job - def startJob (self, id) : - try: - job = self.Jobs[id] - if job.State == "PAUSED" : - job.State = "WAITING" - self._UpdatedDb = True - self._updateParentState (id) - except KeyError: - pass - - # Pause a job - def pauseJob (self, id) : - try: - job = self.Jobs[id] - job.State = "PAUSED" - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - self._UpdatedDb = True - self._updateParentState (id) - except KeyError: - pass - - # Move a job - def moveJob (self, id, dest) : - try: - if id != dest: - job = self.Jobs[id] - oldParent = self.Jobs[job.Parent] - parent = self.Jobs[dest] - oldParent.Children.remove (id) - parent.Children.append (id) - job.Parent = dest - self._UpdatedDb = True - self._updateParentState (dest) - self._updateParentState (oldParent.ID) - except KeyError: - output ("moveJob key error") - pass - - # Can be executed - def canExecute (self, id) : - # Root - if id == 0: - return False - job = self.Jobs[id] - - # Don't execute a finished job or a working job - if job.State == "FINISHED" or job.State == "PAUSED" : - return False - - # Waiting jobs can be executed only if all dependencies are finished - # Error jobs can be ran only if they have no children and tries left - for depId in job.Dependencies : - dep = self.Jobs[depId] - if dep.State != "FINISHED" : - return False - - # Visit parents, or waiting jobs or error jobs with enough retry - return job.hasChildren () or (not job.hasChildren () and (job.State == "WAITING" or (job.State == "ERROR" and job.Try < job.Retry))) - - # Can be executed - def compatibleAffinities (self, job, worker) : - if worker >= job: - return True - return False - - def pickJob (self, id, affinity) : - return self.pickJobSequencial (id, affinity) - - # Pick a job sequencial - def pickJobSequencial (self, id, affinity) : - try: - job = self.Jobs[id] - nextChild = None - nextJobID = None - - # Look for the next job - allFinished = True - for childId in job.Children : - child = self.Jobs[childId] - if child.State != "FINISHED" : - allFinished = False - # if job can be executed and worker affinity is compatible, add this job as a potential one - if self.canExecute (childId): - if self.compatibleAffinities (self._Affinities[childId], affinity): - #output ("++ worker "+str (affinity)+" compatible with job "+str (childId)+" "+str (self._Affinities[childId])) - if nextChild == None or child.Priority > nextChild.Priority or (child.Priority == nextChild.Priority and (child.TotalWorking+child.TotalFinished < nextChild.TotalWorking+nextChild.TotalFinished)) : - tryJobId = None - if child.hasChildren () : - tryJobId = self.pickJob (child.ID, affinity) - else : - tryJobId = child.ID - if tryJobId != None: - nextChild = child - nextJobID = tryJobId - return nextJobID - except KeyError: - pass - - # Pick a job in random - def pickJobRandom (self, id, affinity) : - try: - job = self.Jobs[id] - sumpriority = 0 - jobs = [] - # sum all children priorities - allFinished = True - for childId in job.Children : - child = self.Jobs[childId] - if child.State != "FINISHED" : - allFinished = False - # if job can be executed and worker affinity is compatible, add this job as a potential one - if self.canExecute (childId): - if self.compatibleAffinities (self._Affinities[childId], affinity): - #output ("++ worker "+str (affinity)+" compatible with job "+str (childId)+" "+str (self._Affinities[childId])) - sumpriority += child.Priority - jobs.append (child) - else: - #output ("-- worker "+str (affinity)+" NOT compatible with job "+str (childId)+" "+str (self._Affinities[childId])) - pass - if sumpriority > 0 : - # there are some children that need execution - pick = random.randint (0, sumpriority-1) - sumpriority = 0 - for child in jobs : - if pick >= sumpriority and pick < sumpriority+child.Priority : - if child.hasChildren () : - return self.pickJob (child.ID, affinity) - else: - return child.ID - sumpriority += child.Priority - elif allFinished and State.canExecute (id) : - # all children were successfully executed, execute this job - output ("job.Command : " + job.Command) - return id - except KeyError: - pass - - # Update job state - def updateJobState (self, id, state) : - global GErr, GOk - if state == "ERROR" : - GErr += 1 - if state == "FINISHED" : - GOk += 1 - try : - job = self.Jobs[id] - if job.State != state : - self._UpdatedDb = True - job.State = state - - # Update the event - activity = None - try : - worker = self.getWorker (job.Worker) - activity = self.Activities[worker.CurrentActivity] - if activity.JobID == id: - activity.State = state - except KeyError: - pass - - if state == "WORKING" : - job.Try += 1 - job.StartTime = time.time () - self._ActiveJobs.add (id) - elif state == "ERROR" or state == "FINISHED": - if state == "ERROR" : - job.Priority = max (job.Priority-1, 0) - notifyError (job) - if state == "FINISHED" : - notifyFinished (job) - if not job.hasChildren () : - _time = time.time() - job.Duration = _time - job.StartTime - if activity: - activity.Duration = job.Duration - try: - self._ActiveJobs.remove (id) - except KeyError: - pass - self._updateParentState (job.Parent) - except KeyError: - pass - - # Update parent state - def _updateParentState (self, id) : - if id == 0 : - return - try: - output ("_updateParentState " + str(id)) - job = self.Jobs[id] - jobsToDo = False - hasError = False - total = 0 - totalfinished = 0 - totalerrors = 0 - totalworking = 0 - finished = 0 - errors = 0 - working = 0 - durationAvg = 0; - durationCount = 0; - for childId in job.Children : - if self.canExecute (childId): - jobsToDo = True - child = self.Jobs[childId] - state = child.State - if child.hasChildren () : - total += child.Total or 0 - totalerrors += child.TotalErrors or 0 - totalfinished += child.TotalFinished or 0 - totalworking += child.TotalWorking or 0 - durationAvg += child.Duration * child.Total; - durationCount += child.Total; - else : - total += 1 - if state == "ERROR": - errors += 1 - elif state == "FINISHED": - finished += 1 - elif state == "WORKING": - working += 1 - durationAvg += child.Duration; - durationCount += 1; - if durationCount > 0 : - durationAvg /= durationCount - else : - durationAvg = 0 - totalerrors += errors - totalfinished += finished - totalworking += working - - # If this parent job has finished the notifyafter first jobs, notify the user - if job.Finished < notifyafter and finished >= notifyafter : - notifyFirstFinished (job) - - # If this parent job has finished the notifyafter first jobs, notify the user - if job.Errors < decreasepriorityafter and errors >= decreasepriorityafter : - job.Priority = max (job.Priority-1, 0) - - job.Finished = finished - job.Errors = errors - job.Working = working - job.Total = total - job.TotalErrors = totalerrors - job.TotalFinished = totalfinished - job.TotalWorking = totalworking - job.Duration = durationAvg - - # New job state - newState = "WAITING" - if job.TotalWorking > 0 : - newState = "WORKING" - elif job.TotalErrors > 0 : - newState = "ERROR" - elif job.Total > 0 and job.Total == job.TotalFinished : - newState = "FINISHED" - - if job.State != "PAUSED" and newState != job.State: - if newState == "FINISHED" : - notifyFinished (job) - if newState == "ERROR" : - notifyError (job) - job.State = newState - self._UpdatedDb = True - self._updateAffinity (id) - self._updateParentState (job.Parent) - except KeyError: - pass - - # Update job affinity - def _updateAffinity (self, id) : - job = self.Jobs[id] - self._Affinities[id] = frozenset (re.findall ('([^,]+)', job.Affinity)) - - # Refresh active jobs count - def _refresh (self) : - def safeInt (v, defvalue): - try: - return int (v) - except: - return defvalue - def safeStr (v, defvalue): - try: - return str (v) - except: - return defvalue - active = set () - for id, job in self.Jobs.iteritems (): - if job.State == "WORKING": - active.add (id) - job.Parent = safeInt (job.Parent, 0) - job.Command = safeStr (job.Command, "") - job.Dir = safeStr (job.Dir, "") - job.State = safeStr (job.State, "ERROR") - job.Worker = safeStr (job.Worker, "") - job.StartTime = safeInt (job.StartTime, time.time ()) - job.Duration = safeInt (job.Duration, 0) - job.PingTime = safeInt (job.PingTime, time.time ()) - job.Try = safeInt (job.Try, 0) - job.Retry = safeInt (job.Retry, 10) - job.TimeOut = safeInt (job.TimeOut, 0) - job.Priority = safeInt (job.Priority, 1000) - job.Affinity = safeStr (job.Affinity, "") - job.User = safeStr (job.User, "") - - self._ActiveJobs = active - def _upChildren (job) : - total = 0 - totalerrors = 0 - totalfinished = 0 - for cid in job.Children : - child = self.Jobs[cid] - _upChildren (child) - total += child.Total - totalerrors += child.TotalErrors - totalfinished += child.TotalFinished - if child.State == "ERROR": - totalerrors += 1 - elif child.State == "FINISHED": - totalfinished += 1 - self._updateAffinity (job.ID) - job.Total = total+len (job.Children) - job.TotalFinished = totalfinished - job.TotalErrors = totalerrors - _upChildren (self.Jobs[0]) - - # ----------------------------------------------------------------------- - # worker handling - - # get a worker - def getWorker (self, name) : - try : - worker = self.Workers[name] - worker.PingTime = time.time() - return worker - except KeyError: - # Worker not found, add it - self._UpdatedDb = True - output ("Add worker " + name) - worker = Worker (name) - worker.PingTime = time.time() - self.Workers[name] = worker - return worker - - def stopWorker (self, name): - output ("Stop worker " + name) - try : - self.Workers[name].Active = False - self._UpdatedDb = True - except KeyError: - pass - # Try to stop the worker's jobs - for id, job in self.Jobs.iteritems (): - if job.Worker == name and job.State == "WORKING": - job.State = "WAITING" - self._UpdatedDb = True - - def startWorker (self, name): - output ("Start worker " + name) - try : - self.Workers[name].Active = True - self._UpdatedDb = True - except KeyError: - pass - - def updateWorkerState (self, name, state) : - try: - worker = self.Workers[name] - if state != worker.State: - self._UpdatedDb = True - if state == "ERROR" : - worker.Error += 1 - worker.State = "WAITING" - elif state == "FINISHED" : - worker.Finished += 1 - worker.State = "WAITING" - elif state == "TIMEOUT" : - worker.Error += 1 - worker.State = "TIMEOUT" - else: - worker.State = state - except KeyError: - pass - - # ----------------------------------------------------------------------- - # debug/dump - - def dump (self) : - def dumpJob (id, depth) : - try: - job = self.Jobs[id] - print (" "*(depth*2)) + str (job.ID) + " " + job.Title + " " + job.State + " cmd='" + job.Dir + "/" + job.Command + "' prio=" + str (job.Priority) + " retry=" + str (job.Try) + "/" + str (job.Retry) - try: - dyn = self._Affinities[id] - print (" "*(depth*2+1)), "Dyn:", dyn - except: - pass - for childId in job.Children : - dumpJob (childId, depth+1) - except KeyError: - print (" "*(depth*2)) + "<<< Unknown job " + str (id) + " >>>" - dumpJob (0, 0) - -State = CState() - - - -# Authenticate the user -def authenticate (request): - if LDAPServer != "": - username = request.getUser () - password = request.getPassword () - if username in TrustedUsers: - output (username + " in the clearance list") - output ("Authentication OK") - return True - if username != "" or password != "": - l = ldap.open(LDAPServer) - output ("Authenticate "+username+" with LDAP") - username = LDAPTemplate.replace ("__login__", username) - try: - if l.bind_s(username, password, ldap.AUTH_SIMPLE): - output ("Authentication OK") - return True - except ldap.LDAPError: - output ("Authentication Failed") - pass - else: - output ("Authentication Required") - request.setHeader ("WWW-Authenticate", "Basic realm=\"Coalition Login\"") - request.setResponseCode(http.UNAUTHORIZED) - return False - return True - -# Check if the user can add this command -def grantAddJob (user, cmd): - - def checkWhiteList (wl): - for pattern in wl: - if (re.match (pattern, cmd)): - return True - else: - output ("User '" + user + "' is not allowed to run the command '" + cmd + "'") - return False - - # User defined white list ? - if user in UserCmdWhiteList: - wl = UserCmdWhiteList[user] - if checkWhiteList (wl): - return True - - # If in the global command white list - if GlobalCmdWhiteList: - if checkWhiteList (GlobalCmdWhiteList): - return True - return False - - else: - # If in the global command white list - if GlobalCmdWhiteList: - if not checkWhiteList (GlobalCmdWhiteList): - return False - - # Cleared - return True - -class Root (static.File): - def __init__ (self, path, defaultType='text/html', ignoredExts=(), registry=None, allowExt=0): - static.File.__init__(self, path, defaultType, ignoredExts, registry, allowExt) - - def render (self, request): - if authenticate (request): - return static.File.render (self, request) - return 'Authorization required!' - -class Master (xmlrpc.XMLRPC): - """ """ - - User = "" - - def render (self, request): - global State - if authenticate (request): - # If not autenticated, User == "" - self.User = request.getUser () - # Addjob - - def getArg (name, default): - value = request.args.get (name, [default]) - return value[0] - - if request.path == "/xmlrpc/addjob" or request.path == "/json/addjob": - - parent = getArg ("parent", "0") - title = getArg ("title", "New job") - cmd = getArg ("cmd", "") - dir = getArg ("dir", ".") - environment = getArg ("env", None) - if environment == "": - environment = None - priority = getArg ("priority", "1000") - retry = getArg ("retry", "10") - timeout = getArg ("timeout", "0") - affinity = getArg ("affinity", "") - dependencies = getArg ("dependencies", "") - localprogress = getArg ("localprogress", None) - globalprogress = getArg ("globalprogress", None) - url = getArg ("url", "") - user = getArg ("user", "") - if self.User != "": - user = self.User - - if grantAddJob (self.User, cmd): - output ("Add job : " + cmd) - if isinstance (parent, str): - try: - # try as an int - parent = int (parent) - except ValueError: - bypath = State.findJobByPath (parent) - if bypath: - parent = bypath - else: - parenttitle = parent - parent = State.findJobByTitle (parent) - if parent == None: - print ("Error : can't find job " + str (parenttitle)) - return -1 - if type(dependencies) is str: - # Parse the dependencies string - dependencies = re.findall ('(\d+)', dependencies) - for i, dep in enumerate (dependencies) : - dependencies[i] = int (dep) - - id = State.addJob (parent, Job (str (title), str (cmd), str (dir), str (environment), int (priority), int (retry), int (timeout), str (affinity), str (user), dependencies, localprogress, globalprogress)) - State.Jobs[id].URL = url - - State.update () - return str(id) - else: - return -1 - elif request.path == "/json/getjobs": - return self.json_getjobs (int(getArg ("id", 0)), getArg ("filter", "")) - elif request.path == "/json/clearjobs": - return self.json_clearjobs (request.args.get ("id")) - elif request.path == "/json/resetjobs": - return self.json_resetjobs (request.args.get ("id")) - elif request.path == "/json/reseterrorjobs": - return self.json_reseterrorjobs (request.args.get ("id")) - elif request.path == "/json/startjobs": - return self.json_startjobs (request.args.get ("id")) - elif request.path == "/json/pausejobs": - return self.json_pausejobs (request.args.get ("id")) - elif request.path == "/json/movejobs": - return self.json_movejobs (request.args.get ("id"), getArg ("dest", 0)) - elif request.path == "/json/updatejobs": - return self.json_updatejobs (request.args.get ("id"), request.args.get ("prop"),request.args.get ("value")) - elif request.path == "/json/getlog": - return self.json_getlog (int(getArg ("id", 0))) - elif request.path == "/json/getworkers": - return self.json_getworkers () - elif request.path == "/json/clearworkers": - return self.json_clearworkers (request.args.get ("id")) - elif request.path == "/json/stopworkers": - return self.json_stopworkers (request.args.get ("id")) - elif request.path == "/json/startworkers": - return self.json_startworkers (request.args.get ("id")) - elif request.path == "/json/updateworkers": - return self.json_updateworkers (request.args.get ("id"), request.args.get ("prop"),request.args.get ("value")) - elif request.path == "/json/getactivities": - return self.json_getactivities (int(getArg ("job", -1)), str(getArg ("worker", "")), int(getArg ("howlong", -1))) - else: - # return server.NOT_DONE_YET - return xmlrpc.XMLRPC.render (self, request) - return 'Authorization required!' - - def json_getjobs (self, id, filter): - global State - output ("Send jobs") - - State.update () - - vars = ["ID","Title","Command","Dir","State","Worker","StartTime","Duration","Try","Retry","TimeOut","Priority","Affinity","User","Finished","Errors","Working","Total","TotalFinished","TotalErrors","TotalWorking","Dependencies","URL","LocalProgress","GlobalProgress"]; - - # Get the job - try: - job = State.Jobs[id] - except KeyError: - job = State.Jobs[0] - - # Build the children - jobs = "[" - for childId in job.Children : - try: - child = State.Jobs[childId] - if filter == "" or child.State == filter: - childparams = "[" - for var in vars: - attr = None - try: - attr = getattr (child, var) - except AttributeError: - pass - childparams += json.dumps (attr) + ',' - childparams += "],\n" - jobs += childparams - except KeyError: - pass - jobs += "]" - - parents = [] - # Build the parents - while True: - parents.insert (0, { "ID":job.ID, "Title":job.Title }) - if job.ID == 0: - break - job = State.Jobs[job.Parent] - - return '{ "Vars":'+repr(vars)+', "Jobs":'+jobs+', "Parents":'+repr(parents)+' }' - - def json_clearjobs (self, ids): - global State - for jobId in ids: - output ("Clear job "+str (jobId)) - State.removeJob (int(jobId)) - State.update () - return "1" - - def json_resetjobs (self, ids): - global State - for jobId in ids: - output ("Reset job "+str (jobId)) - State.resetJob (int(jobId)) - State.update () - return "1" - - def json_reseterrorjobs (self, ids): - global State - for jobId in ids: - output ("Reset error job "+str (jobId)) - State.resetErrorJob (int(jobId)) - State.update () - return "1" - - def json_startjobs (self, ids): - global State - for jobId in ids: - output ("Start job "+str (jobId)) - State.startJob (int(jobId)) - State.update () - return "1" - - def json_pausejobs (self, ids): - global State - for jobId in ids: - output ("Pause job "+str (jobId)) - State.pauseJob (int(jobId)) - State.update () - return "1" - - def json_movejobs (self, ids, dest): - global State - for jobId in ids: - output ("Move job "+str (jobId)+" in "+str(dest)) - State.moveJob (int(jobId),int(dest)) - State.update () - return "1" - - def json_updatejobs (self, ids, props, values): - global State - output ("Update job "+str (ids)+" "+str(props)+" "+str(values)) - if props == None or values == None or len(props) != len(values): - return "0" - for i in range(0,len(props)): - prop = props[i] - value = values[i] - for id in ids: - id = int(id) - try: - job = State.Jobs[id] - try: - if prop == "Command": - cmd = str (value) - if grantAddJob (job.User, cmd): - job.Command = cmd - elif prop == "Dir": - job.Dir = str (value) - elif prop == "Priority": - job.Priority = int (value) - elif prop == "Affinity": - State.setAffinity (job.ID, str (value)) - elif prop == "TimeOut": - job.TimeOut = int (value) - elif prop == "Title": - job.Title = str (value) - elif prop == "Retry": - job.Retry = int (value) - elif prop == "Dependencies": - job.Dependencies = str (value) - elif prop == "User": - if not LDAPServer != "": - job.User = str (value) - elif prop == "URL": - job.URL = str (value) - State._UpdatedDb = True - except ValueError: - pass - except KeyError: - pass - State.update () - return "1" - - def json_getlog (self, jobId): - global State - output ("Send log "+str (jobId)) - # Look for the job - log = "" - try: - logFile = open (getLogFilename (jobId), "r") - while (1): - # Read some lines of logs - line = logFile.readline() - # "" means EOF - if line == "": - break - log = log + line - logFile.close () - except IOError: - pass - return repr (log) - - def json_getworkers (self): - global State - output ("Send workers") - - State.update () - - vars = ["Name","IP","Affinity","State","Finished","Error","LastJob","Load","FreeMemory","TotalMemory","Active"] - - # Build the children - workers = "[" - for name, worker in State.Workers.iteritems () : - childparams = "[" - for var in vars: - childparams += json.dumps (getattr (worker, var, None)) + ',' - childparams += "],\n" - workers += childparams - workers += "]" - - result = ('{ "Vars":'+repr(vars)+', "Workers":'+workers+'}') - return result - - def json_clearworkers (self, names): - global State - for name in names: - output ("Clear worker "+str (name)) - try: - State.Workers.pop (name) - State._UpdatedDb = True - except KeyError: - pass - State.update () - return "1" - - def json_stopworkers (self, names): - global State - for name in names: - State.stopWorker (name) - State.update () - return "1" - - def json_startworkers (self, names): - global State - for name in names: - State.startWorker (name) - State.update () - return "1" - - # update several workers props at once - def json_updateworkers (self, names, props, values): - global State - try: - output ("Update workers "+str (names)+" "+str(props)+" "+str(values)) - except: - pass - if len(props) != len(values): - return "0" - for i in range(0,len(props)): - prop = props[i] - value = values[i] - for name in names: - try: - worker = State.Workers[name] - try: - if prop == "Affinity": - worker.Affinity = str (value) - State._UpdatedDb = True - except ValueError: - pass - except KeyError: - pass - State.update () - return "1" - - def json_getactivities (self, job, worker, howlong): - global State - output ("Send activities " +str(job)+" "+str(worker)+" "+str(howlong)) - - State.update () - - vars = ["Start","JobID","JobTitle","State","Worker","Duration","ID"] - - # Build the children - _time = time.time (); - activities = "[" - for name, activity in State.Activities.iteritems () : - if (job == -1 or activity.JobID == job) and (worker == "" or activity.Worker == worker) and (howlong == -1 or _time - activity.Start < howlong): - childparams = "[" - for var in vars: - childparams += json.dumps (getattr (activity, var)) + ',' - childparams += "],\n" - activities += childparams - activities += "]" - - result = ('{ "Vars":'+repr(vars)+', "Activities":'+activities+'}') - return result - -# Unauthenticated connection for workers -class Workers(xmlrpc.XMLRPC): - """ """ - - def render (self, request): - global State - - def getArg (name, default): - value = request.args.get (name, [default]) - return value[0] - - if request.path == "/workers/heartbeat": - return self.json_heartbeat (getArg ('hostname', ''), getArg ('jobId', '-1'), getArg ('log', ''), getArg ('load', '[0]'), getArg ('freeMemory', '0'), getArg ('totalMemory', '0'), request.getClientIP ()) - elif request.path == "/workers/pickjob": - return self.json_pickjob (getArg ('hostname', ''), getArg ('load', '[0]'), getArg ('freeMemory', '0'), getArg ('totalMemory', '0'), request.getClientIP ()) - elif request.path == "/workers/endjob": - return self.json_endjob (getArg ('hostname', ''), getArg ('jobId', '1'), getArg ('errorCode', '0'), request.getClientIP ()) - else: - # return server.NOT_DONE_YET - return xmlrpc.XMLRPC.render (self, request) - - def json_heartbeat (self, hostname, jobId, log, load, freeMemory, totalMemory, ip): - """Get infos from the workers.""" - global State - _time = time.time () - output ("Heart beat for " + str(jobId) + " " + str(load)) - # Update the worker load and ping time - worker = State.getWorker (hostname) - try: - worker.Load = eval (load) - except SyntaxError: - worker.Load = [0] - worker.FreeMemory = int(freeMemory) - worker.TotalMemory = int(totalMemory) - worker.IP = str(ip) - workingJob = None - jobId = int(jobId) - try : - job = State.Jobs[jobId] - if job.State == "WORKING" and job.Worker == hostname : - State.updateWorkerState (hostname, "WORKING") - workingJob = job - job.PingTime = _time - if log != "" : - try: - logFile = open (getLogFilename (jobId), "a") - log = base64.decodestring(log) - - # Filter the log progression message - progress = None - localProgress = getattr(job, "LocalProgressPattern", DefaultLocalProgressPattern) - globalProgress = getattr(job, "GlobalProgressPattern", DefaultGlobalProgressPattern) - if localProgress or globalProgress: - output ("progressPattern : \n" + str(localProgress) + " " + str(globalProgress)) - lp = None - gp = None - if localProgress: - lFilter = getLogFilter (localProgress) - log, lp = lFilter.filterLogs (log) - if globalProgress: - gFilter = getLogFilter (globalProgress) - log, gp = gFilter.filterLogs (log) - if lp != None: - output ("lp : "+ str(lp)+"\n") - job.LocalProgress = lp - if gp != None: - output ("gp : "+ str(gp)+"\n") - job.GlobalProgress = gp - - logFile.write (log) - logFile.close () - except IOError: - output ("Error in logs") - except KeyError: - pass - State.update () - if worker.State == "WORKING" and workingJob != None and workingJob.State == "WORKING": - return "true" - # Stop - output ("Error at " + hostname + " heartbeat, set to WAITING") - State.updateWorkerState (hostname, "WAITING") - if workingJob: - State.updateJobState (jobId, "WAITING") - return "false" - - def json_pickjob (self, hostname, load, freeMemory, totalMemory, ip): - """A worker ask for a job.""" - global State - output (hostname + " wants some job" + " " + load) - worker = State.getWorker (hostname) - try: - worker.Load = eval (load) - except SyntaxError: - worker.Load = [0] - worker.FreeMemory = int(freeMemory) - worker.TotalMemory = int(totalMemory) - worker.IP = str(ip) - if not worker.Active: - State.updateWorkerState (hostname, "WAITING") - return '-1,"","","",None' - affinity = frozenset (re.findall ('([^,]+)', worker.Affinity)) - jobId = State.pickJob (0, affinity) - if jobId != None : - job = State.Jobs[jobId] - if job.State == "FINISHED": - output (hostname + " picked a finished job!") - job.Worker = hostname - job.PingTime = time.time() - job.StartTime = job.PingTime - job.Duration = 0 - State.updateJobState (jobId, "WORKING") - worker.LastJob = job.ID - worker.PingTime = job.PingTime - State.updateWorkerState (hostname, "WORKING") - State.update () - output (hostname + " picked job " + str (jobId) + " " + worker.State) - - # Create the event - event = Activity (hostname, job.ID, job.Title, State.ActivityCounter) - State.ActivityCounter += 1; - State.Activities[event.ID] = event - worker.CurrentActivity = event.ID - - if job.User != None and job.User != "": - return repr (job.ID)+","+repr (job.Command)+","+repr (job.Dir)+","+repr (job.User)+","+repr (job.Environment) - else: - return repr (job.ID)+","+repr (job.Command)+","+repr (job.Dir)+","+'""'+","+repr (job.Environment) - - State.updateWorkerState (hostname, "WAITING") - State.update () - return '-1,"","","",None' - - def json_endjob (self, hostname, jobId, errorCode, ip): - """A worker finished a job.""" - global State - worker = State.getWorker (hostname) - worker.IP = str(ip) - output ("End job " + str(jobId) + " with code " + str (errorCode)) - jobId = int(jobId) - errorCode = int(errorCode) - try: - job = State.Jobs[jobId] - if job.State == "WORKING" and job.Worker == hostname : - result = "FINISHED" - if errorCode != 0 : - result = "ERROR" - State.updateJobState (jobId, result) - State.updateWorkerState (hostname, result) - except KeyError: - pass - State.update () - return "1" - -# Backup the DB -# Erase master_db.maxBackup -# Rename master_db.N in master_db.N+1 -# Copy master_db in master_db.1 -def backup (): - global BackupTime, BackupLastTime, BackupMax - if time.time() - BackupLastTime > BackupTime: - # Remove the last backup - try: - os.remove (dataDir + "/master_db." + str(BackupMax)) - output ('remove ' + dataDir + "/master_db." + str(BackupMax)) - except OSError: - pass - - # Rename the backups - for i in range (BackupMax,1,-1): - try: - os.rename (dataDir + "/master_db." + str(i-1), dataDir + "/master_db." + str(i)) - output ('rename ' + dataDir + "/master_db." + str(i-1) + ' in ' + dataDir + "/master_db." + str(i)) - except OSError: - pass - - # Copy the last db - try: - shutil.copy2 (dataDir + "/master_db", dataDir + "/master_db.1") - output ('copy ' + dataDir + "/master_db in " + dataDir + "/master_db.1") - except OSError: - pass - BackupLastTime = time.time() - -# Write the DB on disk -SaveCoroutine = None -def saveDb (): - global State, dataDir, SaveCoroutine, SaveTime - - delai = SaveTime - - if SaveCoroutine == None and State._UpdatedDb: - output ("Start coroutine") - State._UpdatedDb = False - SaveCoroutine = State.write () - - if SaveCoroutine != None: - output ("Continue coroutine") - if SaveCoroutine.next (): - delai = 1 - else : - output ("Stop coroutine") - SaveCoroutine = None - - reactor.callLater(delai, saveDb) - -# Read the DB from disk -def readDb (): - global State, dataDir - output ("Read DB") - try: - try: - fo = open(dataDir + "/master_db", "rb") - except IOError: - output ("No db found, create a new one") - State = CState() - return - State.read (fo) - except: - print ("Error reading " + dataDir + "/master_db" + " ! Quit !") - sys.exit (1) - output ("DB is OK") - # Touch every working job - _time = time.time() - for id, job in State.Jobs.iteritems () : - if job.State == "WORKING": - job.PingTime = _time - output ("DB is OK") - -# Listen to an UDP socket to respond to workers broadcasts -def listenUDP(): - from socket import SOL_SOCKET, SO_BROADCAST - from socket import socket, AF_INET, SOCK_DGRAM, error - s = socket (AF_INET, SOCK_DGRAM) - s.bind (('0.0.0.0', port)) - while 1: - try: - data, addr = s.recvfrom (1024) - s.sendto ("roxor", addr) - except: - pass - -def main(): - # Start the UDP server used for the broadcast - thread.start_new_thread (listenUDP, ()) - - from twisted.internet import reactor - from twisted.web import server - root = Root("public_html") - webService = Master() - readDb () - workers = Workers() - root.putChild('xmlrpc', webService) - root.putChild('json', webService) - root.putChild('workers', workers) - output ("Listen on port " + str (port)) - reactor.listenTCP(port, server.Site(root)) - reactor.callLater(5, saveDb) - reactor.run() - -def sendEmail (to, message) : - if to != "" : - output ("Send email to " + to + " : " + message) - if smtphost != "" : - # Create a text/plain message - msg = MIMEText(message) - - # me == the sender's email address - # you == the recipient's email address - msg['Subject'] = message - msg['From'] = smtpsender - msg['To'] = to - - # Send the message via our own SMTP server, but don't include the - # envelope header. - try: - s = smtplib.SMTP(smtphost, smtpport) - if smtptls: - s.ehlo() - s.starttls() - s.ehlo() - if smtplogin != '' or smtppasswd != '': - s.login(smtplogin, smtppasswd) - s.sendmail (smtpsender, [to], msg.as_string()) - s.quit() - except Exception as inst: - output (inst) - pass - -def notifyError (job): - if job.User : - sendEmail (job.User, 'ERRORS in job ' + job.Title + ' (' + str(job.ID) + ').') - -def notifyFinished (job): - if job.User : - sendEmail (job.User, 'The job ' + job.Title + ' (' + str(job.ID) + ') is FINISHED.') - -def notifyFirstFinished (job): - if job.User : - sendEmail (job.User, 'The job ' + job.Title + ' (' + str(job.ID) + ') has finished ' + str(notifyafter) + ' jobs.') - -if sys.platform=="win32" and service: - - # Windows Service - import win32serviceutil - import win32service - import win32event - - class WindowsService(win32serviceutil.ServiceFramework): - _svc_name_ = "CoalitionServer" - _svc_display_name_ = "Coalition Server" - - def __init__(self, args): - output ("Service init") - win32serviceutil.ServiceFramework.__init__(self, args) - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - - def SvcStop(self): - output ("Service stop") - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - win32event.SetEvent(self.hWaitStop) - - def SvcDoRun(self): - output ("Service running") - import servicemanager - self.CheckForQuit() - main() - output ("Service quitting") - - def CheckForQuit(self): - output ("Checking for quit...") - retval = win32event.WaitForSingleObject(self.hWaitStop, 10) - if not retval == win32event.WAIT_TIMEOUT: - # Received Quit from Win32 - reactor.stop() - - reactor.callLater(1.0, self.CheckForQuit) - - if __name__=='__main__': - win32serviceutil.HandleCommandLine(WindowsService) -else: - - # Simple server - if __name__ == '__main__': - main() - +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Coalition server. +""" + +from twisted.web import xmlrpc, server, static, http +from twisted.internet import defer, reactor +from twisted.web.server import Session +import cPickle, time, os, getopt, sys, base64, re, thread, ConfigParser, random, shutil +import atexit, json +import smtplib +from email.mime.text import MIMEText +from textwrap import dedent, fill + +from db_sql import LdapError + + +### Functions ### + +# Configuration functions +def cfgInt (name, defvalue): + global config + if config.has_option('server', name): + try: + return int (config.get('server', name)) + except: + pass + return defvalue + + +def cfgBool (name, defvalue): + global config + if config.has_option('server', name): + try: + return int (config.get('server', name)) != 0 + except: + pass + return defvalue + + +def cfgStr (name, defvalue): + global config + if config.has_option('server', name): + try: + return str (config.get('server', name)) + except: + pass + return defvalue + +def usage(): + print ("Usage: server [OPTIONS]") + print ("Start a Coalition server.\n") + print ("Options:") + print (" -h, --help\t\tShow this help") + print (" -p, --port=PORT\tPort used by the server (default: "+str(port)+")") + print (" -v, --verbose\t\tIncrease verbosity") + print (" --init\t\tInitialize the database") + print (" --migrate\t\tMigrate the database with interactive confirmation") + print (" --reset\t\tReset the database (warning: all previous data are lost)") + if sys.platform == "win32": + print (" -c, --console=\t\tRun as a windows console application") + print (" -s, --service=\t\tRun as a windows service") + print ("\nExample : server -p 1234") + + +# Log functions +def vprint (str): + if verbose: + print (str) + sys.stdout.flush() + +def getLogFilename (jobId): + global dataDir + return dataDir + "/logs/" + str(jobId) + ".log" + + +def getLogFilter (pattern): + """Get the pattern filter from the cache or add one""" + global LogFilterCache + try: + filter = LogFilterCache[pattern] + except KeyError: + filter = LogFilter (pattern) + LogFilterCache[pattern] = filter + return filter + + +def writeJobLog (jobId, log): + logFile = open (getLogFilename (jobId), "a") + logFile.write (log) + logFile.close () + +# Notify functions +def sendEmail (to, message) : + if to != "" : + vprint ("Send email to " + to + " : " + message) + if smtphost != "" : + # Create a text/plain message + msg = MIMEText(message) + + # me == the sender's email address + # you == the recipient's email address + msg['Subject'] = message + msg['From'] = smtpsender + msg['To'] = to + + # Send the message via our own SMTP server, but don't include the + # envelope header. + try: + s = smtplib.SMTP(smtphost, smtpport) + if smtptls: + s.ehlo() + s.starttls() + s.ehlo() + if smtplogin != '' or smtppasswd != '': + s.login(smtplogin, smtppasswd) + s.sendmail (smtpsender, [to], msg.as_string()) + s.quit() + except Exception as inst: + vprint (inst) + pass + + +def notifyError (job): + if job['user'] : + sendEmail (job['user'], 'ERRORS in job ' + job['title'] + ' (' + str(job['id']) + ').') + + +def notifyFinished (job): + if job['user'] : + sendEmail (job['user'], 'The job ' + job['title'] + ' (' + str(job['id']) + ') is FINISHED.') + + +def notifyFirstFinished (job): + if job['user'] : + sendEmail (job['user'], 'The job ' + job['title'] + ' (' + str(job['id']) + ') has finished ' + str(notifyafter) + ' jobs.') + +def _interactiveConfirmation(confirmation_sentence="Yes I know what I'm doing."): + """Ask the user for confirmation.""" + text = "Please write this sentence then press enter to confirm:\n"+confirmation_sentence+'\n' + print (text) + sys.stdout.flush() + answer = raw_input() + if answer == confirmation_sentence: + return True + return False + + +### LDAP functions ### + +### LDAP classes and functions ### +def authenticate(request, ldap_permissions): + """Check user authentication via LDAP if LDAP is configured in settings. If authenticated, get users permissions.""" + + def _getLdapPermissions(connection, username): + ldap_base = cfgStr("ldapbase", "") + + def _ldapSearch(connection, query): + if connection.search_ext_s(ldap_base, ldap.SCOPE_SUBTREE, query, ['dn']): + return True + return False + + for permission in ldap_permissions.keys(): + search_template = cfgStr(permission, "").replace("__login__", username) + ldap_permissions[permission] = _ldapSearch(connection, search_template) + + return ldap_permissions + + if LDAPServer: + username = request.getUser() + password = request.getPassword() + + if config.has_option("server", "ldapunsafeapi") and config.getboolean("server", "ldapunsafeapi") and not isWebFrontend(request): + # This request does not comes from the webfrontend and unsafe mode is set. + # Granting full access. + vprint("[LDAP] Access granted for unsafe API") + for k in ldap_permissions.keys(): + ldap_permissions[k] = True + return True, ldap_permissions + + if username or password: + l = ldap.initialize(LDAPServer) + vprint("[LDAP] Authenticate {}".format(username)) + ldapUsername = LDAPTemplateLogin.replace("__login__", username) + try: + if l.bind_s(ldapUsername, password, ldap.AUTH_SIMPLE): + vprint("[LDAP] Authentication accepted for user {}".format(username)) + request.addCookie("authenticated_user", username, path="/") + ldap_permissions = _getLdapPermissions(l, username) + return True, ldap_permissions + + except ldap.LDAPError as e: + vprint("[LDAP] Authentication failed for user {}".format(username)) + vprint("[LDAP] {}".format(e)) + pass + else: + vprint("[LDAP] Authentication required") + request.setHeader("WWW-Authenticate", 'Basic realm="Coalition login"') + request.setResponseCode(http.UNAUTHORIZED) + return False, {} + return True, ldap_permissions + +def grantAddJob(user, cmd): + """Check if the logged in user can add this command.""" + def checkWhiteList(wl): + for pattern in wl: + if (re.match (pattern, cmd)): + return True + else: + vprint("[LDAP] Not authorized. User {} is not allowed to add the command {}".format(user, cmd)) + return False + + # Is user defined white list ? + if user in UserCmdWhiteList: + wl = UserCmdWhiteList[user] + if checkWhiteList(wl): + return True + # If in the global command white list + if GlobalCmdWhiteList: + if checkWhiteList(GlobalCmdWhiteList): + return True + return False + else: + # If in the global command white list + if GlobalCmdWhiteList: + if not checkWhiteList(GlobalCmdWhiteList): + return False + + # Cleared + return True + +def listenUDP(): + """Listen to UDP socket to respond to the workers broadcast.""" + from socket import SOL_SOCKET, SO_BROADCAST + from socket import socket, AF_INET, SOCK_DGRAM, error + s = socket (AF_INET, SOCK_DGRAM) + s.bind (('0.0.0.0', port)) + while 1: + try: + data, addr = s.recvfrom (1024) + s.sendto ("roxor", addr) + except: + pass + + +def main(): + """Start the UDP server used for the broadcast.""" + thread.start_new_thread (listenUDP, ()) + + from twisted.internet import reactor + from twisted.web import server + root = Root("public_html") + webService = Master() + workers = Workers() + root.putChild('xmlrpc', webService) + root.putChild('api', webService) + root.putChild('workers', workers) + vprint ("[Init] Listen on port " + str (port)) + reactor.listenTCP(port, server.Site(root)) + reactor.run() + + +### Classes ### + +class LogFilter: + """A log filter object. The log pattern must include a '%percent' or a '%one' key word.""" + + def __init__ (self, pattern): + # 0~100 or 0~1 ? + self.IsPercent = re.match (".*%percent.*", pattern) != None + + # Build the final pattern for the RE + if self.IsPercent: + pattern = re.sub ("%percent", "([0-9.]+)", pattern) + else: + pattern = re.sub ("%one", "([0-9.]+)", pattern) + + # Final progress filter + self.RE = re.compile(pattern) + + # Put it in the cache + global LogFilterCache + LogFilterCache[pattern] = self + + def filterLogs (self, log): + """Return the filtered log and the last progress, if any""" + progress = None + for m in self.RE.finditer (log): + capture = m.group(1) + try: + progress = float(capture) / (self.IsPercent and 100.0 or 1.0) + except ValueError: + pass + #return self.RE.sub ("", log), progress + return log, progress + + +def ldapUserAllowed(user, action): + """Check if user is allowed to do this action.""" + vprint("Is user {} allowed to do {}?".format(user, action)) + # Cleared + return True + +def isWebFrontend(request): + """Check if the request comes from the webfrontend.""" + m = re.match(r"^/api/webfrontend/", request.path) + if m: + return True + else: + return False + +### Twisted class ### +class Root(static.File): + """Create twisted landing page and check if LDAP authentication is required.""" + + def __init__(self, path, defaultType="text/html", ignoredExts=(), registry=None, allowExt=0): + static.File.__init__(self, path, defaultType, ignoredExts, registry, allowExt) + + def render(self, request): + if isWebFrontend(request): + (authenticated, permissions) = authenticate(request, ldap_permissions) + request.path = request.path.replace("webfrontend/", "", 1) + else: + authenticated = True + permissions = ldap_permissions + if authenticated: + return static.File.render(self, request) + request.setResponseCode(http.UNAUTHORIZED) + return "LDAP authorization required." + +### XMLRPC API classes ### +class Master(xmlrpc.XMLRPC): + """Defines XMLRPC and API for users interactions. Defines logger.""" + + def __init__(self): + self.user = "" # Default value, overwritten later in case of LDAP authentication + + def render(self, request): + with db: + vprint("[{}] {}".format(request.method, request.path)) + if isWebFrontend(request): + (authenticated, permissions) = authenticate(request, ldap_permissions) + request.path = request.path.replace("webfrontend/", "", 1) + else: + authenticated = True + permissions = ldap_permissions + if authenticated: + self.user = db.ldap_user = request.getUser() + db.permissions = permissions + + def getArg(name, default): + value = request.args.get(name, [default]) + return value[0] + + # The legacy method for compatibility + if request.path == "/xmlrpc/addjob": + parent = getArg("parent", "0") + title = getArg("title", "New job") + cmd = getArg("cmd", getArg("command", "")) + dir = getArg("dir", ".") + environment = getArg("env", None) + if environment == "": + environment = None + priority = getArg("priority", "1000") + timeout = getArg("timeout", "0") + affinity = getArg("affinity", "") + dependencies = getArg("dependencies", "") + progress_pattern = getArg("localprogress", "") + url = getArg("url", "") + user = getArg("user", "") + state = getArg("state", "WAITING") + paused = getArg("paused", "0") + if self.user != "": + user = self.user + + if grantAddJob(self.user, cmd): + vprint ("Add job: {}".format(cmd)) + # try as an int + parent = int(parent) + if type(dependencies) is str: + # Parse the dependencies string + dependencies = re.findall('(\d+)', dependencies) + for i, dep in enumerate(dependencies) : + dependencies[i] = int(dep) + + job = db.newJob (parent, str (title), str (cmd), str (dir), str (environment), + str (state), int (paused), int (timeout), int (priority), str (affinity), + str (user), str (url), str (progress_pattern)) + if job is not None: + db.setJobDependencies(job['id'], dependencies) + return str(job['id']) + return "-1" + else: + try: + value = request.content.getvalue() + if request.method != "GET": + data = value and json.loads(request.content.getvalue()) or {} + if verbose: + vprint ("[Content] {}".format(repr(data))) + else: + if verbose: + vprint ("[Content] {}".format(repr(request.args))) + + def getArg(name, default): + if request.method == "GET": + # GET params + value = request.args.get(name, [default])[0] + value = type(default)(default if value == None else value) + assert(value != None) + return value + else: + # JSON params + value = data.get(name) + value = type(default)(default if value == None else value) + assert(value != None) + return value + + def api_rest(): + """REST API.""" + + # REST PUT API + if request.method == "PUT": + if request.path == "/api/jobs": + if grantAddJob(self.user, getArg("command","")): + job = db.newJob ((getArg("parent",0)), + (getArg("title","")), + (getArg("command","")), + (getArg("dir","")), + (getArg("environment","")), + (getArg("state","WAITING")), + (getArg("paused",0)), + (getArg("timeout",1000)), + (getArg("priority",1000)), + (getArg("affinity", "")), + (getArg("user", "")), + (getArg("url", "")), + (getArg("progress_pattern", "")), + (getArg("dependencies", []))) + return job['id'] + else: + return False + + # REST GET API + elif request.method == "GET": + m = re.match(r"^/api/jobs/(\d+)$", request.path) + if m: + return db.getJob(int(m.group (1))) + m = re.match(r"^/api/jobs/(\d+)/children$", request.path) + if m: + return db.getJobChildren(int(m.group (1)), {}) + m = re.match(r"^/api/jobs/(\d+)/dependencies$", request.path) + if m: + return db.getJobDependencies(int(m.group (1))) + m = re.match(r"^/api/jobs/(\d+)/childrendependencies$", request.path) + if m: + return db.getChildrenDependencyIds(int(m.group (1))) + m = re.match(r"^/api/jobs/(\d+)/log$", request.path) + if m: + return self.getLog(int(m.group (1))) + if request.path == "/api/jobs": + return db.getJobChildren(0, {}) + + m = re.match(r"^/api/jobs/count/where/$", request.path) + if m: + return db.getCountJobsWhere(request.args["where_clause"]) + + m = re.match(r"^/api/jobs/where/$", request.path) + if m: + return db.getJobsWhere( + where_clause=request.args["where_clause"][0], + index_min=request.args["min"][0], + index_max=request.args["max"][0], + ) + + if request.path == "/api/workers": + return db.getWorkers() + if request.path == "/api/events": + return db.getEvents(getArg("job", -1), getArg("worker", ""), getArg("howlong", -1)) + if request.path == "/api/affinities": + return db.getAffinities() + + if request.path == "/api/jobs/users/": + return db.getJobsUsers() + + if request.path == "/api/jobs/states/": + return db.getJobsStates() + + if request.path == "/api/jobs/workers/": + return db.getJobsWorkers() + + if request.path == "/api/jobs/priorities/": + return db.getJobsPriorities() + + if request.path == "/api/jobs/affinities/": + return db.getJobsAffinities() + + # REST POST API + elif request.method == "POST": + if request.path == "/api/jobs": + db.editJobs(data) + return 1 + if request.path == "/api/workers": + db.editWorkers(data) + return 1 + m = re.match(r"^/api/jobs/(\d+)/dependencies$", request.path) + if m: + db.setJobDependencies(int(m.group (1)), data) + return 1 + if request.path == "/api/resetjobs": + for jobId in data: + db.resetJob(int(jobId)) + return 1 + if request.path == "/api/reseterrorjobs": + for jobId in data: + db.resetErrorJob(int(jobId)) + return 1 + if request.path == "/api/startjobs": + for jobId in data: + db.startJob(int(jobId)) + return 1 + if request.path == "/api/pausejobs": + for jobId in data: + db.pauseJob(int(jobId)) + return 1 + if request.path == "/api/stopworkers": + for name in data: + db.stopWorker(name) + return 1 + if request.path == "/api/startworkers": + for name in data: + db.startWorker(name) + return 1 + if request.path == "/api/affinities": + db.setAffinities(data) + return 1 + if request.path == "/api/terminateworkers": + if servermode != "normal": # Cloud mode + for name in data: + db.cloudmanager.stopInstance(name) + db._setWorkerState(name, "TERMINATED") + return 1 + else: + return None + + # REST DELETE API + elif request.method == "DELETE": + if request.path == "/api/jobs": + for jobId in data: + deletedJobs = [] + db.deleteJob(int(jobId), deletedJobs) + for deleteJobId in deletedJobs: + self.deleteLog(deleteJobId) + return 1 + if request.path == "/api/workers": + for name in data: + db.deleteWorker(name) + return 1 + + result = api_rest () + if result != None: + # Only JSON right now + return json.dumps(result) + else: + # return server.NOT_DONE_YET + request.setResponseCode(404) + return "Web service not found." + except LdapError as error: + vprint(error) + request.setResponseCode(http.UNAUTHORIZED) + return "LDAP authorization required." + + def getLog (self, jobId): + # Look for the job + log = "" + try: + logFile = open (getLogFilename (jobId), "r") + while (1): + # Read some lines of logs + line = logFile.readline() + # "" means EOF + if line == "": + break + log = log + line + logFile.close () + except IOError: + pass + return log + + def deleteLog (self, jobId): + # Look for the job + try: + os.remove (getLogFilename (jobId)) + except OSError: + pass + + +class Workers(xmlrpc.XMLRPC): + """Unauthenticated XmlRPC server for Worker.""" + + def render (self, request): + with db: + vprint ("[" + request.method + "] "+request.path) + def getArg (name, default): + value = request.args.get (name, [default]) + return value[0] + + if request.path == "/workers/heartbeat": + return self.json_heartbeat (getArg ('hostname', ''), getArg ('jobId', '-1'), getArg ('log', ''), getArg ('load', '[0]'), getArg ('free_memory', '0'), getArg ('total_memory', '0'), request.getClientIP ()) + elif request.path == "/workers/pickjob": + return self.json_pickjob (getArg ('hostname', ''), getArg ('load', '[0]'), getArg ('free_memory', '0'), getArg ('total_memory', '0'), request.getClientIP ()) + elif request.path == "/workers/endjob": + return self.json_endjob (getArg ('hostname', ''), getArg ('jobId', '1'), getArg ('errorCode', '0'), request.getClientIP ()) + else: + # return server.NOT_DONE_YET + return xmlrpc.XMLRPC.render (self, request) + + def json_heartbeat (self, hostname, jobId, log, load, free_memory, total_memory, ip): + result = db.heartbeat (hostname, int(jobId), load, int(free_memory), int(total_memory), str(ip)) + if log != "" : + try: + logFile = open (getLogFilename (jobId), "a") + log = base64.decodestring(log) + + # Filter the log progression message + progress = None + job = db.getJob (int (jobId)) + progress_pattern = getattr (job, "progress_pattern", DefaultLocalProgressPattern) + if progress_pattern != "": + vprint ("progressPattern : \n" + str(progress_pattern)) + lp = None + gp = None + lFilter = getLogFilter (progress_pattern) + log, lp = lFilter.filterLogs (log) + if lp != None: + vprint ("lp : "+ str(lp)+"\n") + if lp != job['progress']: + db.setJobProgress (int (jobId), lp) + logFile.write (log) + if not result: + logFile.write ("KillJob: server required worker to kill job.\n") + logFile.close () + except IOError: + vprint ("Error in logs") + return result and "true" or "false" + + def json_pickjob (self, hostname, load, free_memory, total_memory, ip): + return str (db.pickJob (hostname, load, int(free_memory), int(total_memory), str(ip))) + + def json_endjob (self, hostname, jobId, errorCode, ip): + return str (db.endJob (hostname, int(jobId), int(errorCode), str(ip))) + +GErr=0 +GOk=0 + +# Go to the script directory +global installDir, dataDir +if sys.platform=="win32": + import _winreg + # under windows, uses the registry setup by the installer + try: + hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Mercenaries Engineering\\Coalition", 0, _winreg.KEY_READ) + installDir, _type = _winreg.QueryValueEx (hKey, "Installdir") + dataDir, _type = _winreg.QueryValueEx (hKey, "Datadir") + except OSError: + installDir = "." + dataDir = "." +else: + installDir = "." + dataDir = "." +os.chdir (installDir) + +# Create the logs/ directory +try: + os.mkdir (dataDir + "/logs", 0755); +except OSError: + pass + +config = ConfigParser.SafeConfigParser() +config.read ("coalition.ini") + +# Default config file values +if not config.has_section('server'): + config.add_section("server") +if not config.has_option("server", "db_type"): + config.set ("server", "db_type", "sqlite") + +port = cfgInt ('port', 19211) + +timeout = cfgInt ('timeout', 60) +verbose = cfgBool ('verbose', False) +service = cfgBool ('service', False) +notifyafter = cfgInt ('notifyafter', 10) +decreasepriorityafter = cfgInt ('decreasepriorityafter', 10) +smtpsender = cfgStr ('smtpsender', "") +smtphost = cfgStr ('smtphost', "") +smtpport = cfgInt ('smtpport', 587) +smtptls = cfgBool ('smtptls', True) +smtplogin = cfgStr ('smtplogin', "") +smtppasswd = cfgStr ('smtppasswd', "") + +# LDAP and permissions +LDAPServer = cfgStr ('ldaphost', "") +LDAPTemplateLogin = cfgStr ('ldaptemplatelogin', "") +_CmdWhiteList = cfgStr ('commandwhitelist', "") +GlobalCmdWhiteList = None +UserCmdWhiteList = {} +UserCmdWhiteListUser = None +for line in _CmdWhiteList.splitlines (False): + _re = re.match ("^@(.*)", line) + if _re: + UserCmdWhiteListUser = _re.group(1) + if not UserCmdWhiteListUser in UserCmdWhiteList: + UserCmdWhiteList[UserCmdWhiteListUser] = [] + else: + if UserCmdWhiteListUser: + UserCmdWhiteList[UserCmdWhiteListUser].append (line) + else: + if not GlobalCmdWhiteList: + GlobalCmdWhiteList = [] + GlobalCmdWhiteList.append (line) + +ldap_permissions = { + "ldaptemplatecreatejob": True, + "ldaptemplateviewjob": True, + "ldaptemplateeditjob": True, + "ldaptemplatedeletejob": True, + "ldaptemplatecreatejobglobal": True, + "ldaptemplateviewjobglobal": True, + "ldaptemplateeditjobglobal": True, + "ldaptemplatedeletejobglobal": True, + } + +DefaultLocalProgressPattern = "PROGRESS:%percent" +DefaultGlobalProgressPattern = None + +# Service only on Windows +service = service and sys.platform == "win32" + +migratedb = False +resetdb = False +initdb = False + +# Cloud mode +servermode = cfgStr ('servermode', 'normal') +if servermode != "normal": + cloudconfig = ConfigParser.SafeConfigParser() + if servermode == "aws": + cloudconfig.read("cloud_aws.ini") + elif servermode == "gcloud": + cloudconfig.read("cloud_gcloud.ini") + elif servermode == "qarnot_api": + cloudconfig.read("cloud_qarnot.ini") + cloudconfig.set("coalition", "port", str(port)) +else: + cloudconfig = None + +# Parse the options +try: + opts, args = getopt.getopt(sys.argv[1:], "hp:vcs", ["help", "port=", + "verbose", "init", "migrate", "reset"]) + if len(args) != 0: + usage() + sys.exit(2) +except getopt.GetoptError, err: + # print help information and exit: + print str(err) # will print something like "option -a not recognized" + usage() + sys.exit(2) +for o, a in opts: + if o in ("-h", "--help"): + usage () + sys.exit(2) + elif o in ("-v", "--verbose"): + verbose = True + elif o in ("-p", "--port"): + port = int(a) + elif o in ("--migrate"): + migratedb = True + elif o in ("--reset"): + resetdb = True + elif o in ("--init"): + initdb = True + else: + assert False, "unhandled option " + o + + if LDAPServer != "": + import ldap + +if not verbose or service: + try: + outfile = open(dataDir + '/server.log', 'a') + sys.stdout = outfile + sys.stderr = outfile + def _closeLogFile(): + outfile.close() + atexit.register(_closeLogFile) + except: + pass + +vprint ("[Init] --- Start ------------------------------------------------------------") +print ("[Init] "+time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime(time.time ()))) +if service: + vprint ("[Init] Running service") +else: + vprint ("[Init] Running standard console") + +# Init the good database +if cfgStr ('db_type', 'sqlite') == "mysql": + vprint ("[Init] Use mysql") + from db_mysql import DBMySQL + db = DBMySQL (cfgStr ('db_mysql_host', "127.0.0.1"), cfgStr ('db_mysql_user', ""), cfgStr ('db_mysql_password', ""), cfgStr ('db_mysql_base', "base"), config=config, cloudconfig=cloudconfig) +else: + vprint ("[Init] Use sqlite") + from db_sqlite import DBSQLite + db = DBSQLite (cfgStr ('db_sqlite_file', "coalition.db"), config=config, cloudconfig=cloudconfig) + +db.NotifyError = notifyError +db.NotifyFinished = notifyFinished +db.Verbose = verbose + +if initdb: + vprint ("[Init] Initial database setup") + if not db.initDatabase(): + exit(1) + +if not len(db._getDatabaseTables()): + db.initDatabase () + +with db: + requires_migration = db.requiresMigration() + if not migratedb and requires_migration: + print(dedent(""" + Coalition cannot start since the database schema and the source code + are not compatible. The database needs to be migrated. First the + database should be backuped in case the migration fails. Then, the + command 'coalition server.py --verbose --migrate' should be run. + Another option is to install the previous version of coalition code + that worked with the current database schema.""")) + exit(1) + + if migratedb and not requires_migration: + print(dedent(""" + The database does not require migration, but the '--migrate' parameter was provided.""")) + exit(1) + + if requires_migration and migratedb: + print(dedent(""" + Please consider doing a backup of the database first. Are you ready to proceed?""")) + if _interactiveConfirmation("Yes, proceed to migration!"): + success = db.migrateDatabase() + if success: + print("Database migration was successfull.") + exit(0) + else: + print("A problem occured during the database migration.") + exit(1) + else: + print("Database migration was cancelled by user.") + exit(0) + + if resetdb: + db.reset () + +LogFilterCache = {} + + +### Main manager ### + +if sys.platform=="win32" and service: + # Windows Service + import win32serviceutil + import win32service + import win32event + + class WindowsService(win32serviceutil.ServiceFramework): + _svc_name_ = "CoalitionServer" + _svc_display_name_ = "Coalition Server" + + def __init__(self, args): + vprint ("[Init] Service init") + win32serviceutil.ServiceFramework.__init__(self, args) + self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) + + def SvcStop(self): + vprint ("[Stop] Service stop") + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + win32event.SetEvent(self.hWaitStop) + + def SvcDoRun(self): + vprint ("[Run] Service running") + import servicemanager + self.CheckForQuit() + main() + vprint ("Service quitting") + + def CheckForQuit(self): + vprint ("[Stop] Checking for quit...") + retval = win32event.WaitForSingleObject(self.hWaitStop, 10) + if not retval == win32event.WAIT_TIMEOUT: + # Received Quit from Win32 + reactor.stop() + + reactor.callLater(1.0, self.CheckForQuit) + + if __name__=='__main__': + win32serviceutil.HandleCommandLine(WindowsService) +else: + # Simple server + if __name__ == '__main__': + main() + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/setup_py2exe.py b/setup_py2exe.py index 6a3c9df..ce6ff15 100644 --- a/setup_py2exe.py +++ b/setup_py2exe.py @@ -1,3 +1,9 @@ -from distutils.core import setup -import py2exe -setup(service=['server', 'worker_service'], console=['worker.py','control.py'], options = {"py2exe": { "dll_excludes": ["MSWSOCK.dll","POWRPROF.dll"]}}) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from distutils.core import setup +import py2exe +setup(service=['server', 'worker_service'], console=['worker.py','control.py'], options = {"py2exe": { "dll_excludes": ["MSWSOCK.dll","POWRPROF.dll"]}}) + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/test_addjobs.py b/test_addjobs.py deleted file mode 100644 index 543ce57..0000000 --- a/test_addjobs.py +++ /dev/null @@ -1,12 +0,0 @@ -import os - -nbP = 20 # nb parents -nbC = 10 # nb children - -for p in range(0,nbP): - pipe = os.popen ('python control.py -c "" -t "Parent job' + str(p) + '" http://127.0.0.1:19211 add') - parent = int(pipe.read ()) - - for c in range(0,nbC): - pipe = os.popen ('python control.py -c "python job.py" -t "Job ' + str(c) + '" -P ' + str(parent) + ' http://127.0.0.1:19211 add') - child = int(pipe.read ()) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/main_test.py b/tests/main_test.py new file mode 100755 index 0000000..a020ffb --- /dev/null +++ b/tests/main_test.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys, os, time, subprocess, unittest +sys.path.append(os.path.abspath(".")) +from api import coalition + +HOST="localhost" +PORT="19211" +NUM_WORKERS = 4 +NUM_JOBS = 10 +VERBOSITY = 5 + +def test_server_python_api(): + tests = [ + "test_newJob", + "test_getJob", + "test_getWorkers", + "test_editWorkers", + "test_priorities", + #"test_affinities_first", + "test_setJobDependencies", + "test_states", + "test_children_finish_before_parent", + "test_no_job_error",] + suite = unittest.TestSuite(map(ServerPythonApiTestCase, tests)) + return unittest.TextTestRunner(verbosity=VERBOSITY).run(suite) + +def test_server_xmlrpc(): + tests = ['test_setJobDependencies',] + suite = unittest.TestSuite(map(ServerXmlrpcTestCase, tests)) + unittest.TextTestRunner(verbosity=VERBOSITY).run(suite) + +def launch_server(): + """Launch a coalition server.""" + # The --init parameter prevents database overwriting. The database has to be + # initially empty. + cmd = ["python", "server.py"] + return subprocess.Popen(cmd) + + +def launch_worker(identifier): + """Launch coalition worker.""" + cmd = ["python", "worker.py", "-n", identifier, "http://{}:{}".format(HOST,PORT)] + return subprocess.Popen(cmd) + + +class ServerPythonApiTestCase(unittest.TestCase): + @classmethod + def setUpClass(self): + self.server = launch_server() + time.sleep(5) + if self.server.poll() is not None: + print("Server failed to start.") + exit(1) + self.workers = [launch_worker("worker-{}".format(i)) for i in range(2)] + self.conn = coalition.Connection(HOST, PORT) + affinities = dict() + for i in range(1, 65): + affinities[str(i)] = "" + affinities["1"] = "linux" + affinities["2"] = "win" + affinities["3"] = "windows project" + affinities["4"] = "windows" + affinities["5"] = "dos" + self.conn.setAffinities(affinities) + self.depJobID = self.conn.newJob(command="echo dependencies", title="jobDependencies", state='PAUSED') + self.parentID = self.conn.newJob(title="parent") + self.childrenID = [self.conn.newJob(command="echo 'job-{}'".format(i), + title="job-{}".format(i), parent=self.parentID, state='PAUSED') for i in range(NUM_JOBS)] + self.firstSleepJobId = self.conn.newJob(command="sleep 2", title="First Job", state="WAITING", affinity="linux", priority=129) + self.secondSleepJobId = self.conn.newJob(command="sleep 2", + title="Second Job", state="WAITING", affinity="linux", priority=128) + self.windowsProjectJobId = self.conn.newJob(command="sleep 2", title="windows project", state="WAITING", affinity="windows project", priority=127) + self.winJobId = self.conn.newJob(command="sleep 2", title="Win", state="WAITING", affinity="win", priority=127) + self.dosJobId = self.conn.newJob(command="sleep 2", title="Dos", state="WAITING", affinity="dos", priority=127) + self.basicJobId = self.conn.newJob(command="sleep 2", title="Basic", + state="WAITING", priority=300) + + @classmethod + def tearDownClass(self): + for worker in self.workers: + worker.terminate() + self.server.terminate() + + def test_newJob(self): + self.assertNotEqual(self.depJobID, None) + self.assertNotEqual(self.parentID, None) + + def test_getJob(self): + depJob = self.conn.getJob(self.depJobID) + firstSleepJob = self.conn.getJob(self.firstSleepJobId) + secondSleepJob = self.conn.getJob(self.secondSleepJobId) + + self.assertEqual(depJob.id, self.depJobID) + self.assertEqual(depJob.title, 'jobDependencies') + self.assertEqual(depJob.command, 'echo dependencies') + self.assertEqual(depJob.state, "PAUSED") + + self.assertEqual(firstSleepJob.id, self.firstSleepJobId) + self.assertEqual(firstSleepJob.title, "First Job") + #self.assertEqual(firstSleepJob.state, "WAITING") + self.assertEqual(firstSleepJob.affinity, "linux") + self.assertEqual(firstSleepJob.priority, 129) + + self.assertEqual(secondSleepJob.id, self.secondSleepJobId) + self.assertEqual(secondSleepJob.title, "Second Job") + #self.assertEqual(secondSleepJob.state, "WAITING") + self.assertEqual(secondSleepJob.affinity, "linux") + self.assertEqual(secondSleepJob.priority, 128) + + def test_getWorkers(self): + for k in range(1,10): + workers = self.conn.getWorkers() + if len(workers) >= 2: + break + time.sleep (1) + + self.assertEqual(len(workers), 2) + + def test_editWorkers(self): + workers = self.conn.getWorkers() + new_workers = dict() + new_workers[workers[0]['name']] = dict() + new_workers[workers[0]['name']]['affinity'] = "windows\nlinux" + new_workers[workers[1]['name']] = dict() + new_workers[workers[1]['name']]['affinity'] = "windows project\nwin\ndos" + self.assertEqual(self.conn.editWorkers(new_workers), '1') + + def test_priorities(self): + firstSleepJob = self.conn.getJob(self.firstSleepJobId) + secondSleepJob = self.conn.getJob(self.secondSleepJobId) + while not (secondSleepJob.state == "FINISHED") & (firstSleepJob.state == "FINISHED"): + time.sleep(1) + firstSleepJob = self.conn.getJob(self.firstSleepJobId) + secondSleepJob = self.conn.getJob(self.secondSleepJobId) + self.assertTrue(secondSleepJob.start_time > firstSleepJob.start_time) + + def test_affinities_first(self): + # This test does not work properly with MySQL db + # The Affinity logic in pickJob is a bit bizarre and this + # test does not make a clear sense. + # Disabled in the test list + windowsProjectJob = self.conn.getJob( self.windowsProjectJobId ) + winJob = self.conn.getJob( self.winJobId ) + dosJob = self.conn.getJob( self.dosJobId ) + basicJob = self.conn.getJob( self.basicJobId ) + + while not ( windowsProjectJob.state == "FINISHED" ) & ( winJob.state == "FINISHED" ) & ( dosJob.state == "FINISHED" ) & ( basicJob.state == "FINISHED" ): + windowsProjectJob = self.conn.getJob( self.windowsProjectJobId ) + winJob = self.conn.getJob( self.winJobId ) + dosJob = self.conn.getJob( self.dosJobId ) + basicJob = self.conn.getJob( self.basicJobId ) + + self.assertTrue( windowsProjectJob.start_time < winJob.start_time ) + self.assertTrue( winJob.start_time < dosJob.start_time ) + self.assertTrue( windowsProjectJob.start_time < dosJob.start_time ) + self.assertTrue( ( dosJob.start_time < basicJob.start_time ) | ( dosJob.worker != basicJob.worker ) ) + + def test_setJobDependencies(self): + self.conn.setJobDependencies (self.depJobID, self.childrenID) + depsJobs = self.conn.getJobDependencies (self.depJobID) + depsID = [job.id for job in depsJobs] + with self.conn: + depJob = self.conn.getJob(self.depJobID) + depJob.state = "WAITING" + self.assertTrue(any(map(lambda v: v in self.childrenID, depsID))) + + def test_states(self): + with self.conn: + for job in self.conn.getJobChildren(self.parentID): + job.state = "WAITING" + depJob = self.conn.getJob (self.depJobID) + self.assertNotEqual(depJob.state, "PAUSED") + + def test_children_finish_before_parent(self): + for i in self.childrenID: + job = self.conn.getJob (i) + with self.conn: + job.state = "WAITING" + + depJob = self.conn.getJob (self.depJobID) + while depJob.state != "FINISHED": + depJob = self.conn.getJob (self.depJobID) + time.sleep(1) + + def test_no_job_error(self): + parent = self.conn.getJob (self.parentID) + self.assertEqual (parent.state, "FINISHED") + self.assertEqual (parent.working, 0) + self.assertEqual (parent.errors, 0) + self.assertEqual (parent.run_done, 0) + + +class ServerXmlrpcTestCase(unittest.TestCase): + @classmethod + def setUpClass(self): + self.server = launch_server() + self.workers = [launch_worker("worker1"), launch_worker("worker2")] + self.conn = coalition.Connection(HOST, PORT) + + @classmethod + def tearDownClass(self): + for worker in self.workers: + worker.terminate() + self.server.terminate() + + def test_setJobDependencies(self): + """set job2 dependent on job1""" + job1 = self.conn.newJob(0, "Test-1", "ls /", ".", "", "WAITING", False, 100, 15, "" , "", "", "")['id'] + job2 = self.conn.newJob(0, "Test-2", "ls /", ".", "", "WAITING", False, 100, 15, "" , "", "", "")['id'] + self.conn.setJobDependencies(job2, [job1]) + self.assertEqual(len(self.getJobDependencies(job2)), 1) + self.assertEqual(self.getJob(job2)['state'], "PENDING") + + def test_pick_job(self): + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], job1) + assertEqual(self.getWorker ('worker1') ['state'], "WORKING") + assertEqual(self.getJob(job1)['state'], "WORKING") + assertEqual(pick2[0], -1) + assertEqual(self.getJob(job2)['state'], "PENDING") + + def test_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + h2 = self.heartbeat ("worker2", pick2[0], 1, 1, 1, '127.0.0.1') + assertIsNotNone (h1) + assertEqual(self.getWorker('worker1')['state'], "WORKING") + assertIsNone(h2) + assertEqual(self.getWorker('worker2')['state'], "WAITING") + + def test_worker1_finish_job(self): + self.endJob("worker1", pick1[0], 0, "127.0.0.1") + assertEqual(self.getWorker('worker1')['state'], "WAITING") + assertEqual(self.getJob(job1)['state'], "FINISHED") + assertEqual(self.getJob(job2)['state'], "WAITING") + + def test_worker1_pick_job(self): + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], job2) + assertEqual(self.getWorker('worker1')['state'], "WORKING") + assertEqual(self.getJob(job2)['state'], "WORKING") + + def test_worker1_finish_job(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + assertEqual(self.getWorker('worker1')['state'], "WAITING") + assertEqual(self.getJob(job1) ['state'], "FINISHED") + + def test_worker2_pick_job(self): + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual (pick2[0], -1) + assertEqual (self.getWorker ('worker2') ['state'], "WAITING") + + def test_worker2_pick_job(self): + job3 = self.newJob (0, "Test-1", "ls /", ".", "", "PAUSED", False, 100, 15, "" , "", "", "") ['id'] + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual (pick2[0], -1) + assertEqual (self.getJob (job3) ['state'], "PAUSED") + assertEqual (self.getWorker ('worker2') ['state'], "WAITING") + + def test_start_job3(self): + self.startJob (job3) + assertEqual (self.getJob (job3)['state'], "WAITING") + + def test_worker2_pick_job(self): + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual (pick2[0], job3) + assertEqual (self.getJob (job3) ['state'], "WORKING") + assertEqual (self.getWorker ('worker2') ['state'], "WORKING") + + def test_worker2_error_job(self): + self.endJob ("worker2", pick2[0], 1, "127.0.0.1") + assertEqual (self.getJob (job3) ['state'], "ERROR") + assertEqual (self.getWorker ('worker2') ['state'], "WAITING") + + def test_worker2_pick_job(sefl): + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual (pick2[0], -1) + + def test_reset_job3(self): + self.resetJob (job3) + assertEqual (self.getJob (job3)['state'], "WAITING") + + def test_worker2_pick_job(self): + pick2 = self.pickJob ("worker2", 1, 1, 1, "127.0.0.1") + assertEqual (pick2[0], job3) + + def test_worker2_end_job(self): + self.endJob ("worker2", pick2[0], 0, "127.0.0.1") + assertEqual (self.getJob (job3) ['state'], "FINISHED") + assertEqual (self.getWorker ('worker2') ['state'], "WAITING") + + def test_worker1_pick_job(self): + job4 = self.newJob (0, "Test-1", "ls /", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + job5 = self.newJob (0, "Test-2", "ls /", ".", "", "WAITING", False, 100, 12, "" , "", "", "") ['id'] + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], job5) + + def test_delete_job4(self): + self.deleteJob (job4) + assertIsNone(self.getJob(job4)) + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNtNone (h1) + assertEqual(self.getWorker('worker1')['state'], "WORKING") + + def test_delete_job5(self): + self.deleteJob (job5) + assertIsNone(self.getJob(job5)) + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNone(h1) + assertEqual(self.getWorker('worker1')['state'], "WAITING") + + def test_pause_jobs(self): + job6 = self.newJob (0, "Test-1", "ls /", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + self.pauseJob (job6) + assertEqual (self.getJob (job6) ['state'], "PAUSED") + assertEqual (self.getJob (job6) ['h_paused'], True) + + def test_worker1_pick_job(self): + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], -1) + + def test_start_job6(self): + self.startJob (job6) + assertEqual (self.getJob (job6) ['state'], "WAITING") + assertEqual (self.getJob (job6) ['h_paused'], False) + + def test_worker1_pick_job(self): + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], job6) + assertEqual (self.getJob (job6) ['state'], "WORKING") + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNotNone (h1) + + def test_pause_job6(self): + self.pauseJob (job6) + assertEqual (self.getJob (job6) ['state'], "PAUSED") + assertEqual (self.getJob (job6) ['h_paused'], True) + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNone(h1) + + def test_start_job6(self): + self.startJob (job6) + assertEqual (self.getJob (job6) ['state'], "WAITING") + assertEqual (self.getJob (job6) ['h_paused'], False) + + def test_worker1_pick_job(self): + self.stopWorker ("worker1") + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], -1) + assertEqual (self.getWorker ('worker1') ['state'], "WAITING") + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNone(h1) + + def test_worker1_pick_job(self): + self.startWorker ("worker1") + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], job6) + assertEqual (self.getJob (job6) ['state'], "WORKING") + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNotNone(h1) + + def test_worker1_deleted(self): + self.deleteWorker ("worker1") + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNotNone(h1) + + def test_worker1_stopped(self): + self.stopWorker ("worker1") + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNone(h1) + + def test_delete_job6(self): + self.deleteJob (job6) + assertIsNone(self.getJob (job6)) + + def test_worker1_pick_job(self): + self.startWorker ("worker1") + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], -1) + + def test_create_job7(self): + job7 = self.newJob (0, "Parent-1", "", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], -1) + + def test_create_job8(self): + job8 = self.newJob (job7, "Child-1", "ls /", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual (pick1[0], job8) + + def test_worker1_heartbeats(self): + h1 = self.heartbeat ("worker1", pick1[0], 1, 1, 1, '127.0.0.1') + assertIsNotNone(h1) + + def test_worker1_finish_job_and_change_job7_priority(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + job8prio = self.getJob (job8) ['h_priority'] + self.editJobs ({ job7: { "priority": 12 } }) + assertLess(job8prio, self.getJob(job8)['h_priority']) + + def test_worker1_pick_job12(self): + job9 = self.newJob (0, "Parent-2", "", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + job10 = self.newJob (0, "Parent-3", "", ".", "", "WAITING", False, 100, 11, "" , "", "", "") ['id'] + job11 = self.newJob (job9, "Child-2", "ls /", ".", "", "WAITING", False, 100, 10, "" , "", "", "") ['id'] + job12 = self.newJob (job10, "Child-3", "ls /", ".", "", "WAITING", False, 100, 8, "" , "", "", "") ['id'] + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assert (pick1[0] == job12) + + def test_worker1_pick_job11(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assert (pick1[0] == job11) + + def test_worker1_finish_job(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + + + def test_intricate_dependencies(self): + # performs intricate dependencies testing + # like a group dependent on a paused job + # and job dependent on this group + job20 = self.newJob (0, "job20", "sleep 2", ".", "", "PAUSED", False, 0, 100, "" , "", "", "") ['id'] + job21 = self.newJob (0, "job21", "", ".", "", "WAITING", False, 0, 100, "" , "", "", "") ['id'] + self.setJobDependencies (job21, [ job20 ]) + job22 = self.newJob (job21, "job22", "sleep 2", ".", "", "WAITING", False, 0, 100, "" , "", "", "") ['id'] + job23 = self.newJob (0, "job23", "sleep 2", ".", "", "WAITING", False, 0, 150, "" , "", "", "") ['id'] + self.setJobDependencies (job23, [ job21 ]) + job24 = self.newJob (0, "job24", "sleep 2", ".", "", "WAITING", False, 0, 200, "" , "", "", "") ['id'] + self.setJobDependencies (job24, [ job20 ]) + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + # can't start any job, all are dependent on job20 which is paused + assertEqual(pick1[0], -1) + assertEqual(self.getJob(job20)['state'], 'PAUSED') + assertEqual(self.getJob(job21)['state'], 'PENDING') + assertEqual(self.getJob(job22)['state'], 'WAITING') + assertIsNotNone(self.getJob(job22)['h_paused']) + assertEqual(self.getJob(job23)['state'], 'PENDING') + assertEqual(self.getJob(job24)['state'], 'PENDING') + + def test_can_only_pick_jobs20(self): + self.startJob (job20) + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], job20) + + def test_worker1_finish_job(self): + self.endJob("worker1", pick1[0], 0, "127.0.0.1") + assertEqual(self.getJob(job20)['state'], 'FINISHED') + assertEqual(self.getJob(job21)['state'], 'WAITING') + assertEqual(self.getJob(job22)['state'], 'WAITING') + assertNotNone(not self.getJob(job22) ['h_paused']) + assertEqual(self.getJob(job23)['state'], 'PENDING') + assertEqual(self.getJob(job24)['state'], 'WAITING') + + def test_worker1_pick_jobs24(self): + # pick job24 as it is the top priority job + pick1 = self.pickJob("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], job24) + + def test_worker1_finish_job(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + assertEqual(self.getJob(job20)['state'], 'FINISHED') + assertEqual(self.getJob(job21)['state'], 'WAITING') + assertEqual(self.getJob(job22)['state'], 'WAITING') + assertIsNone(self.getJob(job22)['h_paused']) + assertEqual(self.getJob(job23)['state'], 'PENDING') + assertEqual(self.getJob(job24)['state'], 'FINISHED') + + def test_pick_job22(self): + # pick job22 as job23 is still pending and job21 can't be picked as a group + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assertEqual(pick1[0], job22) + + def test_worker1_finish_job(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + + assertEqual (self.getJob (job20) ['state'], 'FINISHED') + assertEqual (self.getJob (job21) ['state'], 'FINISHED') + assertEqual (self.getJob (job22) ['state'], 'FINISHED') + assertEqual (self.getJob (job23) ['state'], 'WAITING') + assertEqual (self.getJob (job24) ['state'], 'FINISHED') + + def test_worker1_pick_job23(self): + # and eventually pick job23 + pick1 = self.pickJob ("worker1", 1, 1, 1, "127.0.0.1") + assert (pick1[0] == job23) + def test_worker1_finish_jobs(self): + self.endJob ("worker1", pick1[0], 0, "127.0.0.1") + + assertEqual (self.getJob (job20) ['state'], 'FINISHED') + assertEqual (self.getJob (job21) ['state'], 'FINISHED') + assertEqual (self.getJob (job22) ['state'], 'FINISHED') + assertEqual (self.getJob (job23) ['state'], 'FINISHED') + assertEqual (self.getJob (job24) ['state'], 'FINISHED') + + +if __name__ == "__main__": + result = test_server_python_api() + if result.wasSuccessful(): + exit(0) + else: + exit(1) +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/version b/version index 5174c53..5186d07 100644 --- a/version +++ b/version @@ -1 +1 @@ -3.16 +4.0 diff --git a/worker.py b/worker.py index 7f67849..d097d4a 100644 --- a/worker.py +++ b/worker.py @@ -1,535 +1,557 @@ -import socket, time, subprocess, thread, getopt, sys, os, base64, signal, string, re, platform, ConfigParser, httplib, urllib, datetime -from sys import modules -from os.path import splitext, abspath - -import host_cpu, host_mem - -if sys.platform=="win32": - import _winreg - import win32serviceutil - import win32service - import win32event - import win32api - -# Options -global serverUrl, debug, verbose, sleepTime, broadcastPort, gogogo, workers -debug = False -verbose = False -sleepTime = 5 -affinity = "" - -name = socket.gethostname() - -broadcastPort = 19211 -gogogo = True -serverUrl = "" -workers = 1 -cpus = None -startup = "" -service = __name__!='__main__' and sys.platform == "win32" -install = False -Headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"} - -# Go to the script directory -global coalitionDir -if sys.platform=="win32": - import _winreg - # under windows, uses the registry setup by the installer - try: - hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Mercenaries Engineering\\Coalition", 0, _winreg.KEY_READ) - coalitionDir, type = _winreg.QueryValueEx (hKey, "Installdir") - except OSError: - coalitionDir = "." -else: - coalitionDir = "." -os.chdir (coalitionDir) - -# Read the config file -config = ConfigParser.SafeConfigParser() -config.read ("coalition.ini") - -def cfgInt (name, defvalue): - global config - if config.has_option('worker', name): - try: - return int (config.get('worker', name)) - except: - pass - return defvalue - -def cfgBool (name, defvalue): - global config - if config.has_option('worker', name): - try: - return int (config.get('worker', name)) != 0 - except: - pass - return defvalue - -def cfgStr (name, defvalue): - global config - if config.has_option('worker', name): - try: - return config.get('worker', name) - except: - pass - return defvalue - -serverUrl = cfgStr ('serverUrl', '') -workers = cfgInt ('workers', 1) -name = cfgStr ('name', socket.gethostname()) -sleepTime = cfgInt ('sleep', 2) -cpus = cfgInt ('cpus', None) -startup = cfgStr ('startup', '') -verbose = cfgBool ('verbose', False) -runcommand = cfgStr ('runcommand', '') -logfile = cfgStr ('logfile', './worker.log') - -def usage(): - print ("Usage: worker [OPTIONS] [SERVER_URL]") - print ("Start a Coalition worker using the server located at SERVER_URL.") - print ("If no SERVER_URL is specified, the worker will try to locate the server using a broadcast.\n") - print ("Options:") - print (" -h, --help\t\tShow this help") - print (" -v, --verbose\t\tIncrease verbosity") - print (" -d, --debug\t\tRun without the main try/catch") - print (" -u, --startup=COMMAND\t\tStartup command executed at worker startup") - #print (" -a, --affinity=AFFINITY\tAffinity words to jobs (default: \"\"") - print (" -n, --name=NAME\tWorker name (default: "+name+")") - print (" -s, --sleep=SLEEPTIME\tSleep time between two heart beats (default: "+str (sleepTime)+"s)") - print (" -w, --workers=WORKERS\t\tNumber of workers to run (default: 1)") - print (" -c, --cpus=CPUS\t\tIndicated number of cpus per worker, determines the number of worker to execute (default: 0, all available cpus)") - print (" -i, --install\t\tInstall service (Windows only)") - print ("\nExample : worker -s 30 -v http://localhost:19211") - -if not service: - # Parse the options - try: - opts, args = getopt.getopt(sys.argv[1:], "a:c:dhin:s:u:vw:", ["affinity=", "cpus=", "debug", "help", "install", "name=", "sleep=", "startup=", "verbose", "workers="]) - if len(args) > 0: - serverUrl = args[0] - except getopt.GetoptError, err: - # print help information and exit: - print str(err) # will print something like "option -a not recognized" - usage() - sys.exit(2) - for o, a in opts: - if o in ("-a", "--affinity"): - affinity = a - elif o in ("-c", "--cpus"): - cpus = int (a) - elif o in ("-d", "--debug"): - debug = True - elif o in ("-h", "--help"): - usage() - sys.exit(2) - elif o in ("-i", "--install"): - install = True - elif o in ("-n", "--name"): - name = a - elif o in ("-s", "--sleep"): - sleepTime = float (a) - elif o in ("-u", "--startup"): - startup = a - elif o in ("-v", "--verbose"): - verbose = True - elif o in ("-w", "--workers"): - workers = int (a) - else: - assert False, "unhandled option " + o - -if not verbose or service: - outfile = open(logfile, 'a') - sys.stdout = outfile - sys.stderr = outfile - -# Log for debugging -def debugOutput (str): - if verbose: - print (str) - -# Log for debugging -def debugRaw (str): - if verbose: - print (str), - -debugOutput ("--- Start ------------------------------------------------------------") - -# If 'cpus' option set, compute the number of workers out of the total number of cpus -if cpus != None: - if platform.platform == "win32": - try: - totalcpus = int (os.getenv ("NUMBER_OF_PROCESSORS")) - cpus = min (totalcpus, cpus) - workers = max (1, totalcpus / cpus) - except: - pass - else: - pass - -debugOutput ("Running with " + str (workers) + " workers.") - -# Safe method to run a command on the server, if retry is true, the function won't return until the message is passed -def workerRun (worker, func, retry): - global sleepTime, gogogo - while (gogogo): - serverConn = None - try: - serverConn = httplib.HTTPConnection (re.sub ('^http://', '', serverUrl)) - result = func (serverConn) - serverConn.close () - return result - except (socket.error,httplib.HTTPException),err: - print ("Error sending to the server : ", str (err)) - pass - if serverConn != None: - serverConn.close () - if not retry: - debugOutput ("Server down, continue...") - break - debugOutput ("No server") - if gogogo: - time.sleep (sleepTime) - -# A Singler worker -class Worker: - def __init__ (self, name): - self.Name = name # The worker name - self.Working = False # The worker current state - self.PId = 0 # The worker current process pid - self.User = "" - self.ErrorCode = 0 # The process exit error code - self.LogLock = thread.allocate_lock() # Logs lock - self.Log = "" # Logs - self.HostCPU = host_cpu.HostCPU () - self.TotalMemory = host_mem.getTotalMem () - - - # LoadAvg - def workerGetLoadAvg (self): - self.HostCPU - return self.HostCPU.getUsage () - - def workerEvalEnv (self, _str, _env): - if platform.system () != 'Windows': - def _mapDrive (match): - return '$(' + match.group(1).upper () + '_DRIVE)' - _str = re.sub ('^([a-zA-Z]):', _mapDrive, _str) - def _getenv (match): - m = match.group(1) - # if _env exists, first try in _env - if _env: - try: - return _env[m] - except: - pass - result = os.getenv (m) - if result == None: - self.info ("ERROR : Environment variable not found : " + match.group(1)) - result = "" - return result - while re.search ('\$\(([^)]*)\)', _str): - _str = re.sub ('\$\(([^)]*)\)', _getenv, _str) - return _str - - # Add to the logs - def info (self, str): - self.LogLock.acquire() - try: - self.Log = self.Log + "* " + str + "\n"; - debugOutput (str) - finally: - self.LogLock.release() - - # Thread function to execute the job process - def _execProcess (self, cmd, dir, user, environment): - self.info ("START **********************************") - self.info ("WORKER : " + self.Name) - self.info ("DATE : " + datetime.datetime.today ().strftime("%d/%m/%y %H:%M")) - - # Special command ? - if runcommand != '': - cmd = string.replace(string.replace(string.replace(runcommand, '__user__', user), '__dir__', dir), '__cmd__', cmd) - else: - if dir != "" : - try: - # Linux, change the \\ for / - if sys.platform != "win32" : - dir = re.sub ("\\\\", "/", dir) - os.chdir (dir) - except OSError, err: - self.info ("ERROR : Can't change dir to " + dir + ": " + str (err)) - - # Serious quoting under windows - if sys.platform=="win32": - cmd = '"' + cmd + '"' - - # Run the job - self.info ("CMD : " + cmd) - - # Make sure - os.umask(002) - process = subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=environment) - - # Get the pid - self.PId = int (process.pid) - self.User = user - while (1): - # Read some lines of logs - line = process.stdout.readline() - - # "" means EOF - if line == "": - break - - debugRaw (line) - self.LogLock.acquire() - try: - self.Log = self.Log + line - finally: - self.LogLock.release() - - # Get the error code of the job - self.ErrorCode = process.wait () - self.info ("EXIT : " + str(self.ErrorCode)) - self.info ("END ********\n") - - - def execProcess (self, cmd, dir, user, env): - global debug, sleepTime - if debug: - self._execProcess (cmd, dir, user, env) - else: - try: - self._execProcess (cmd, dir, user, env) - except: - self.ErrorCode = -1 - print ("Fatal error executing the job...") - time.sleep (sleepTime) - # Signal to the main process the job is finished - self.Working = False - - ### To kill the current worker job - def killJob (self): - if self.PId != 0: - debugOutput ("kill " + str (self.PId)) - try: - self.killr (self.PId) - self.PId = 0 - except OSError as exc: - debugOutput ("kill failed") - debugOutput (exc) - pass - - ### To kill all child process - def killr (self, pid): - if sys.platform != "win32": - names = os.listdir ("/proc/") - for name in names: - try: - f = open ("/proc/" + name +"/stat","r") - line = f.readline() - words = string.split(line) - if words[3] == str (pid): - debugOutput ("Found in " + name) - self.killr (int (name)) - except IOError as exc: - #debugOutput (exc) - pass - try: - if sys.platform == "win32": - subprocess.Popen ("taskkill /F /T /PID %i"%pid, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - elif runcommand != '': - killcmd = "kill -9 "+ str (pid) - cmd = string.replace(string.replace(string.replace(runcommand, '__user__', self.User), '__dir__', '.'), '__cmd__', killcmd) - debugOutput ("Kill process with runcommand : "+cmd) - subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - else: - debugOutput ("Kill process with os.kill") - os.kill (pid, signal.SIGKILL) - except OSError as exc: - debugOutput ("Can't kill the process %i"%pid) - debugOutput (exc) - except: - debugOutput ("Can't kill the process %i"%pid) - debugOutput (sys.exc_info ()[0]) - - # Flush the logs to the server - def heartbeat (self, jobId, retry): - debugOutput ("Flush logs (" + str (len (self.Log)) + " bytes)") - def func (serverConn): - result = True - - self.LogLock.acquire() - try: - params = urllib.urlencode ({ - 'hostname':self.Name, - 'jobId':jobId, - 'log':base64.b64encode (self.Log), - 'load':self.workerGetLoadAvg (), - 'freeMemory':int(host_mem.getAvailableMem()/1024/1024), - 'totalMemory':int(self.TotalMemory/1024/1024) - }) - serverConn.request ("POST", "/workers/heartbeat", params, Headers) - response = serverConn.getresponse() - result = response.read() - self.Log = "" - finally: - self.LogLock.release() - - if result == "false": - debugOutput ("Server ask to stop the job " + str (jobId)) - # Send the kill signal to the process - self.killJob () - workerRun (self, func, retry) - - # Worker main loop - def mainLoop (self): - global sleepTime - debugOutput ("Ask for a job") - # Function to ask a job to the server - def startFunc (serverConn): - params = urllib.urlencode ({ - 'hostname':self.Name, - 'load':self.workerGetLoadAvg (), - 'freeMemory':int(host_mem.getAvailableMem()/1024/1024), - 'totalMemory':int(self.TotalMemory/1024/1024) - }) - serverConn.request ("POST", "/workers/pickjob", params, Headers) - response = serverConn.getresponse() - result = response.read() - return eval (result) - - # Block until this message to handled by the server - jobId, cmd, dir, user, env = workerRun (self, startFunc, True) - - if jobId != -1: - self.Log = "" - - _env = None - if env: - # Duplicate environment to add overrides - _env = {} - try: - for key, value in os.environ.items (): - _env[key] = value; - except: - pass - try: - for k in env.split ("\\n"): - try: - key, value = k.split ("=", 1) - _env[key] = value - except: - pass - except: - _env = None - - _cmd = self.workerEvalEnv (cmd, _env) - _dir = self.workerEvalEnv (dir, _env) - - debugOutput ("Start job " + str (jobId) + " in " + _dir + " : " + _cmd) - debugOutput ("Job environment is " + str (_env)) - - # Reset the globals - self.Working = True - stop = False - self.PId = 0 - - # Launch a new thread to run the process - - # Set the working directory in the main thead - thread.start_new_thread (self.execProcess, (_cmd, _dir, user, _env)) - - # Flush the logs - while (self.Working): - self.heartbeat (jobId, False) - time.sleep (sleepTime) - - # Flush for real for the last time - self.heartbeat (jobId, True) - - debugOutput ("Finished job " + str (jobId) + " (code " + str (self.ErrorCode) + ") : " + _cmd) - - # Function to end the job - def endFunc (serverConn): - params = urllib.urlencode ({ - 'hostname':self.Name, - 'jobId':jobId, - 'errorCode':self.ErrorCode, - }) - serverConn.request ("POST", "/workers/endjob", params, Headers) - serverConn.getresponse() - - # Block until this message to handled by the server - workerRun (self, endFunc, True) - - time.sleep (sleepTime) - -def main (): - global name, serverUrl, sleepTime, broadcastPort, gogogo, workers, startup - - print ("Startup command is '" + str (startup) + "'") - if startup != "": - cmd = startup - if sys.platform=="win32": - cmd = '"' + cmd + '"' - process = subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) - errorCode = process.wait () - print ("Startup command exited with code " + str (errorCode)) - - # If no server, look for it with a broadcast - if serverUrl == "": - from socket import SOL_SOCKET, SO_BROADCAST - from socket import socket, AF_INET, SOCK_DGRAM, timeout - - s = socket (AF_INET, SOCK_DGRAM) - s.setsockopt(SOL_SOCKET, SO_BROADCAST, True) - s.bind (('0.0.0.0', 0)) - s.settimeout (1) - while (gogogo): - try: - debugOutput ("Broadcast port " + str (broadcastPort)) - s.sendto ("coalition", ('255.255.255.255', broadcastPort)) - data, addr = s.recvfrom (1024) - if data == "roxor": - serverUrl = "http://" + addr[0] + ":" + str (broadcastPort) - print ("Server found at " + serverUrl) - debugOutput ("Found : " + serverUrl) - found = True - break - except timeout: - pass - s.close () - - while serverUrl[-1] == '/': - serverUrl = serverUrl[:-1] - - print ("Working...") - - def threadfunc (worker): - global debug, sleepTime, gogogo - while gogogo: - if debug: - worker.mainLoop () - else: - try: - worker.mainLoop () - except: - print ("Fatal error, retry...") - if gogogo: - time.sleep (sleepTime) - debugOutput ("WORKER " + worker.Name + " is kindly asked to quit.") - # kill any job in process - worker.killJob () - - # start each thread - for k in range (workers): - worker = Worker (name + "-" + str (k+1)) - thread.start_new_thread (threadfunc, (worker,)) - # and let the main thread wait - while gogogo: - time.sleep (sleepTime) - -if not service: - main() +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Coalition worker. +""" + +import socket, time, subprocess, thread, getopt, sys, os, base64, signal, string, re, platform, ConfigParser, httplib, urllib, datetime, threading +import random +from sys import modules +from os.path import splitext, abspath + +import host_cpu, host_mem + +if sys.platform=="win32": + import _winreg + import win32serviceutil + import win32service + import win32event + import win32api + +# Options +global serverUrl, debug, verbose, sleepTime, broadcastPort, gogogo, workers +debug = False +verbose = False +affinity = "" + +name = socket.gethostname() + +broadcastPort = 19211 +gogogo = True +serverUrl = "" +workers = 1 +cpus = None +startup = "" +service = __name__!='__main__' and sys.platform == "win32" +install = False +Headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"} +Event = threading.Event () + +# Go to the script directory +global coalitionDir +if sys.platform=="win32": + import _winreg + # under windows, uses the registry setup by the installer + try: + hKey = _winreg.OpenKey (_winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Mercenaries Engineering\\Coalition", 0, _winreg.KEY_READ) + coalitionDir, type = _winreg.QueryValueEx (hKey, "Installdir") + except OSError: + coalitionDir = "." +else: + coalitionDir = "." +os.chdir (coalitionDir) + +# Read the config file +config = ConfigParser.SafeConfigParser() +config.read ("coalition.ini") + +def cfgInt (name, defvalue): + global config + if config.has_option('worker', name): + try: + return int (config.get('worker', name)) + except: + pass + return defvalue + +def cfgBool (name, defvalue): + global config + if config.has_option('worker', name): + try: + return int (config.get('worker', name)) != 0 + except: + pass + return defvalue + +def cfgStr (name, defvalue): + global config + if config.has_option('worker', name): + try: + return config.get('worker', name) + except: + pass + return defvalue + +serverUrl = cfgStr ('serverUrl', '') +workers = cfgInt ('workers', 1) +name = cfgStr ('name', socket.gethostname()) +sleepTime = cfgInt ('sleep', 2) +cpus = cfgInt ('cpus', None) +startup = cfgStr ('startup', '') +verbose = cfgBool ('verbose', False) +runcommand = cfgStr ('runcommand', '') +logfile = cfgStr ('logfile', './worker.log') + +def usage(): + print ("Usage: worker [OPTIONS] [SERVER_URL]") + print ("Start a Coalition worker using the server located at SERVER_URL.") + print ("If no SERVER_URL is specified, the worker will try to locate the server using a broadcast.\n") + print ("Options:") + print (" -h, --help\t\tShow this help") + print (" -v, --verbose\t\tIncrease verbosity") + print (" -d, --debug\t\tRun without the main try/catch") + print (" -u, --startup=COMMAND\t\tStartup command executed at worker startup") + #print (" -a, --affinity=AFFINITY\tAffinity words to jobs (default: \"\"") + print (" -n, --name=NAME\tWorker name (default: "+name+")") + print (" -s, --sleep=SLEEPTIME\tSleep time between two heart beats (default: "+str (sleepTime)+"s)") + print (" -w, --workers=WORKERS\t\tNumber of workers to run (default: 1)") + print (" -c, --cpus=CPUS\t\tIndicated number of cpus per worker, determines the number of worker to execute (default: 0, all available cpus)") + print (" -i, --install\t\tInstall service (Windows only)") + print ("\nExample : worker -s 30 -v http://localhost:19211") + +if not service: + # Parse the options + try: + opts, args = getopt.getopt(sys.argv[1:], "a:c:dhin:s:u:vw:", ["affinity=", "cpus=", "debug", "help", "install", "name=", "sleep=", "startup=", "verbose", "workers="]) + if len(args) > 0: + serverUrl = args[0] + except getopt.GetoptError, err: + # print help information and exit: + print str(err) # will print something like "option -a not recognized" + usage() + sys.exit(2) + for o, a in opts: + if o in ("-a", "--affinity"): + affinity = a + elif o in ("-c", "--cpus"): + cpus = int (a) + elif o in ("-d", "--debug"): + debug = True + elif o in ("-h", "--help"): + usage() + sys.exit(2) + elif o in ("-i", "--install"): + install = True + elif o in ("-n", "--name"): + name = a + elif o in ("-s", "--sleep"): + sleepTime = float (a) + elif o in ("-u", "--startup"): + startup = a + elif o in ("-v", "--verbose"): + verbose = True + elif o in ("-w", "--workers"): + workers = int (a) + else: + assert False, "unhandled option " + o + +if not verbose or service: + outfile = open(logfile, 'a') + sys.stdout = outfile + sys.stderr = outfile + +# Log for debugging +def vprint (str): + if verbose: + print (str) + sys.stdout.flush() + +# Log for debugging +def debugRaw (str): + if verbose: + print (str) + sys.stdout.flush() + +vprint ("--- Start ------------------------------------------------------------") + +# If 'cpus' option set, compute the number of workers out of the total number of cpus +if cpus != None: + if platform.platform == "win32": + try: + totalcpus = int (os.getenv ("NUMBER_OF_PROCESSORS")) + cpus = min (totalcpus, cpus) + workers = max (1, totalcpus / cpus) + except: + pass + else: + pass + +vprint ("Running with " + str (workers) + " workers.") + +random.seed () +def shuffleSleepTime (sleepTime): + return sleepTime * (1.0 + (random.random ()-0.5)*0.2) + +# Safe method to run a command on the server, if retry is true, the function won't return until the message is passed +def workerRun (worker, func, retry): + global sleepTime, gogogo + while (gogogo): + serverConn = None + try: + serverConn = httplib.HTTPConnection (re.sub ('^http://', '', serverUrl)) + result = func (serverConn) + serverConn.close () + return result + except (socket.error,httplib.HTTPException),err: + print ("Error sending to the server : ", str (err)) + pass + if serverConn != None: + serverConn.close () + if not retry: + vprint ("Server down, continue...") + break + vprint ("No server") + if gogogo: + time.sleep (shuffleSleepTime (sleepTime)) + +# A Singler worker +class Worker: + def __init__ (self, name): + self.Name = name # The worker name + self.Working = False # The worker current state + self.PId = 0 # The worker current process pid + self.User = "" + self.ErrorCode = 0 # The process exit error code + self.LogLock = thread.allocate_lock() # Logs lock + self.Log = "" # Logs + self.HostCPU = host_cpu.HostCPU () + self.total_memory = host_mem.getTotalMem () + + + # LoadAvg + def workerGetLoadAvg (self): + usage = self.HostCPU.getUsage () + return usage + + def workerEvalEnv (self, _str, _env): + if platform.system () != 'Windows': + def _mapDrive (match): + return '$(' + match.group(1).upper () + '_DRIVE)' + _str = re.sub ('^([a-zA-Z]):', _mapDrive, _str) + def _getenv (match): + m = match.group(1) + # if _env exists, first try in _env + if _env: + try: + return _env[m] + except: + pass + result = os.getenv (m) + if result == None: + self.info ("ERROR : Environment variable not found : " + match.group(1)) + result = "" + return result + while re.search ('\$\(([^)]*)\)', _str): + _str = re.sub ('\$\(([^)]*)\)', _getenv, _str) + return _str + + # Add to the logs + def info (self, str): + self.LogLock.acquire() + try: + self.Log = self.Log + "* " + str + "\n"; + vprint (str) + finally: + self.LogLock.release() + + # Thread function to execute the job process + def _execProcess (self, cmd, dir, user, environment): + self.info ("START **********************************") + self.info ("WORKER : " + self.Name) + self.info ("DATE : " + datetime.datetime.today ().strftime("%d/%m/%y %H:%M")) + + # Special command ? + if runcommand != '': + cmd = string.replace(string.replace(string.replace(runcommand, '__user__', user), '__dir__', dir), '__cmd__', cmd) + else: + if dir != "" : + try: + # Linux, change the \\ for / + if sys.platform != "win32" : + dir = re.sub ("\\\\", "/", dir) + os.chdir (dir) + except OSError, err: + self.info ("ERROR : Can't change dir to " + dir + ": " + str (err)) + + # Run the job + self.info ("CMD : " + cmd) + + # Make sure + os.umask(002) + process = subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=environment) + + # Get the pid + self.PId = int (process.pid) + self.User = user + while (1): + # Read some lines of logs + line = process.stdout.readline() + + # "" means EOF + if line == "": + break + + debugRaw (line) + self.LogLock.acquire() + try: + self.Log = self.Log + line + finally: + self.LogLock.release() + + # Get the error code of the job + self.ErrorCode = process.wait () + self.info ("EXIT : " + str(self.ErrorCode)) + self.info ("END ********\n") + + + def execProcess (self, cmd, dir, user, env): + global debug, sleepTime + if debug: + self._execProcess (cmd, dir, user, env) + else: + try: + self._execProcess (cmd, dir, user, env) + except: + self.ErrorCode = -1 + print ("Fatal error executing the job...") + time.sleep (shuffleSleepTime (sleepTime)) + # Signal to the main process the job is finished + self.Working = False + Event.set () + + ### To kill the current worker job + def killJob (self): + if self.PId != 0: + vprint ("kill " + str (self.PId)) + try: + self.killr (self.PId) + self.PId = 0 + except OSError as exc: + vprint ("kill failed") + vprint (exc) + pass + + ### To kill all child process + def killr (self, pid): + if sys.platform != "win32": + names = os.listdir ("/proc/") + for name in names: + try: + f = open ("/proc/" + name +"/stat","r") + line = f.readline() + words = string.split(line) + if words[3] == str (pid): + vprint ("Found in " + name) + self.killr (int (name)) + except IOError as exc: + #vprint (exc) + pass + try: + if sys.platform == "win32": + subprocess.Popen ("taskkill /F /T /PID %i"%pid, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + elif runcommand != '': + killcmd = "kill -9 "+ str (pid) + cmd = string.replace(string.replace(string.replace(runcommand, '__user__', self.User), '__dir__', '.'), '__cmd__', killcmd) + vprint ("Kill process with runcommand : "+cmd) + subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + else: + vprint ("Kill process with os.kill") + os.kill (pid, signal.SIGKILL) + except OSError as exc: + vprint ("Can't kill the process %i"%pid) + vprint (exc) + except: + vprint ("Can't kill the process %i"%pid) + vprint (sys.exc_info ()[0]) + + # Flush the logs to the server + def heartbeat (self, jobId, retry): + vprint ("Flush logs (" + str (len (self.Log)) + " bytes)") + def func (serverConn): + result = True + + self.LogLock.acquire() + try: + params = urllib.urlencode ({ + 'hostname':self.Name, + 'jobId':jobId, + 'log':base64.b64encode (self.Log), + 'load':self.workerGetLoadAvg (), + 'free_memory':int(host_mem.getAvailableMem()/1024/1024), + 'total_memory':int(self.total_memory/1024/1024) + }) + serverConn.request ("POST", "/workers/heartbeat", params, Headers) + response = serverConn.getresponse() + result = response.read() + self.Log = "" + finally: + self.LogLock.release() + + if result == "false": + vprint ("Server ask to stop the job " + str (jobId)) + # Send the kill signal to the process + self.killJob () + workerRun (self, func, retry) + + # Worker main loop + def mainLoop (self): + global sleepTime + vprint ("Ask for a job") + # Function to ask a job to the server + def startFunc (serverConn): + params = urllib.urlencode ({ + 'hostname':self.Name, + 'load':self.workerGetLoadAvg (), + 'free_memory':int(host_mem.getAvailableMem()/1024/1024), + 'total_memory':int(self.total_memory/1024/1024) + }) + serverConn.request ("POST", "/workers/pickjob", params, Headers) + response = serverConn.getresponse() + result = response.read() + return eval (result) + + # Block until this message to handled by the server + jobId, cmd, dir, user, env = workerRun (self, startFunc, True) + + if jobId != -1: + self.Log = "" + + _env = None + if env: + # Duplicate environment to add overrides + _env = {} + try: + for key, value in os.environ.items (): + _env[key] = value; + except: + pass + try: + for k in env.split ("\\n"): + try: + key, value = k.split ("=", 1) + _env[key] = value + except: + pass + except: + _env = None + + _cmd = self.workerEvalEnv (cmd, _env) + _dir = self.workerEvalEnv (dir, _env) + + vprint ("Start job " + str (jobId) + " in " + _dir + " : " + _cmd) + vprint ("Job environment is " + str (_env)) + + # Reset the globals + self.Working = True + stop = False + self.PId = 0 + + # Launch a new thread to run the process + + # Set the working directory in the main thead + thread.start_new_thread (self.execProcess, (_cmd, _dir, user, _env)) + + # Flush the logs + while (self.Working): + self.heartbeat (jobId, False) + Event.clear () + Event.wait (shuffleSleepTime (sleepTime)) + + # Flush for real for the last time + self.heartbeat (jobId, True) + + vprint ("Finished job " + str (jobId) + " (code " + str (self.ErrorCode) + ") : " + _cmd) + + # Function to end the job + def endFunc (serverConn): + params = urllib.urlencode ({ + 'hostname':self.Name, + 'jobId':jobId, + 'errorCode':self.ErrorCode, + }) + serverConn.request ("POST", "/workers/endjob", params, Headers) + response = serverConn.getresponse() + response.read () + + # Block until this message to handled by the server + workerRun (self, endFunc, True) + else: + time.sleep (shuffleSleepTime (sleepTime)) + +def main (): + global name, serverUrl, sleepTime, broadcastPort, gogogo, workers, startup + + print ("Startup command is '" + str (startup) + "'") + if startup != "": + cmd = startup + if sys.platform=="win32": + cmd = '"' + cmd + '"' + process = subprocess.Popen (cmd, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + errorCode = process.wait () + print ("Startup command exited with code " + str (errorCode)) + + # If no server, look for it with a broadcast + if serverUrl == "": + from socket import SOL_SOCKET, SO_BROADCAST + from socket import socket, AF_INET, SOCK_DGRAM, timeout + + s = socket (AF_INET, SOCK_DGRAM) + s.setsockopt(SOL_SOCKET, SO_BROADCAST, True) + s.bind (('0.0.0.0', 0)) + s.settimeout (1) + while (gogogo): + try: + vprint ("Broadcast port " + str (broadcastPort)) + s.sendto ("coalition", ('255.255.255.255', broadcastPort)) + data, addr = s.recvfrom (1024) + if data == "roxor": + serverUrl = "http://" + addr[0] + ":" + str (broadcastPort) + print ("Server found at " + serverUrl) + vprint ("Found : " + serverUrl) + found = True + break + except timeout: + pass + s.close () + + while serverUrl[-1] == '/': + serverUrl = serverUrl[:-1] + + print ("Working...") + + def threadfunc (worker): + global debug, sleepTime, gogogo + while gogogo: + if debug: + worker.mainLoop () + else: + try: + worker.mainLoop () + except: + print ("Fatal error, retry...") + if gogogo: + time.sleep (shuffleSleepTime (sleepTime)) + vprint ("WORKER " + worker.Name + " is kindly asked to quit.") + # kill any job in process + worker.killJob () + + # start each thread + if workers == 1: + # No suffix if one worker + worker = Worker (name) + thread.start_new_thread (threadfunc, (worker,)) + else: + for k in range (workers): + worker = Worker (name + "-" + str (k+1)) + thread.start_new_thread (threadfunc, (worker,)) + + # and let the main thread wait + while gogogo: + time.sleep (shuffleSleepTime (sleepTime)) + +if not service: + main() + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 + diff --git a/worker_service.py b/worker_service.py index 7629d7c..e88a8e4 100644 --- a/worker_service.py +++ b/worker_service.py @@ -1,34 +1,40 @@ -import worker - -# Windows Service -import win32serviceutil -import win32service -import win32event -import servicemanager - -class WindowsService(win32serviceutil.ServiceFramework): - _svc_name_ = "CoalitionWorker" - _svc_display_name_ = "Coalition Worker" - - def __init__(self, args): - win32serviceutil.ServiceFramework.__init__(self, args) - self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) - - def SvcStop(self): - global gogogo - gogogo = False - self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) - win32event.SetEvent(self.hWaitStop) - - def SvcDoRun(self): - self.CheckForQuit() - main() - - def CheckForQuit(self): - global gogogo - retval = win32event.WaitForSingleObject(self.hWaitStop, 10) - if not retval == win32event.WAIT_TIMEOUT: - # Received Quit from Win32 - gogogo = False - -win32serviceutil.HandleCommandLine(WindowsService) +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import worker + +# Windows Service +import win32serviceutil +import win32service +import win32event +import servicemanager + +class WindowsService(win32serviceutil.ServiceFramework): + _svc_name_ = "CoalitionWorker" + _svc_display_name_ = "Coalition Worker" + + def __init__(self, args): + win32serviceutil.ServiceFramework.__init__(self, args) + self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) + + def SvcStop(self): + global gogogo + gogogo = False + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + win32event.SetEvent(self.hWaitStop) + + def SvcDoRun(self): + self.CheckForQuit() + main() + + def CheckForQuit(self): + global gogogo + retval = win32event.WaitForSingleObject(self.hWaitStop, 10) + if not retval == win32event.WAIT_TIMEOUT: + # Received Quit from Win32 + gogogo = False + +win32serviceutil.HandleCommandLine(WindowsService) + +# vim: tabstop=4 noexpandtab shiftwidth=4 softtabstop=4 textwidth=79 +