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>
# 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 *
import locale, gettext
import anki.lang
@ -103,13 +103,78 @@ def setupLang(pm, app, force=None):
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):
from anki.hooks import runHook
if evt.type() == QEvent.FileOpen:
runHook("macLoadEvent", unicode(evt.file()))
self.emit(SIGNAL("appMsg"), evt.file() or "raise")
return True
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():
global mw
from anki.utils import isWin, isMac
@ -122,15 +187,12 @@ def run():
# create the app
app = AnkiApp(sys.argv)
QCoreApplication.setApplicationName("Anki")
if app.secondInstance():
# we've signaled the primary instance, so we should close
return
# parse args
import optparse
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, args = parseArgs(sys.argv)
opts.base = unicode(opts.base or "", sys.getfilesystemencoding())
opts.profile = unicode(opts.profile or "", sys.getfilesystemencoding())
@ -142,12 +204,11 @@ def run():
setupLang(pm, app, opts.lang)
# remaining pm init
pm.checkPid()
pm.ensureProfile()
# load the main window
import aqt.main
mw = aqt.main.AnkiQt(app, pm)
mw = aqt.main.AnkiQt(app, pm, args)
app.exec_()
if __name__ == "__main__":

View file

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

View file

@ -21,8 +21,9 @@ from aqt.utils import saveGeom, restoreGeom, showInfo, showWarning, \
tooltip, openHelp, openLink
class AnkiQt(QMainWindow):
def __init__(self, app, profileManager):
def __init__(self, app, profileManager, args):
QMainWindow.__init__(self)
self.state = "startup"
aqt.mw = self
self.app = app
self.pm = profileManager
@ -43,21 +44,23 @@ class AnkiQt(QMainWindow):
except:
showInfo(_("Error during startup:\n%s") % traceback.format_exc())
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
# close on profile load error.
self.progress.timer(10, self.setupProfile, False)
def setupUI(self):
self.col = None
self.state = "overview"
self.hideSchemaMsg = False
self.setupAppMsg()
self.setupKeys()
self.setupThreads()
self.setupFonts()
self.setupMainWindow()
self.setupSystemSpecific()
self.setupStyle()
self.setupProxy()
self.setupMenus()
self.setupProgress()
self.setupErrorHandler()
@ -75,6 +78,7 @@ class AnkiQt(QMainWindow):
##########################################################################
def setupProfile(self):
self.pendingImport = None
# profile not provided on command line?
if not self.pm.name:
# if there's a single profile, load it automatically
@ -91,6 +95,7 @@ class AnkiQt(QMainWindow):
self.loadProfile()
def showProfileManager(self):
self.state = "profileManager"
d = self.profileDiag = QDialog()
f = self.profileForm = aqt.forms.profiles.Ui_Dialog()
f.setupUi(d)
@ -210,6 +215,15 @@ Are you sure?""")):
self.raise_()
# maybe sync (will load DB)
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")
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."
self.autosave()
self.resetModal = modal
if self.state in ("overview", "review", "deckBrowser"):
if self.interactiveState():
self.moveToState("resetRequired")
def interactiveState(self):
"True if not in profile manager, syncing, etc."
return self.state in ("overview", "review", "deckBrowser")
def maybeReset(self):
self.autosave()
if self.state == "resetRequired":
@ -995,12 +1013,8 @@ will be lost. Continue?"""))
ws.setFontSize(QWebSettings.DefaultFontSize, self.fontHeight)
def setupSystemSpecific(self):
# use system font for webviews
# mac tweaks
addHook("macLoadEvent", self.onMacLoad)
if isMac:
qt_mac_set_menubar_icons(False)
#self.setUnifiedTitleAndToolBarOnMac(self.pm.profile['showToolbar'])
# mac users expect a minimize option
self.minimizeShortcut = QShortcut("Ctrl+M", self)
self.connect(self.minimizeShortcut, SIGNAL("activated()"),
@ -1025,12 +1039,40 @@ will be lost. Continue?"""))
def onMacMinimize(self):
self.setWindowState(self.windowState() | Qt.WindowMinimized)
def onMacLoad(self, fname):
self.loadDeck(fname)
# Proxy support
# Single instance support
##########################################################################
def setupProxy(self):
return
# need to bundle socksipy and install a default socket handler
def setupAppMsg(self):
self.connect(self.app, SIGNAL("appMsg"), self.onAppMsg)
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.""")
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
######################################################################

View file

@ -155,3 +155,7 @@ Your pysqlite2 is too old. Anki will appear frozen during long operations."""
def _unsetBusy(self):
self._disabled = False
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.QtGui import *
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
from PyQt4 import pyqtconfig
def debug():

View file

@ -25,9 +25,9 @@ def showCritical(text, parent=None, help=""):
"Show a small critical error with an OK button."
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."
if not parent:
if parent is False:
parent = aqt.mw.app.activeWindow() or aqt.mw
if type == "warning":
icon = QMessageBox.Warning