V0.5.4 - Fix macOS autostart implementation (#82)

* Add own macOS Keychain implementation to avoid conflict with autostart. #81
* Resolve conflight with pytest-xdist and pyobjc.
* Move requirements.txt files to own folder.
This commit is contained in:
Manuel Riel 2018-12-06 18:53:42 +08:00 committed by GitHub
commit 97fcf08934
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 52 deletions

View File

@ -67,7 +67,7 @@ install:
- pip install -U setuptools pip
- pip install .
- pip install borgbackup
- pip install -r requirements-dev.txt
- pip install -r requirements.d/dev.txt
before_script:
- if [ $TRAVIS_OS_NAME = "linux" ]; then (herbstluftwm )& fi

View File

@ -51,7 +51,7 @@ $ vorta
Install developer packages we use (pytest, tox, pyinstaller):
```
pip install -r requirements-dev.txt
pip install -r requirements.d/dev.txt
```
Qt Creator is used to edit views. Install from [their site](https://www.qt.io/download) or using Homebrew and then open the .ui files in `vorta/UI`:

23
requirements.d/conda.yml Normal file
View File

@ -0,0 +1,23 @@
name: vorta
channels:
- conda-forge
- defaults
dependencies:
- python=3.6.7
- appdirs
- paramiko
- pyqt
- peewee
- python-dateutil
- keyring
- apscheduler
- sentry-sdk
- psutil
- pyobjc-core
- pyobjc-framework-Cocoa
- pyinstaller
- pip:
- sentry-sdk

View File

@ -41,6 +41,7 @@ install_requires =
psutil
pyobjc-core; sys_platform == 'darwin'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
pyobjc-framework-LaunchServices; sys_platform == 'darwin'
tests_require =
pytest
pytest-qt

View File

@ -0,0 +1,76 @@
# flake8: noqa
"""
A dirty objc implementation to access the macOS Keychain. Because the
keyring implementation was causing trouble when used together with other
objc modules.
Adapted from https://gist.github.com/apettinen/5dc7bf1f6a07d148b2075725db6b1950
"""
from keyring.backend import KeyringBackend
class VortaDarwinKeyring(KeyringBackend):
"""Homemade macOS Keychain Service"""
login_keychain = None
def _set_keychain(self):
"""
Lazy import to avoid conflict with pytest-xdist.
"""
import objc
from Foundation import NSBundle
Security = NSBundle.bundleWithIdentifier_('com.apple.security')
S_functions = [
('SecKeychainGetTypeID', b'I'),
('SecKeychainItemGetTypeID', b'I'),
('SecKeychainAddGenericPassword', b'i^{OpaqueSecKeychainRef=}I*I*I*o^^{OpaqueSecKeychainItemRef}'),
('SecKeychainOpen', b'i*o^^{OpaqueSecKeychainRef}'),
('SecKeychainFindGenericPassword', b'i@I*I*o^Io^^{OpaquePassBuff}o^^{OpaqueSecKeychainItemRef}'),
]
objc.loadBundleFunctions(Security, globals(), S_functions)
SecKeychainRef = objc.registerCFSignature('SecKeychainRef', b'^{OpaqueSecKeychainRef=}', SecKeychainGetTypeID())
SecKeychainItemRef = objc.registerCFSignature('SecKeychainItemRef', b'^{OpaqueSecKeychainItemRef=}', SecKeychainItemGetTypeID())
PassBuffRef = objc.createOpaquePointerType('PassBuffRef', b'^{OpaquePassBuff=}', None)
# Get the login keychain
result, login_keychain = SecKeychainOpen(b'login.keychain', None)
self.login_keychain = login_keychain
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
if not self.login_keychain: self._set_keychain()
SecKeychainAddGenericPassword(
self.login_keychain,
len(service), service.encode(),
len(repo_url), repo_url.encode(),
len(password), password.encode(),
None)
def get_password(self, service, repo_url):
if not self.login_keychain: self._set_keychain()
result, password_length, password_buffer, keychain_item = SecKeychainFindGenericPassword(
self.login_keychain, len(service), service.encode(), len(repo_url), repo_url.encode(), None, None, None)
password = None
if (result == 0) and (password_length != 0):
# We apparently were able to find a password
password = _resolve_password(password_length, password_buffer)
return password
def delete_password(self, service, repo_url):
pass
def _resolve_password(password_length, password_buffer):
from ctypes import c_char
return (c_char * password_length).from_address(password_buffer.__pointer__)[:].decode('utf-8')

32
src/vorta/keyring_db.py Normal file
View File

@ -0,0 +1,32 @@
import keyring
class VortaDBKeyring(keyring.backend.KeyringBackend):
"""
Our own fallback keyring service. Uses the main database
to store repo passwords if no other (more secure) backend
is available.
"""
@classmethod
def priority(cls):
return 5
def set_password(self, service, repo_url, password):
from .models import RepoPassword
keyring_entry, created = RepoPassword.get_or_create(
url=repo_url,
defaults={'password': password}
)
keyring_entry.password = password
keyring_entry.save()
def get_password(self, service, repo_url):
from .models import RepoPassword
try:
keyring_entry = RepoPassword.get(url=repo_url)
return keyring_entry.password
except Exception:
return None
def delete_password(self, service, repo_url):
pass

View File

@ -7,9 +7,9 @@ def get_updater():
if sys.platform == 'darwin' and getattr(sys, 'frozen', False):
# Use sparkle framework on macOS.
# Examples: https://programtalk.com/python-examples/objc.loadBundle/
from objc import loadBundle
import objc
bundle_path = os.path.join(os.path.dirname(sys.executable), os.pardir, 'Frameworks', 'Sparkle.framework')
loadBundle('Sparkle', globals(), bundle_path)
objc.loadBundle('Sparkle', globals(), bundle_path)
sparkle = SUUpdater.sharedUpdater() # noqa: F821
if SettingsModel.get(key='updates_include_beta').value:
sparkle.SharedUpdater.FeedURL = 'https://borgbase.github.io/vorta/appcast-pre.xml'

View File

@ -18,39 +18,23 @@ from PyQt5.QtGui import QIcon
from PyQt5 import QtCore
import subprocess
import keyring
from vorta.keyring_db import VortaDBKeyring
class VortaKeyring(keyring.backend.KeyringBackend):
"""Fallback keyring service."""
@classmethod
def priority(cls):
return 5
"""
Set the most appropriate Keyring backend for the current system.
def set_password(self, service, repo_url, password):
from .models import RepoPassword
keyring_entry, created = RepoPassword.get_or_create(
url=repo_url,
defaults={'password': password}
)
keyring_entry.password = password
keyring_entry.save()
For macOS we use our own implementation due to conflicts between
Keyring and the autostart code.
def get_password(self, service, repo_url):
from .models import RepoPassword
try:
keyring_entry = RepoPassword.get(url=repo_url)
return keyring_entry.password
except Exception:
return None
def delete_password(self, service, repo_url):
pass
# Select keyring/Workaround for pyinstaller+keyring issue.
For Linux not every system has SecretService available, so it will
fall back to a simple database keystore if needed.
"""
if sys.platform == 'darwin':
from keyring.backends import OS_X
keyring.set_keyring(OS_X.Keyring())
# from keyring.backends import OS_X
# keyring.set_keyring(OS_X.Keyring())
from vorta.keyring_darwin import VortaDarwinKeyring
keyring.set_keyring(VortaDarwinKeyring())
elif sys.platform == 'win32':
from keyring.backends import Windows
keyring.set_keyring(Windows.WinVaultKeyring())
@ -60,9 +44,9 @@ elif sys.platform == 'linux':
SecretService.Keyring.priority() # Test if keyring works.
keyring.set_keyring(SecretService.Keyring())
except Exception:
keyring.set_keyring(VortaKeyring())
keyring.set_keyring(VortaDBKeyring())
else: # Fall back to saving password to database.
keyring.set_keyring(VortaKeyring())
keyring.set_keyring(VortaDBKeyring())
def nested_dict():
@ -222,22 +206,25 @@ def set_tray_icon(tray, active=False):
def open_app_at_startup(enabled=True):
"""
This function adds/removes the current app bundle from Login items in macOS
"""
if sys.platform == 'darwin':
print('Not implemented due to conflict with keyring package.')
# From https://stackoverflow.com/questions/26213884/cocoa-add-app-to-startup-in-sandbox-using-pyobjc
# from Foundation import NSDictionary
# from Cocoa import NSBundle, NSURL
# from CoreFoundation import kCFAllocatorDefault
# from LaunchServices import (LSSharedFileListCreate, kLSSharedFileListSessionLoginItems,
# LSSharedFileListInsertItemURL, kLSSharedFileListItemHidden,
# kLSSharedFileListItemLast, LSSharedFileListItemRemove)
#
# app_path = NSBundle.mainBundle().bundlePath()
# url = NSURL.alloc().initFileURLWithPath_(app_path)
# login_items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, None)
# props = NSDictionary.dictionaryWithObject_forKey_(True, kLSSharedFileListItemHidden)
#
# new_item = LSSharedFileListInsertItemURL(login_items, kLSSharedFileListItemLast,
# None, None, url, props, None)
# if not enabled:
# LSSharedFileListItemRemove(login_items, new_item)
from Foundation import NSDictionary
from Cocoa import NSBundle, NSURL
from CoreFoundation import kCFAllocatorDefault
# CF = CDLL(find_library('CoreFoundation'))
from LaunchServices import (LSSharedFileListCreate, kLSSharedFileListSessionLoginItems,
LSSharedFileListInsertItemURL, kLSSharedFileListItemHidden,
kLSSharedFileListItemLast, LSSharedFileListItemRemove)
app_path = NSBundle.mainBundle().bundlePath()
url = NSURL.alloc().initFileURLWithPath_(app_path)
login_items = LSSharedFileListCreate(kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, None)
props = NSDictionary.dictionaryWithObject_forKey_(True, kLSSharedFileListItemHidden)
new_item = LSSharedFileListInsertItemURL(login_items, kLSSharedFileListItemLast,
None, None, url, props, None)
if not enabled:
LSSharedFileListItemRemove(login_items, new_item)