importing from the command line

- a file path provided on the command line now will now trigger the import
  process
- if anki was already running on win/lin, send the import to that process
- if no profile was open, wait until profile opened before importing
- don't do that for password-protected profiles, as it could be used abusively
  in a school environment
- drop the old pid-based instance check
This commit is contained in:
Damien Elmes 2012-07-28 13:01:41 +09:00
parent f7b41a11f6
commit bc758220a7
7 changed files with 139 additions and 61 deletions

View file

@ -1,7 +1,7 @@
# Copyright: Damien Elmes <anki@ichi2.net> # Copyright: Damien Elmes <anki@ichi2.net>
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import os, sys, __builtin__ import os, sys, optparse, atexit, __builtin__
from aqt.qt import * from aqt.qt import *
import locale, gettext import locale, gettext
import anki.lang import anki.lang
@ -103,13 +103,78 @@ def setupLang(pm, app, force=None):
class AnkiApp(QApplication): class AnkiApp(QApplication):
# Single instance support on Win32/Linux
##################################################
KEY = "anki"
TMOUT = 5000
def __init__(self, argv):
QApplication.__init__(self, argv)
self._argv = argv
self._shmem = QSharedMemory(self.KEY)
self.alreadyRunning = self._shmem.attach()
def secondInstance(self):
if not self.alreadyRunning:
# use a 1 byte shared memory instance to signal we exist
if not self._shmem.create(1):
raise Exception("shared memory not supported")
atexit.register(self._shmem.detach)
# and a named pipe/unix domain socket for ipc
QLocalServer.removeServer(self.KEY)
self._srv = QLocalServer(self)
self.connect(self._srv, SIGNAL("newConnection()"), self.onRecv)
self._srv.listen(self.KEY)
# if we were given a file on startup, send import it
else:
# we accept only one command line argument. if it's missing, send
# a blank screen to just raise the existing window
opts, args = parseArgs(self._argv)
buf = "raise"
if args and args[0]:
buf = os.path.abspath(args[0])
self.sendMsg(buf)
return True
def sendMsg(self, txt):
sock = QLocalSocket(self)
sock.connectToServer(self.KEY, QIODevice.WriteOnly)
if not sock.waitForConnected(self.TMOUT):
raise Exception("existing instance not responding")
sock.write(txt)
if not sock.waitForBytesWritten(self.TMOUT):
raise Exception("existing instance not emptying")
sock.disconnectFromServer()
def onRecv(self):
sock = self._srv.nextPendingConnection()
if not sock.waitForReadyRead(self.TMOUT):
sys.stderr.write(sock.errorString())
return
buf = sock.readAll()
self.emit(SIGNAL("appMsg"), unicode(buf))
sock.disconnectFromServer()
# OS X file/url handler
##################################################
def event(self, evt): def event(self, evt):
from anki.hooks import runHook from anki.hooks import runHook
if evt.type() == QEvent.FileOpen: if evt.type() == QEvent.FileOpen:
runHook("macLoadEvent", unicode(evt.file())) self.emit(SIGNAL("appMsg"), evt.file() or "raise")
return True return True
return QApplication.event(self, evt) return QApplication.event(self, evt)
def parseArgs(argv):
"Returns (opts, args)."
parser = optparse.OptionParser()
parser.usage = "%prog [OPTIONS] [file to import]"
parser.add_option("-b", "--base", help="path to base folder")
parser.add_option("-p", "--profile", help="profile name to load")
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
return parser.parse_args(argv[1:])
def run(): def run():
global mw global mw
from anki.utils import isWin, isMac from anki.utils import isWin, isMac
@ -122,15 +187,12 @@ def run():
# create the app # create the app
app = AnkiApp(sys.argv) app = AnkiApp(sys.argv)
QCoreApplication.setApplicationName("Anki") QCoreApplication.setApplicationName("Anki")
if app.secondInstance():
# we've signaled the primary instance, so we should close
return
# parse args # parse args
import optparse opts, args = parseArgs(sys.argv)
parser = optparse.OptionParser()
parser.usage = "%prog [OPTIONS]"
parser.add_option("-b", "--base", help="path to base folder")
parser.add_option("-p", "--profile", help="profile name to load")
parser.add_option("-l", "--lang", help="interface language (en, de, etc)")
(opts, args) = parser.parse_args(sys.argv[1:])
opts.base = unicode(opts.base or "", sys.getfilesystemencoding()) opts.base = unicode(opts.base or "", sys.getfilesystemencoding())
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding()) opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding())
@ -142,12 +204,11 @@ def run():
setupLang(pm, app, opts.lang) setupLang(pm, app, opts.lang)
# remaining pm init # remaining pm init
pm.checkPid()
pm.ensureProfile() pm.ensureProfile()
# load the main window # load the main window
import aqt.main import aqt.main
mw = aqt.main.AnkiQt(app, pm) mw = aqt.main.AnkiQt(app, pm, args)
app.exec_() app.exec_()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -249,6 +249,9 @@ def onImport(mw):
if not file: if not file:
return return
file = unicode(file) file = unicode(file)
importFile(mw, file)
def importFile(mw, file):
ext = os.path.splitext(file)[1] ext = os.path.splitext(file)[1]
importer = None importer = None
done = False done = False

View file

@ -21,8 +21,9 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
tooltip, openHelp, openLink tooltip, openHelp, openLink
class AnkiQt(QMainWindow): class AnkiQt(QMainWindow):
def __init__(self, app, profileManager): def __init__(self, app, profileManager, args):
QMainWindow.__init__(self) QMainWindow.__init__(self)
self.state = "startup"
aqt.mw = self aqt.mw = self
self.app = app self.app = app
self.pm = profileManager self.pm = profileManager
@ -43,21 +44,23 @@ class AnkiQt(QMainWindow):
except: except:
showInfo(_("Error during startup:\n%s") % traceback.format_exc()) showInfo(_("Error during startup:\n%s") % traceback.format_exc())
sys.exit(1) sys.exit(1)
# were we given a file to import?
if args and args[0]:
self.onAppMsg(args[0])
# Load profile in a timer so we can let the window finish init and not # Load profile in a timer so we can let the window finish init and not
# close on profile load error. # close on profile load error.
self.progress.timer(10, self.setupProfile, False) self.progress.timer(10, self.setupProfile, False)
def setupUI(self): def setupUI(self):
self.col = None self.col = None
self.state = "overview"
self.hideSchemaMsg = False self.hideSchemaMsg = False
self.setupAppMsg()
self.setupKeys() self.setupKeys()
self.setupThreads() self.setupThreads()
self.setupFonts() self.setupFonts()
self.setupMainWindow() self.setupMainWindow()
self.setupSystemSpecific() self.setupSystemSpecific()
self.setupStyle() self.setupStyle()
self.setupProxy()
self.setupMenus() self.setupMenus()
self.setupProgress() self.setupProgress()
self.setupErrorHandler() self.setupErrorHandler()
@ -75,6 +78,7 @@ class AnkiQt(QMainWindow):
########################################################################## ##########################################################################
def setupProfile(self): def setupProfile(self):
self.pendingImport = None
# profile not provided on command line? # profile not provided on command line?
if not self.pm.name: if not self.pm.name:
# if there's a single profile, load it automatically # if there's a single profile, load it automatically
@ -91,6 +95,7 @@ class AnkiQt(QMainWindow):
self.loadProfile() self.loadProfile()
def showProfileManager(self): def showProfileManager(self):
self.state = "profileManager"
d = self.profileDiag = QDialog() d = self.profileDiag = QDialog()
f = self.profileForm = aqt.forms.profiles.Ui_Dialog() f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
f.setupUi(d) f.setupUi(d)
@ -210,6 +215,15 @@ Are you sure?""")):
self.raise_() self.raise_()
# maybe sync (will load DB) # maybe sync (will load DB)
self.onSync(auto=True) self.onSync(auto=True)
# import pending?
if self.pendingImport:
if self.pm.profile['key']:
showInfo(_("""\
To import into a password protected profile, please open the profile before attempting to import."""))
else:
import aqt.importing
aqt.importing.importFile(self, self.pendingImport)
self.pendingImport = None
runHook("profileLoaded") runHook("profileLoaded")
def unloadProfile(self, browser=True): def unloadProfile(self, browser=True):
@ -352,9 +366,13 @@ Are you sure?""")):
"Signal queue needs to be rebuilt when edits are finished or by user." "Signal queue needs to be rebuilt when edits are finished or by user."
self.autosave() self.autosave()
self.resetModal = modal self.resetModal = modal
if self.state in ("overview", "review", "deckBrowser"): if self.interactiveState():
self.moveToState("resetRequired") self.moveToState("resetRequired")
def interactiveState(self):
"True if not in profile manager, syncing, etc."
return self.state in ("overview", "review", "deckBrowser")
def maybeReset(self): def maybeReset(self):
self.autosave() self.autosave()
if self.state == "resetRequired": if self.state == "resetRequired":
@ -995,12 +1013,8 @@ will be lost. Continue?"""))
ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight) ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight)
def setupSystemSpecific(self): def setupSystemSpecific(self):
# use system font for webviews
# mac tweaks
addHook("macLoadEvent", self.onMacLoad)
if isMac: if isMac:
qt_mac_set_menubar_icons(False) qt_mac_set_menubar_icons(False)
#self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar'])
# mac users expect a minimize option # mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+M", self) self.minimizeShortcut = QShortcut("Ctrl+M", self)
self.connect(self.minimizeShortcut, SIGNAL("activated()"), self.connect(self.minimizeShortcut, SIGNAL("activated()"),
@ -1025,12 +1039,40 @@ will be lost. Continue?"""))
def onMacMinimize(self): def onMacMinimize(self):
self.setWindowState(self.windowState() | Qt.WindowMinimized) self.setWindowState(self.windowState() | Qt.WindowMinimized)
def onMacLoad(self, fname): # Single instance support
self.loadDeck(fname)
# Proxy support
########################################################################## ##########################################################################
def setupProxy(self): def setupAppMsg(self):
return self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg)
# need to bundle socksipy and install a default socket handler
def onAppMsg(self, buf):
if self.state == "startup":
# try again in a second
return self.progress.timer(1000, lambda: self.onAppMsg(buf), False)
elif self.state == "profileManager":
self.pendingImport = buf
return showInfo(_("Deck will be imported when a profile is opened."))
if not self.interactiveState() or self.progress.busy():
# we can't raise the main window while in profile dialog, syncing, etc
if buf != "raise":
showInfo(_("""\
Please ensure a profile is open and Anki is not busy, then try again."""),
parent=None)
return
# raise window
if isWin:
# on windows we can raise the window by minimizing and restoring
self.showMinimized()
self.setWindowState(Qt.WindowActive)
self.showNormal()
else:
# on osx we can raise the window. on unity the icon in the tray will just flash.
self.activateWindow()
self.raise_()
if buf == "raise":
return
# import
if not os.path.exists(buf):
return showInfo(_("Provided file does not exist."))
import aqt.importing
aqt.importing.importFile(self, buf)

View file

@ -82,39 +82,6 @@ Anki can't write to the harddisk. Please see the \
documentation for information on using a flash drive.""") documentation for information on using a flash drive.""")
raise raise
# Pid checking
######################################################################
def checkPid(self):
p = os.path.join(self.base, "pid")
# check if an existing instance is running
if os.path.exists(p):
pid = int(open(p).read())
exists = False
if isWin:
# no posix on windows, sigh
from win32process import EnumProcesses as enum
if pid in enum():
exists = True
else:
try:
os.kill(pid, 0)
exists = True
except OSError:
pass
if exists:
QMessageBox.warning(
None, "Error", _("""\
Anki is already running. Please close the existing copy or restart your \
computer."""))
raise Exception("Already running")
# write out pid to the file
open(p, "w").write(str(os.getpid()))
# add handler to cleanup on exit
def cleanup():
os.unlink(p)
atexit.register(cleanup)
# Profile load/save # Profile load/save
###################################################################### ######################################################################

View file

@ -155,3 +155,7 @@ Your pysqlite2 is too old. Anki will appear frozen during long operations."""
def _unsetBusy(self): def _unsetBusy(self):
self._disabled = False self._disabled = False
self.app.restoreOverrideCursor() self.app.restoreOverrideCursor()
def busy(self):
"True if processing."
return self._levels

View file

@ -10,6 +10,7 @@ sip.setapi('QUrl', 2)
from PyQt4.QtCore import * from PyQt4.QtCore import *
from PyQt4.QtGui import * from PyQt4.QtGui import *
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
from PyQt4 import pyqtconfig from PyQt4 import pyqtconfig
def debug(): def debug():

View file

@ -25,9 +25,9 @@ def showCritical(text, parent=None, help=""):
"Show a small critical error with an OK button." "Show a small critical error with an OK button."
return showInfo(text, parent, help, "critical") return showInfo(text, parent, help, "critical")
def showInfo(text, parent=None, help="", type="info"): def showInfo(text, parent=False, help="", type="info"):
"Show a small info window with an OK button." "Show a small info window with an OK button."
if not parent: if parent is False:
parent = aqt.mw.app.activeWindow() or aqt.mw parent = aqt.mw.app.activeWindow() or aqt.mw
if type == "warning": if type == "warning":
icon = QMessageBox.Warning icon = QMessageBox.Warning