Skip to content

Instantly share code, notes, and snippets.

@zhsj
Last active January 16, 2020 09:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zhsj/d263556dd5d884e63cd92a677d6eb2df to your computer and use it in GitHub Desktop.
Save zhsj/d263556dd5d884e63cd92a677d6eb2df to your computer and use it in GitHub Desktop.
nutstore-pydaemon.py python3 port
#!/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