mirror of
https://github.com/fcwu/docker-ubuntu-vnc-desktop
synced 2026-08-05 16:12:41 +02:00
feat: support video mode
This commit is contained in:
@@ -6,10 +6,6 @@ class Development(Default):
|
||||
PHASE = 'development'
|
||||
|
||||
|
||||
class Staging(Default):
|
||||
PHASE = 'staging'
|
||||
|
||||
|
||||
class Production(Default):
|
||||
PHASE = 'production'
|
||||
DEBUG = False
|
||||
|
||||
@@ -4,16 +4,16 @@ import logging
|
||||
import logging.handlers
|
||||
|
||||
|
||||
#The terminal has 8 colors with codes from 0 to 7
|
||||
# The terminal has 8 colors with codes from 0 to 7
|
||||
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
|
||||
|
||||
#These are the sequences need to get colored ouput
|
||||
# These are the sequences need to get colored ouput
|
||||
RESET_SEQ = "\033[0m"
|
||||
COLOR_SEQ = "\033[1;%dm"
|
||||
BOLD_SEQ = "\033[1m"
|
||||
|
||||
#The background is set with 40 plus the number of the color,
|
||||
#and the foreground with 30
|
||||
# The background is set with 40 plus the number of the color,
|
||||
# and the foreground with 30
|
||||
COLORS = {
|
||||
'WARNING': COLOR_SEQ % (30 + YELLOW) + 'WARN ' + RESET_SEQ,
|
||||
'INFO': COLOR_SEQ % (30 + WHITE) + 'INFO ' + RESET_SEQ,
|
||||
@@ -64,18 +64,19 @@ class LoggingConfiguration(object):
|
||||
|
||||
# Log to rotating file
|
||||
try:
|
||||
fh = logging.handlers.RotatingFileHandler(log_filename,
|
||||
mode='a+',
|
||||
backupCount=3)
|
||||
fh = logging.FileHandler(log_filename, mode='a+')
|
||||
fh = logging.handlers.RotatingFileHandler(
|
||||
log_filename,
|
||||
mode='a+',
|
||||
backupCount=3
|
||||
)
|
||||
fh.setFormatter(ColoredFormatter(FILE_FORMAT, False))
|
||||
fh.setLevel(log_level)
|
||||
logger.addHandler(fh)
|
||||
if not append:
|
||||
# Create a new log file on every new
|
||||
fh.doRollover()
|
||||
except:
|
||||
pass
|
||||
except IOError as e:
|
||||
print('ignore to log to {}: {}'.format(log_filename, e))
|
||||
|
||||
# Log to sys.stderr using log level passed through command line
|
||||
if log_level != logging.NOTSET:
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
Flask==0.10.1
|
||||
Flask-Login==0.2.11
|
||||
Jinja2==2.7.3
|
||||
MarkupSafe==0.23
|
||||
Werkzeug==0.9.6
|
||||
argparse==1.2.1
|
||||
backports.ssl-match-hostname==3.4.0.2
|
||||
docker-py==0.5.3
|
||||
gevent==1.0.1
|
||||
gevent-websocket==0.9.3
|
||||
greenlet==0.4.5
|
||||
backports.ssl-match-hostname==3.5.0.1
|
||||
certifi==2018.1.18
|
||||
chardet==3.0.4
|
||||
click==6.7
|
||||
Flask==0.12.2
|
||||
Flask-Login==0.4.1
|
||||
gevent==1.2.2
|
||||
gevent-websocket==0.10.1
|
||||
greenlet==0.4.13
|
||||
idna==2.6
|
||||
itsdangerous==0.24
|
||||
peewee==2.4.1
|
||||
requests==2.4.3
|
||||
six==1.8.0
|
||||
websocket-client==0.21.0
|
||||
wsgiref==0.1.2
|
||||
Jinja2==2.10
|
||||
MarkupSafe==1.0
|
||||
meld3==1.0.2
|
||||
requests==2.18.4
|
||||
six==1.11.0
|
||||
supervisor==3.2.0
|
||||
urllib3==1.22
|
||||
websocket-client==0.47.0
|
||||
Werkzeug==0.14.1
|
||||
|
||||
@@ -1,108 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, with_statement
|
||||
)
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import subprocess
|
||||
import signal
|
||||
|
||||
|
||||
def run_with_reloader(main_func, extra_files=None, interval=1):
|
||||
"""Run the given function in an independent python interpreter."""
|
||||
def find_files(directory="./"):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for basename in files:
|
||||
if basename.endswith('.py'):
|
||||
filename = os.path.join(root, basename)
|
||||
yield filename
|
||||
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
try:
|
||||
os.setpgid(0, 0)
|
||||
main_func()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
|
||||
procs = None
|
||||
try:
|
||||
while True:
|
||||
print('* Restarting with reloader ' + str(sys.executable))
|
||||
args = [sys.executable] + sys.argv
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
|
||||
procs = subprocess.Popen(args, env=new_environ)
|
||||
mtimes = {}
|
||||
restart = False
|
||||
while not restart:
|
||||
for filename in find_files():
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
print('* Detected change in %r, reloading' % filename)
|
||||
restart = True
|
||||
break
|
||||
time.sleep(interval)
|
||||
|
||||
killpg(procs.pid, signal.SIGTERM)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
killpg(procs.pid, signal.SIGTERM)
|
||||
|
||||
|
||||
def killpg(pgid, send_signal=signal.SIGKILL):
|
||||
print('kill PGID {}'.format(pgid))
|
||||
try:
|
||||
os.killpg(pgid, send_signal)
|
||||
#os.killpg(pgid, signal.SIGKILL)
|
||||
except:
|
||||
pass
|
||||
from vnc.util import ignored
|
||||
|
||||
|
||||
def main():
|
||||
def run_with_reloader(main_func, extra_files=None, interval=3):
|
||||
"""Run the given function in an independent python interpreter."""
|
||||
def find_files(directory="./"):
|
||||
for root, dirs, files in os.walk(directory):
|
||||
for basename in files:
|
||||
if basename.endswith('.py'):
|
||||
filename = os.path.join(root, basename)
|
||||
yield filename
|
||||
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
try:
|
||||
main_func()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return
|
||||
|
||||
proc = None
|
||||
try:
|
||||
while True:
|
||||
log.info('Restarting with reloader {} {}'.format(
|
||||
sys.executable,
|
||||
' '.join(sys.argv))
|
||||
)
|
||||
args = [sys.executable] + sys.argv
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
|
||||
proc = subprocess.Popen(
|
||||
args,
|
||||
env=new_environ,
|
||||
close_fds=True,
|
||||
preexec_fn=os.setsid
|
||||
)
|
||||
mtimes = {}
|
||||
restart = False
|
||||
while not restart:
|
||||
for filename in find_files():
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
old_time = mtimes.get(filename)
|
||||
if old_time is None:
|
||||
mtimes[filename] = mtime
|
||||
continue
|
||||
elif mtime > old_time:
|
||||
log.info(
|
||||
'Detected change in {}, reloading'.format(
|
||||
filename
|
||||
)
|
||||
)
|
||||
restart = True
|
||||
proc.terminate()
|
||||
break
|
||||
time.sleep(interval)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
with ignored(Exception):
|
||||
proc.terminate()
|
||||
|
||||
def run_server():
|
||||
import socket
|
||||
|
||||
os.environ['CONFIG'] = CONFIG
|
||||
from vnc import app
|
||||
from gevent.wsgi import WSGIServer
|
||||
from vnc.app import app
|
||||
|
||||
# websocket conflict: WebSocketHandler
|
||||
if DEBUG or STAGING:
|
||||
if DEBUG:
|
||||
# from werkzeug.debug import DebuggedApplication
|
||||
app.debug = True
|
||||
# app = DebuggedApplication(app, evalex=True)
|
||||
|
||||
pgid = os.getpgid(0)
|
||||
signal.signal(signal.SIGTERM, lambda *args: killpg(pgid))
|
||||
signal.signal(signal.SIGHUP, lambda *args: killpg(pgid))
|
||||
signal.signal(signal.SIGINT, lambda *args: killpg(pgid))
|
||||
|
||||
try:
|
||||
app.run(host='', port=PORT)
|
||||
log.info('Listening on http://localhost:{}'.format(PORT))
|
||||
http_server = WSGIServer(('localhost', PORT), app)
|
||||
http_server.serve_forever()
|
||||
# app.run(host='localhost', port=PORT)
|
||||
except socket.error as e:
|
||||
print(e)
|
||||
log.exception(e)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
http_server.stop(timeout=10)
|
||||
log.info('shutdown gracefully')
|
||||
|
||||
DEBUG = True if '--debug' in sys.argv else False
|
||||
STAGING = True if '--staging' in sys.argv else False
|
||||
CONFIG = 'config.Development' if DEBUG else 'config.Production'
|
||||
CONFIG = 'config.Staging' if STAGING else CONFIG
|
||||
PORT = 6079
|
||||
signal.signal(signal.SIGCHLD, signal.SIG_IGN)
|
||||
DEBUG = False
|
||||
os.environ['CONFIG'] = 'config.Production'
|
||||
entrypoint = run_server
|
||||
if '--debug' in sys.argv:
|
||||
DEBUG = True
|
||||
os.environ['CONFIG'] = 'config.Development'
|
||||
entrypoint = lambda: run_with_reloader(run_server)
|
||||
|
||||
if DEBUG or STAGING:
|
||||
main = lambda: run_with_reloader(run_server)
|
||||
else:
|
||||
main = run_server
|
||||
main()
|
||||
# logging
|
||||
import logging
|
||||
from log.config import LoggingConfiguration
|
||||
LoggingConfiguration.set(
|
||||
logging.DEBUG if DEBUG else logging.INFO,
|
||||
'/var/log/web.log'
|
||||
)
|
||||
logging.getLogger("werkzeug").setLevel(logging.WARNING)
|
||||
log = logging.getLogger('novnc2')
|
||||
entrypoint()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
from flask import (Flask,
|
||||
request,
|
||||
abort,
|
||||
)
|
||||
import os
|
||||
import json
|
||||
from functools import wraps
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
# Flask app
|
||||
app = Flask('novnc2')
|
||||
CONFIG = os.environ.get('CONFIG') or 'config.Development'
|
||||
app.config.from_object('config.Default')
|
||||
app.config.from_object(CONFIG)
|
||||
FIRST = 'RESOLUTION' not in os.environ
|
||||
|
||||
|
||||
# logging
|
||||
import logging
|
||||
from log.config import LoggingConfiguration
|
||||
LoggingConfiguration.set(
|
||||
logging.DEBUG if os.getenv('DEBUG') else logging.INFO,
|
||||
'/var/log/web.log'
|
||||
)
|
||||
|
||||
|
||||
def exception_to_json(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
return result
|
||||
except (BadRequest,
|
||||
KeyError,
|
||||
ValueError,
|
||||
) as e:
|
||||
result = {'error': {'code': 400,
|
||||
'message': str(e)}}
|
||||
except PermissionDenied as e:
|
||||
result = {'error': {'code': 403,
|
||||
'message': ', '.join(e.args)}}
|
||||
except (NotImplementedError, RuntimeError, AttributeError) as e:
|
||||
result = {'error': {'code': 500,
|
||||
'message': ', '.join(e.args)}}
|
||||
return json.dumps(result)
|
||||
return wrapper
|
||||
|
||||
|
||||
class PermissionDenied(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Exception):
|
||||
pass
|
||||
|
||||
|
||||
HTML_INDEX = '''<html><head>
|
||||
<script type="text/javascript">
|
||||
var w = window,
|
||||
d = document,
|
||||
e = d.documentElement,
|
||||
g = d.getElementsByTagName('body')[0],
|
||||
x = w.innerWidth || e.clientWidth || g.clientWidth,
|
||||
y = w.innerHeight|| e.clientHeight|| g.clientHeight;
|
||||
var url = "redirect.html?width=" + x + "&height=" + (parseInt(y));
|
||||
window.location.href = url;
|
||||
</script>
|
||||
<title>Page Redirection</title>
|
||||
</head><body></body></html>'''
|
||||
|
||||
|
||||
HTML_REDIRECT = '''<html><head>
|
||||
<script type="text/javascript">
|
||||
var port = window.location.port;
|
||||
if (!port)
|
||||
port = window.location.protocol[4] == 's' ? 443 : 80;
|
||||
window.location.href = "vnc.html?autoconnect=1&autoscale=0&quality=3";
|
||||
</script>
|
||||
<title>Page Redirection</title>
|
||||
</head><body></body></html>'''
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return HTML_INDEX
|
||||
|
||||
|
||||
@app.route('/api/status')
|
||||
def status():
|
||||
global FIRST
|
||||
return json.dumps({
|
||||
'default_resolution': FIRST
|
||||
})
|
||||
|
||||
|
||||
@app.route('/redirect.html')
|
||||
def redirectme():
|
||||
global FIRST
|
||||
|
||||
if not FIRST:
|
||||
return HTML_REDIRECT
|
||||
|
||||
env = {'width': 1024, 'height': 768}
|
||||
if 'width' in request.args:
|
||||
env['width'] = request.args['width']
|
||||
if 'height' in request.args:
|
||||
env['height'] = request.args['height']
|
||||
|
||||
# sed
|
||||
cmd = (
|
||||
'sed -i \'s#'
|
||||
'^exec /usr/bin/Xvfb.*$'
|
||||
'#'
|
||||
'exec /usr/bin/Xvfb :1 -screen 0 {width}x{height}x16'
|
||||
'#\' /usr/local/bin/xvfb.sh'
|
||||
).format(**env),
|
||||
subprocess.check_call(cmd, shell=True)
|
||||
# supervisorctrl reload
|
||||
subprocess.check_call(['supervisorctl', 'restart', 'x:'])
|
||||
|
||||
# check all running
|
||||
for i in range(40):
|
||||
output = subprocess.check_output(['supervisorctl', 'status'])
|
||||
for line in output.strip().split('\n'):
|
||||
if line.find('RUNNING') < 0:
|
||||
break
|
||||
else:
|
||||
FIRST = False
|
||||
return HTML_REDIRECT
|
||||
time.sleep(1)
|
||||
logging.info('wait services is ready...')
|
||||
abort(500, 'service is not ready, please restart container')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=app.config['ADDRESS'], port=app.config['PORT'])
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, with_statement
|
||||
)
|
||||
import re
|
||||
from os import environ
|
||||
from flask import (
|
||||
Flask,
|
||||
request,
|
||||
Response,
|
||||
jsonify,
|
||||
)
|
||||
from gevent import subprocess as gsp, spawn, sleep
|
||||
from geventwebsocket.exceptions import WebSocketError
|
||||
from .response import httperror
|
||||
from .util import ignored
|
||||
from .state import state
|
||||
from .log import log
|
||||
|
||||
|
||||
# Flask app
|
||||
app = Flask('novnc2')
|
||||
app.config.from_object('config.Default')
|
||||
app.config.from_object(environ.get('CONFIG') or 'config.Development')
|
||||
|
||||
|
||||
@app.route('/api/state')
|
||||
@httperror
|
||||
def apistate():
|
||||
state.wait(int(request.args.get('id', -1)), 30)
|
||||
state.switch_video(request.args.get('video', 'false') == 'true')
|
||||
mystate = state.to_dict()
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'data': mystate,
|
||||
})
|
||||
|
||||
|
||||
@app.route('/api/reset')
|
||||
def reset():
|
||||
if 'w' in request.args and 'h' in request.args:
|
||||
args = {
|
||||
'w': request.args.get('w'),
|
||||
'h': request.args.get('h'),
|
||||
}
|
||||
state.set_size(args['w'], args['h'])
|
||||
|
||||
state.apply_and_restart()
|
||||
|
||||
# check all running
|
||||
for i in range(40):
|
||||
if state.health:
|
||||
break
|
||||
sleep(1)
|
||||
log.info('wait services is ready...')
|
||||
else:
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'errorMessage': 'service is not ready, please restart container'
|
||||
})
|
||||
return jsonify({'code': 200})
|
||||
|
||||
|
||||
@app.route('/api/live.flv')
|
||||
@httperror
|
||||
def liveflv():
|
||||
def generate():
|
||||
xenvs = {
|
||||
'DISPLAY': ':1',
|
||||
}
|
||||
bufsize = 1024 * 1
|
||||
framerate = 20
|
||||
|
||||
# sound
|
||||
sound_cmd_input = []
|
||||
sound_cmd_parameters = []
|
||||
zero_latency_make_sound_not_good = [
|
||||
'-tune', 'zerolatency',
|
||||
]
|
||||
|
||||
xenvs['X_WIDTH'] = state.w
|
||||
xenvs['X_HEIGHT'] = state.h
|
||||
xenvs['X_WIDTH'] -= state.w % 2
|
||||
xenvs['X_HEIGHT'] -= state.h % 2
|
||||
|
||||
pixels_count = xenvs['X_WIDTH'] * xenvs['X_HEIGHT']
|
||||
# factor (720p)
|
||||
# 383: 2400k
|
||||
# 300: 3000k
|
||||
# 230: 4000k
|
||||
factor = 265
|
||||
maxbitrate_cmd = [
|
||||
'-maxrate', str(int(pixels_count / factor)) + 'k',
|
||||
'-bufsize', str(int(pixels_count / factor / 3)) + 'k'
|
||||
]
|
||||
|
||||
# TODO move to global
|
||||
# get default source
|
||||
sound_cmd_input = [
|
||||
'-f', 'alsa',
|
||||
'-i', 'hw:2,1',
|
||||
]
|
||||
sound_cmd_parameters = [
|
||||
'-ar', '44100',
|
||||
'-c:a', 'mp3',
|
||||
]
|
||||
# flv.js report error if enabling hw acceleration
|
||||
# hwaccel_dev = ['-vaapi_device', '/dev/dri/renderD128']
|
||||
# hwaccel_if = ['-vf', 'format=nv12,hwupload']
|
||||
# vcodec = 'h264_vaapi'
|
||||
hwaccel_dev = []
|
||||
hwaccel_if = []
|
||||
vcodec = 'libx264'
|
||||
# zero_latency_make_sound_not_good = []
|
||||
# sound_cmd_parameters = []
|
||||
# sound_cmd_input = []
|
||||
cmd = ['/usr/local/ffmpeg/ffmpeg'] + sound_cmd_input + hwaccel_dev + [
|
||||
'-video_size', '{X_WIDTH}x{X_HEIGHT}'.format(**xenvs),
|
||||
'-framerate', '{}'.format(framerate),
|
||||
'-f', 'x11grab', '-draw_mouse', '1',
|
||||
'-i', '{DISPLAY}'.format(**xenvs),
|
||||
] + hwaccel_if + [
|
||||
'-r', '{}'.format(framerate),
|
||||
'-g', '{}'.format(framerate),
|
||||
'-flags:v', '+global_header',
|
||||
'-vcodec', vcodec,
|
||||
'-preset', 'ultrafast',
|
||||
'-b_strategy', '0',
|
||||
'-pix_fmt', 'yuv420p',
|
||||
'-bsf:v', 'dump_extra=freq=e',
|
||||
] + maxbitrate_cmd \
|
||||
+ sound_cmd_parameters + zero_latency_make_sound_not_good + [
|
||||
'-f', 'flv', 'pipe:1',
|
||||
]
|
||||
log.info('command: ' + ' '.join(cmd))
|
||||
pobj = gsp.Popen(
|
||||
cmd,
|
||||
stdout=gsp.PIPE,
|
||||
stderr=gsp.PIPE,
|
||||
env={k: str(v) for k, v in xenvs.iteritems()},
|
||||
)
|
||||
|
||||
def readerr(f):
|
||||
reobj = re.compile(r'bitrate=(\S+)')
|
||||
global av_bitrate
|
||||
try:
|
||||
while True:
|
||||
buf = f.read(bufsize)
|
||||
if len(buf) == 0:
|
||||
break
|
||||
patterns = reobj.findall(buf.decode('utf-8', 'ignore'))
|
||||
if len(patterns) > 0:
|
||||
av_bitrate = patterns[-1]
|
||||
# log.info(str(buf))
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
preaderr = None
|
||||
try:
|
||||
preaderr = spawn(readerr, pobj.stderr)
|
||||
try:
|
||||
while True:
|
||||
buf = pobj.stdout.read(bufsize)
|
||||
if len(buf) == 0:
|
||||
break
|
||||
# ws.send(buf)
|
||||
yield buf
|
||||
except WebSocketError:
|
||||
pass
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
finally:
|
||||
with ignored(Exception):
|
||||
pobj.kill()
|
||||
preaderr.join()
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
finally:
|
||||
log.info('exiting')
|
||||
with ignored(Exception):
|
||||
pobj.kill()
|
||||
with ignored(Exception):
|
||||
preaderr.kill()
|
||||
log.info('exited')
|
||||
return Response(generate(), mimetype='video/x-flv')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host=app.config['ADDRESS'], port=app.config['PORT'])
|
||||
@@ -0,0 +1,2 @@
|
||||
import logging
|
||||
log = logging.getLogger('novnc2')
|
||||
@@ -0,0 +1,40 @@
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, with_statement
|
||||
)
|
||||
from functools import wraps
|
||||
import logging
|
||||
from flask import jsonify
|
||||
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
class PermissionDenied(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class BadRequest(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def httperror(f):
|
||||
@wraps(f)
|
||||
def func(*args, **kwargs):
|
||||
result = {
|
||||
'code': 400,
|
||||
'errorMessage': '',
|
||||
}
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except PermissionDenied as e:
|
||||
result['code'] = 403
|
||||
result['errorMessage'] = str(e)
|
||||
except BadRequest as e:
|
||||
result['code'] = 400
|
||||
result['errorMessage'] = str(e)
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
result['code'] = 500
|
||||
result['errorMessage'] = str(e)
|
||||
return jsonify(result)
|
||||
return func
|
||||
@@ -0,0 +1,127 @@
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, with_statement
|
||||
)
|
||||
from os import environ
|
||||
from gevent.event import Event
|
||||
from gevent import subprocess as gsp
|
||||
from re import search as research
|
||||
from .log import log
|
||||
|
||||
|
||||
class State(object):
|
||||
def __init__(self):
|
||||
self._eid = 0
|
||||
self._event = Event()
|
||||
self._w = self._h = self._health = None
|
||||
self.size_changed_count = 0
|
||||
|
||||
def wait(self, eid, timeout=5):
|
||||
if eid < self._eid:
|
||||
return
|
||||
self._event.clear()
|
||||
self._event.wait(timeout)
|
||||
return self._eid
|
||||
|
||||
def notify(self):
|
||||
self._eid += 1
|
||||
self._event.set()
|
||||
|
||||
def _update_health(self):
|
||||
if self._health:
|
||||
return
|
||||
|
||||
health = True
|
||||
output = gsp.check_output(['supervisorctl', 'status'])
|
||||
for line in output.strip().split('\n'):
|
||||
if not line.startswith('web') and line.find('RUNNING') < 0:
|
||||
health = False
|
||||
break
|
||||
if self._health != health:
|
||||
self._health = health
|
||||
self.notify()
|
||||
return self._health
|
||||
|
||||
def to_dict(self):
|
||||
self._update_health()
|
||||
|
||||
state = {
|
||||
'id': self._eid,
|
||||
'config': {
|
||||
'fixedResolution': 'RESOLUTION' in environ,
|
||||
'sizeChangedCount': self.size_changed_count
|
||||
}
|
||||
}
|
||||
|
||||
self._update_size()
|
||||
state.update({
|
||||
'width': self.w,
|
||||
'height': self.h,
|
||||
})
|
||||
|
||||
return state
|
||||
|
||||
def set_size(self, w, h):
|
||||
gsp.check_call((
|
||||
'sed -i \'s#'
|
||||
'^exec /usr/bin/Xvfb.*$'
|
||||
'#'
|
||||
'exec /usr/bin/Xvfb :1 -screen 0 {}x{}x16'
|
||||
'#\' /usr/local/bin/xvfb.sh'
|
||||
).format(w, h), shell=True)
|
||||
self.size_changed_count += 1
|
||||
|
||||
def apply_and_restart(self):
|
||||
gsp.check_call(['supervisorctl', 'restart', 'x:'])
|
||||
self._w = self._h = self._health = None
|
||||
self.notify()
|
||||
|
||||
def switch_video(self, onoff):
|
||||
xenvs = {
|
||||
'DISPLAY': ':1',
|
||||
}
|
||||
try:
|
||||
cmd = 'nofb' if onoff else 'fb'
|
||||
gsp.check_output(['x11vnc', '-remote', cmd], env=xenvs)
|
||||
except gsp.CalledProcessError as e:
|
||||
log.warn('failed to set x11vnc fb: ' + str(e))
|
||||
|
||||
def _update_size(self):
|
||||
if self._w is not None and self._h is not None:
|
||||
return
|
||||
xenvs = {
|
||||
'DISPLAY': ':1',
|
||||
}
|
||||
try:
|
||||
output = gsp.check_output([
|
||||
'x11vnc', '-query', 'dpy_x,dpy_y'
|
||||
], env=xenvs).decode('utf-8')
|
||||
mobj = research(r'dpy_x:(\d+).*dpy_y:(\d+)', output)
|
||||
if mobj is not None:
|
||||
w, h = int(mobj.group(1)), int(mobj.group(2))
|
||||
changed = False
|
||||
if self._w != w:
|
||||
changed = True
|
||||
self._w = w
|
||||
if self._h != h:
|
||||
changed = True
|
||||
self._h = h
|
||||
if changed:
|
||||
self.notify()
|
||||
except gsp.CalledProcessError as e:
|
||||
log.warn('failed to get dispaly size: ' + str(e))
|
||||
|
||||
@property
|
||||
def w(self):
|
||||
return self._w
|
||||
|
||||
@property
|
||||
def h(self):
|
||||
return self._h
|
||||
|
||||
@property
|
||||
def health(self):
|
||||
self._update_health()
|
||||
return self._health
|
||||
|
||||
|
||||
state = State()
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import (
|
||||
absolute_import, division, print_function, with_statement
|
||||
)
|
||||
from contextlib import contextmanager
|
||||
from gevent import GreenletExit
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ignored(*exceptions):
|
||||
try:
|
||||
yield
|
||||
except GreenletExit as e:
|
||||
raise e
|
||||
except exceptions:
|
||||
pass
|
||||
Reference in New Issue
Block a user