Last active
January 16, 2020 09:04
-
-
Save zhsj/d263556dd5d884e63cd92a677d6eb2df to your computer and use it in GitHub Desktop.
nutstore-pydaemon.py python3 port
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# vim: set sw=4 ts=4: | |
import socket | |
import os | |
import threading | |
import time | |
import sys | |
import platform | |
import tempfile | |
import fcntl | |
import errno | |
import base64 | |
import subprocess | |
NOTIFY_HAS_ACTIONS = False | |
CAN_SELECT_FILE = False | |
try: | |
import notify2 | |
notify2.init("Nutstore") | |
if 'actions' in notify2.get_server_caps(): | |
NOTIFY_HAS_ACTIONS = True | |
except: | |
print("Python: can not import python-notify package") | |
try: | |
out = subprocess.check_output(["nautilus", "--version"]) | |
if out and out.startswith("GNOME nautilus"): | |
CAN_SELECT_FILE = True | |
else: | |
print("unsupported select file in file manager") | |
except Exception as e: | |
print(("Cannot found expected file manager,", e)) | |
def is_linux(): | |
return platform.system() == 'Linux' | |
def is_OSX(): | |
return platform.system() == 'Darwin' | |
def is_x86_64(): | |
return platform.machine() == 'x86_64' | |
def get_dist_dir(): | |
"""Get the nutstore distribution install directory by the path to the python script""" | |
path = os.path.dirname(sys.argv[0]) | |
abs_path = os.path.abspath(path) | |
if os.path.basename(abs_path) != 'bin': | |
raise SystemError("the python script is not under bin") | |
return os.path.dirname(abs_path) | |
def set_dir_icon(dir_path, icon_type): | |
"""set the custom icon of the directory """ | |
if not is_linux(): | |
return | |
dist_dir = get_dist_dir() | |
resource_dir = os.path.join(dist_dir, 'res') | |
if icon_type == 'normal': | |
os.system('gvfs-set-attribute -t unset "%s" metadata::custom-icon' % dir_path) | |
elif icon_type == 'sandbox_ronly_bound': | |
os.system('gvfs-set-attribute -t string "%s" metadata::custom-icon file://%s' % (dir_path, os.path.join(resource_dir, 'linux_sandbox_ronly_bound.png'))) | |
elif icon_type == 'sandbox_rw_bound': | |
os.system('gvfs-set-attribute -t string "%s" metadata::custom-icon file://%s' % (dir_path, os.path.join(resource_dir, 'linux_sandbox_rw_bound.png'))) | |
else: | |
print('Unknown directory type %s' % icon_type) | |
def notifier_open_URL(notif, action): | |
url = notif.get_data('url') | |
notif.close() | |
os.system('xdg-open "%s"' % url) | |
def notifier_closed(notif): | |
notif.close() | |
def show_message(opts): | |
if 'notify2' not in sys.modules: | |
return | |
title = opts['TITLE'] | |
desc = opts['DESC'] if 'DESC' in opts else '' | |
level = opts['LEVEL'] if 'LEVEL' in opts else 'normal' | |
timeout = int(opts['TIMEOUT']) if 'TIMEOUT' in opts else 10 | |
url = opts['URL'] if 'URL' in opts else None | |
obj = notify2.Notification(title, desc, "nutstore") | |
if level == 'critical': | |
obj.set_urgency(notify2.URGENCY_CRITICAL) | |
elif level == 'low': | |
obj.set_urgency(notify2.URGENCY_LOW) | |
else: | |
obj.set_urgency(notify2.URGENCY_NORMAL) | |
obj.set_category("%d" % show_message.message_counter) # fake category | |
show_message.message_counter += 1 | |
if timeout > 0: | |
obj.set_timeout(timeout * 1000) | |
else: | |
obj.set_timeout(notify2.EXPIRES_NEVER) | |
# FIXME -- do NOT support URL opening | |
# if NOTIFY_HAS_ACTIONS and url: | |
# obj.set_data('url', url) | |
# obj.add_action("Open", "Open URL", notifier_open_URL) | |
# obj.connect('closed', notifier_closed) | |
obj.show() | |
show_message.message_counter = 0 | |
class JavaAppWatchDog(threading.Thread): | |
def __init__(self): | |
# invoke the super class's constructor, if it has | |
fun = getattr(threading.Thread, "__init__", lambda x: None) | |
fun(self) | |
self.daemon = True | |
# It should only be set when the java app is restarted to migrate to another nutstore home | |
self.__new_nutstore_home = None | |
# It should only be set when the java app is restarted by adding more ignore path | |
self.__ignore_path_list = None | |
# How many times the java app has been restarted | |
self.__restart_num = 0 | |
# It should only be set when the java app is restarted by switch account | |
self.__switch_account = None | |
# flag to indicate whether the watch dog thread should quit | |
self.__exit = False | |
# debug settings | |
self.__debug_args = ' '.join(sys.argv[1:]) | |
self.__lock = threading.RLock() | |
def set_new_nutstore_home(self, new_home_dir): | |
self.__lock.acquire() | |
try: | |
self.__new_nutstore_home = new_home_dir | |
finally: | |
self.__lock.release() | |
def get_new_nutstore_home(self): | |
self.__lock.acquire() | |
try: | |
return self.__new_nutstore_home | |
finally: | |
self.__lock.release() | |
# Used to tell the java app to migrate to a new nutstore home dir | |
new_nutstore_home = property(get_new_nutstore_home, set_new_nutstore_home) | |
def set_ignore_path_list(self, ignore_path_list): | |
self.__lock.acquire() | |
try: | |
self.__ignore_path_list = ignore_path_list | |
finally: | |
self.__lock.release() | |
def get_ignore_path_list(self): | |
self.__lock.acquire() | |
try: | |
return self.__ignore_path_list | |
finally: | |
self.__lock.release() | |
# Used to tell the java app to change the ignore path list | |
ignore_path_list = property(get_ignore_path_list, set_ignore_path_list) | |
def set_switch_account(self, arg): | |
self.__lock.acquire() | |
try: | |
self.__switch_account = arg | |
finally: | |
self.__lock.release() | |
def get_switch_account(self): | |
self.__lock.acquire() | |
try: | |
return self.__switch_account | |
finally: | |
self.__lock.release() | |
# Used to tell the java app to switch account | |
switch_account = property(get_switch_account, set_switch_account) | |
def get_class_path(self, lib_dir): | |
class_path = [] | |
for name in os.listdir(lib_dir): | |
child = os.path.join(lib_dir, name) | |
if os.path.isdir(child) and child != 'native': | |
class_path.extend(self.get_class_path(child)) | |
elif os.path.isfile(child) and name.endswith('.jar'): | |
class_path.append(child) | |
return class_path | |
def start_java_app(self, args): | |
""" start the java client application. The directory hierarchy is hard coded. This method is blocked until | |
the java application is terminated. | |
args -- the string passed as application string | |
""" | |
const_jvm_settings = 'exec java -ea -client -Dfile.encoding=UTF-8 -Xmx320M -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40 -Dlog4j.defaultInitOverride=true' | |
if is_OSX(): | |
const_jvm_settings += ' -XstartOnFirstThread' | |
dist_dir = get_dist_dir() | |
conf_dir = os.path.join(dist_dir, 'conf') | |
lib_dir = os.path.join(dist_dir, 'lib') | |
native_lib_dir = os.path.join(lib_dir, 'native') | |
jvm_settings_A = '-Djava.util.logging.config.file=%s/java.logging.properties -Dnutstore.config.dir=%s -Dnutstore.x64=%s' % (conf_dir, conf_dir, is_x86_64()) | |
jvm_settings_B = '-Djava.library.path=%s -cp %s nutstore.client.gui.NutstoreGUI %s' % (native_lib_dir, ':'.join(self.get_class_path(lib_dir)), args) | |
jvm_settings = '%s %s %s' % (const_jvm_settings, jvm_settings_A, jvm_settings_B) | |
os.system(jvm_settings) | |
def is_java_app_alive(self, block=True): | |
""" check whether the java client is still alive by try to lock the exclusive lock | |
block -- block to acquire the file lock if it is set as True """ | |
lock_file_path = os.path.join(tempfile.gettempdir(), 'NutstoreTmp0xyz', 'nutstore.flock') | |
if not os.path.isfile(lock_file_path): | |
return False | |
lock_file = None | |
# Python 2.4 doesn't support try-except-finally, but support try-except and try-finally | |
try: | |
try: | |
lock_file = open(lock_file_path, 'w') | |
lock_flag = fcntl.LOCK_EX | |
if not block: | |
lock_flag |= fcntl.LOCK_NB | |
fcntl.lockf(lock_file, lock_flag) | |
# we can acquire the lock, so the java client must be dead | |
fcntl.lockf(lock_file, fcntl.LOCK_UN) | |
return False | |
except IOError as exception: | |
if exception.errno == errno.EAGAIN or exception.errno == errno.EACCES: | |
# can not acquire the file lock, the java client is alive | |
return True | |
raise | |
finally: | |
if lock_file: | |
lock_file.close() | |
def exit(self): | |
""" Tell the watch dog thread to exit """ | |
self.__lock.acquire() | |
try: | |
self.__exit = True | |
finally: | |
self.__lock.release() | |
def should_exit(self): | |
self.__lock.acquire() | |
try: | |
return self.__exit | |
finally: | |
self.__lock.release() | |
def inc_and_get_restart_num(self): | |
self.__lock.acquire() | |
try: | |
self.__restart_num += 1 | |
return self.__restart_num | |
finally: | |
self.__lock.release() | |
def reset_restart_num(self): | |
self.__lock.acquire() | |
try: | |
self.__restart_num = 0 | |
finally: | |
self.__lock.release() | |
def run(self): | |
time_start = 0 | |
while not self.should_exit(): | |
# double check should_exit() after the blocking wait on the file lock | |
if not self.is_java_app_alive() and not self.should_exit(): | |
print('The java client is dead, try to restart it') | |
now = time.time() | |
if now - time_start > 600: | |
# reset the restart number every 10 minutes | |
self.reset_restart_num() | |
time_start = now | |
# Tell the java client how many times it has been restarted | |
restart_num = self.inc_and_get_restart_num() | |
if restart_num > 10: | |
print('We have restarted %d times, so abort it' % restart_num) | |
# avoid restarting the java client again and again. The threshold should be | |
# larger than the threshold of java client, which is 5 so that java client can detect the | |
# problem and notify the user. This should only be triggered when java client is | |
# crashed too early, e.g. the gnome/gtk environment is not ready and it can not | |
# be initialized | |
os._exit(-1) | |
if self.__debug_args: | |
extra_args = self.__debug_args | |
self.__debug_args = None | |
else: | |
extra_args = '' | |
extra_args = '%s --restart %d ' % (extra_args, restart_num) | |
new_home = self.new_nutstore_home | |
if new_home: | |
# clear it so that we never migrate the nutstore box again and again | |
self.new_nutstore_home = None | |
extra_args = '%s --migrate %s ' % (extra_args, new_home) | |
ignore_path = self.ignore_path_list | |
if ignore_path: | |
self.ignore_path_list = None | |
extra_args = '%s --ignore-path %s ' % (extra_args, ignore_path) | |
switch_account_arg = self.switch_account | |
if switch_account_arg: | |
self.switch_account = None | |
extra_args = '%s --switch-account %s ' % (extra_args, switch_account_arg) | |
self.start_java_app(extra_args) | |
# Backoff before restart it again. If we restart it too frequently, something unexpected will happen. | |
# For exmaple, it might prevent the GUI process from being shutdown properly. | |
time.sleep(5) | |
class FileOrURLAnchor(threading.Thread): | |
""" open a file or URL in another thread, that is, the original thread will not be blocked to wait for the script return""" | |
def __init__(self, file_or_url): | |
# invoke the super class's constructor, if it has | |
fun = getattr(threading.Thread, "__init__", lambda x: None) | |
fun(self) | |
self.daemon = True | |
self.__file_or_url = file_or_url | |
self.__is_url = not file_or_url.startswith('/') | |
def open_file_with_select(self): | |
if self.__is_url: | |
self.xdg_open() | |
return | |
if CAN_SELECT_FILE: | |
try: | |
subprocess.call(["nautilus", self.__file_or_url]) | |
except OSError as e: | |
print("cannot execute nautilus", e) | |
return | |
if os.path.isfile(self.__file_or_url): | |
self.__file_or_url = os.path.dirname(self.__file_or_url) | |
self.xdg_open() | |
def xdg_open(self): | |
"""open a URL or file address by xdg_open. This is only works for linux. """ | |
try: | |
os.system('xdg-open "%s"' % self.__file_or_url) | |
except OSError as exception: | |
print('Cannot execute xdg-open', exception) | |
def osx_open(self): | |
"""open a URL or file address by open. This is only works for mac os x. """ | |
try: | |
os.system('open "%s"' % self.__file_or_url) | |
except OSError as exception: | |
print('Cannot execute open', exception) | |
def run(self): | |
if is_linux(): | |
self.open_file_with_select() | |
elif is_OSX(): | |
self.osx_open() | |
else: | |
print("unknown os type %s" % platform.system()) | |
def upgrade(tar_file): | |
"""upgrade the nutstore runtime, by zipping out the tar file and execute the upgrade script """ | |
tmp_dir = tempfile.mkdtemp(prefix="Nutstore") | |
os.system('tar xzf "%s" -C "%s"' % (tar_file, tmp_dir)) | |
os.execv(os.path.join(tmp_dir, "bin", "runtime_upgrade"), ("runtime_upgrade", tar_file)) | |
def main_loop(): | |
watchDog = JavaAppWatchDog() | |
watchDog.start() | |
listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
listen.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) | |
listen.bind(('localhost', 19645)) | |
while True: | |
listen.listen(16) | |
(conn, addr) = listen.accept() | |
file = None | |
try: | |
file = conn.makefile(mode='rw') | |
lines = [] | |
while True: | |
line = file.readline().rstrip('\r\n') | |
lines.append(line) | |
if line == 'done' or line == '': | |
break | |
if lines[-1] != 'done': | |
print('malformed request %s' % lines) | |
continue | |
if lines[0] == 'open': | |
if len(lines) != 3: | |
print('malformed request %s' % lines) | |
else: | |
# This line is encoded by base64 | |
file_or_url = base64.standard_b64decode(lines[1]) | |
anchor = FileOrURLAnchor(file_or_url) | |
anchor.start() | |
elif lines[0] == 'migrate_home': | |
if len(lines) != 3: | |
print('malformed request %s' % lines) | |
else: | |
watchDog.new_nutstore_home = lines[1] | |
# send back the response only when we are prepared to migrate the home to another dir | |
file.write('rsp\ndone\n') | |
elif lines[0] == 'ignore_paths': | |
if len(lines) != 3: | |
print('malformed request %s' % lines) | |
else: | |
watchDog.ignore_path_list = lines[1] | |
# send back the response only when we are prepared to migrate the home to another dir | |
file.write('rsp\ndone\n') | |
elif lines[0] == 'switch_account': | |
if len(lines) != 3: | |
print('malformed request %s' % lines) | |
else: | |
watchDog.switch_account = lines[1] | |
# send back the response only when we are prepared to switch account | |
file.write('rsp\ndone\n') | |
elif lines[0] == 'set_folder_icon': | |
if len(lines) != 4: | |
print('malformed request %s ' % lines) | |
else: | |
# This line is encoded by base64 | |
dir_path = base64.standard_b64decode(lines[1]) | |
icon_type = lines[2] | |
set_dir_icon(dir_path, icon_type) | |
# send back the response only when we are prepared to migrate the home to another dir | |
file.write('rsp\ndone\n') | |
elif lines[0] == 'show_message': | |
if len(lines) < 3: | |
print('malformed request %s ' % lines) | |
else: | |
opts = {} | |
for s in lines[1:-1]: | |
kv_array = s.split('=', 1) | |
if len(kv_array) == 2: | |
key = kv_array[0].strip(' ') | |
val = kv_array[1].strip(' ') | |
opts[key] = val | |
if 'TITLE' not in opts: | |
print('TITLE is required but not found in request: %s ' % lines) | |
else: | |
file.write('rsp\ndone\n') | |
show_message(opts) | |
elif lines[0] == 'restart': | |
# The java app likes to restart itself, reset the restart number so that it will not report the failure | |
# because the java app is restarted frequently | |
watchDog.reset_restart_num() | |
file.write('rsp\ndone\n') | |
elif lines[0] == 'upgrade': | |
if len(lines) != 3: | |
print('malformed request %s' % lines) | |
else: | |
# avoid the watch dog to restart the java application | |
watchDog.exit() | |
# ack the java client so that it can quit immediately | |
file.write('rsp\ndone\n') | |
# close the socket because we will start to execute the script | |
file.close() | |
conn.close() | |
file = None | |
conn = None | |
listen.close() | |
# When the watch dog is dead, we are sure that the java client was exit | |
watchDog.join() | |
upgrade(lines[1]) | |
elif lines[0] == 'exit': | |
sys.exit(0) | |
else: | |
print("unknown request %s " % lines) | |
finally: | |
if file: | |
file.close() | |
if conn: | |
conn.close() | |
if __name__ == '__main__': | |
main_loop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment