diff --git a/bin/rdfdb b/bin/rdfdb --- a/bin/rdfdb +++ b/bin/rdfdb @@ -196,6 +196,7 @@ class WatchedFiles(object): def dirChange(self, watch, path, mask): if mask & IN_CREATE: + log.debug("%s created; consider adding a watch", path) self.watchFile(path.path) def watchFile(self, inFile): @@ -238,8 +239,23 @@ class WatchedFiles(object): gf = GraphFile(self.notifier, inFile, ctx, self.patch, self.getSubgraph) self.graphFiles[ctx] = gf + log.info("%s do initial read", inFile) gf.reread() + def aboutToPatch(self, ctx): + """ + warn us that a patch is about to come to this context. it's more + straightforward to create the new file now + """ + + g = self.getSubgraph(ctx) + + if ctx not in self.graphFiles: + outFile = self.fileForUri(ctx) + log.info("starting new file %r", outFile) + self.graphFiles[ctx] = GraphFile(self.notifier, outFile, ctx, + self.patch, self.getSubgraph) + def dirtyFiles(self, ctxs): """mark dirty the files that we watch in these contexts. @@ -251,12 +267,6 @@ class WatchedFiles(object): """ for ctx in ctxs: g = self.getSubgraph(ctx) - - if ctx not in self.graphFiles: - outFile = self.fileForUri(ctx) - self.graphFiles[ctx] = GraphFile(self.notifier, outFile, ctx, - self.patch, self.getSubgraph) - self.graphFiles[ctx].dirty(g) def uriFromFile(self, filename): @@ -301,6 +311,9 @@ class Db(object): log.info("patching graph %s -%d +%d" % ( ctx, len(p.delQuads), len(p.addQuads))) + if hasattr(self, 'watchedFiles'): # not during startup + self.watchedFiles.aboutToPatch(ctx) + patchQuads(self.graph, p.delQuads, p.addQuads, perfect=True) senderUpdateUri = getattr(p, 'senderUpdateUri', None) diff --git a/bin/subcomposer b/bin/subcomposer --- a/bin/subcomposer +++ b/bin/subcomposer @@ -18,17 +18,17 @@ subcomposer from __future__ import division, nested_scopes import time, logging from optparse import OptionParser -import logging +import logging, urllib import Tkinter as tk import louie as dispatcher from twisted.internet import reactor, tksupport, task -from rdflib import URIRef, RDF +from rdflib import URIRef, RDF, RDFS, Literal from run_local import log log.setLevel(logging.DEBUG) from light9.dmxchanedit import Levelbox -from light9 import dmxclient, Submaster, prof +from light9 import dmxclient, Submaster, prof, showconfig from light9.Patch import get_channel_name from light9.uihelpers import toplevelat from light9.rdfdb.syncedgraph import SyncedGraph @@ -71,7 +71,10 @@ class Subcomposer(tk.Frame): self.graph = graph self.session = session - # this is a URIRef or Local + # this is a URIRef or Local. Strangely, the Local case still + # has a uri, which you can get from + # self.currentSub.uri. Probably that should be on the Local + # object too, or maybe Local should be a subclass of URIRef self._currentChoice = Observable(Local) # this is a PersistentSubmaster (even for local) @@ -82,9 +85,68 @@ class Subcomposer(tk.Frame): log.info("change viewed sub to %s", val) self._currentChoice.subscribe(pc) - EditChoice(self, self.graph, self._currentChoice).frame.pack(side='top') + ec = self.editChoice = EditChoice(self, self.graph, self._currentChoice) + ec.frame.pack(side='top') + + ec.subIcon.bind("", self.clickSubIcon) self.setupSubChoiceLinks() self.setupLevelboxUi() + + def clickSubIcon(self, *args): + box = tk.Toplevel(self.editChoice.frame) + box.wm_transient(self.editChoice.frame) + tk.Label(box, text="Name this sub:").pack() + e = tk.Entry(box) + e.pack() + b = tk.Button(box, text="Make global") + b.pack() + def clicked(*args): + self.makeGlobal(newName=e.get()) + box.destroy() + + b.bind("", clicked) + e.focus() + + def makeGlobal(self, newName): + """promote our local submaster into a non-local, named one""" + uri = self.currentSub().uri + newUri = showconfig.showUri() + ("/sub/%s" % + urllib.quote(newName, safe='')) + with self.graph.currentState( + tripleFilter=(uri, None, None)) as current: + if (uri, RDF.type, L9['LocalSubmaster']) not in current: + raise ValueError("%s is not a local submaster" % uri) + if (newUri, None, None) in current: + raise ValueError("new uri %s is in use" % newUri) + + # the local submaster was storing in ctx=self.session, but now + # we want it to be in ctx=uri + + self.relocateSub(newUri, newName) + + # these are in separate patches for clarity as i'm debugging this + self.graph.patch(Patch(addQuads=[ + (newUri, RDFS.label, Literal(newName), newUri), + ], delQuads=[ + (newUri, RDF.type, L9['LocalSubmaster'], newUri), + ])) + self.graph.patchObject(self.session, + self.session, L9['currentSub'], newUri) + + def relocateSub(self, newUri, newName): + # maybe this goes in Submaster + uri = self.currentSub().uri + + def repl(u): + if u == uri: + return newUri + return u + delQuads = self.currentSub().allQuads() + addQuads = [(repl(s), p, repl(o), newUri) for s,p,o,c in delQuads] + # patch can't span contexts yet + self.graph.patch(Patch(addQuads=addQuads, delQuads=[])) + #self.graph.patch(Patch(addQuads=[], delQuads=delQuads)) + def setupSubChoiceLinks(self): graph = self.graph @@ -133,7 +195,9 @@ class Subcomposer(tk.Frame): self.sendupdate() def makeLocal(self): - # todo: put a type on this, so subChanged can identify it right + """ + change our display to a local submaster + """ # todo: where will these get stored, or are they local to this # subcomposer process and don't use PersistentSubmaster at all? diff --git a/light9/Submaster.py b/light9/Submaster.py --- a/light9/Submaster.py +++ b/light9/Submaster.py @@ -187,6 +187,8 @@ class PersistentSubmaster(Submaster): typeStmt = (self.uri, RDF.type, L9['Submaster']) with self.graph.currentState(tripleFilter=typeStmt) as current: try: + log.debug("submaster's type statement is in %r" % + list(current.contextsForStatement(typeStmt))) ctx = current.contextsForStatement(typeStmt)[0] except IndexError: log.info("declaring %s to be a submaster" % self.uri) @@ -212,6 +214,17 @@ class PersistentSubmaster(Submaster): for lev in levs: self.graph.removeMappingNode(self._saveContext(), lev) + def allQuads(self): + """all the quads for this sub""" + quads = [] + with self.graph.currentState() as current: + quads.extend(current.quads((self.uri, None, None))) + for s,p,o,c in quads: + if p == L9['lightLevel']: + quads.extend(current.quads((o, None, None))) + return quads + + def save(self): raise NotImplementedError("obsolete?") if self.temporary: diff --git a/light9/editchoice.py b/light9/editchoice.py --- a/light9/editchoice.py +++ b/light9/editchoice.py @@ -7,7 +7,7 @@ class Local(object): manage. Set resourceObservable to Local to indicate that you're unlinked""" -class EditChoice(tk.Frame): +class EditChoice(object): """ Observable <-> linker UI diff --git a/light9/rdfdb/graphfile.py b/light9/rdfdb/graphfile.py --- a/light9/rdfdb/graphfile.py +++ b/light9/rdfdb/graphfile.py @@ -20,6 +20,8 @@ class GraphFile(object): self.path, self.uri = path, uri self.patch, self.getSubgraph = patch, getSubgraph + self.lastWriteTimestamp = 0 # mtime from the last time _we_ wrote + if not os.path.exists(path): # can't start notify until file exists try: @@ -28,11 +30,12 @@ class GraphFile(object): pass f = open(path, "w") f.close() - iolog.info("created %s", path) + iolog.info("%s created", path) + self.lastWriteTimestamp = os.path.getmtime(path) + self.flushDelay = 2 # seconds until we have to call flush() when dirty self.writeCall = None # or DelayedCall - self.lastWriteTimestamp = 0 # mtime from the last time _we_ wrote # emacs save comes in as IN_MOVE_SELF, maybe @@ -49,19 +52,25 @@ class GraphFile(object): def notify(self, notifier, filepath, mask): maskNames = humanReadableMask(mask) - if maskNames[0] in ['open', 'access', 'close_nowrite', 'attrib', 'delete_self']: - log.debug("file %s changed, ignoring %s" % (filepath, maskNames)) + if maskNames[0] == 'delete_self': + log.warn("%s delete_self event: need to dump the stmts from " + "this file", filepath) + # this is happening surprisingly often, even to files that + # are still there + return + if maskNames[0] in ['open', 'access', 'close_nowrite', 'attrib']: + log.debug("%s %s event, ignoring" % (filepath, maskNames)) return try: if filepath.getModificationTime() == self.lastWriteTimestamp: - log.debug("file %s changed, but we did this write", filepath) + log.debug("%s changed, but we did this write", filepath) return except OSError as e: - log.error("watched file %s: %r" % (filepath, e)) + log.error("%s: %r" % (filepath, e)) return - log.info("file %s changed (%s)", filepath, maskNames) + log.info("%s needs reread because of %s event", filepath, maskNames) try: self.reread() except Exception: @@ -79,10 +88,10 @@ class GraphFile(object): except SyntaxError as e: print e traceback.print_exc() - log.error("syntax error in %s" % self.path) + log.error("%s syntax error", self.path) return except IOError as e: - log.error("rereading %s: %r" % (self.uri, e)) + log.error("%s rereading %s: %r", self.path, self.uri, e) return old = inContext(old, self.uri) @@ -90,11 +99,12 @@ class GraphFile(object): p = Patch.fromDiff(old, new) if p: + log.debug("%s applying patch for changes in file", self.path) self.patch(p, dueToFileChange=True) def dirty(self, graph): """ - there are new contents for our file + there are new contents to write to our file graph is the rdflib.Graph that contains the contents of the file. It is allowed to change. Note that dirty() will probably @@ -108,7 +118,7 @@ class GraphFile(object): going towards a text file editor """ - log.info("%s dirty, %s stmt" % (self.uri, len(graph))) + log.info("%s dirty, needs write", self.path) self.graphToWrite = graph if self.writeCall: @@ -127,5 +137,5 @@ class GraphFile(object): f.close() self.lastWriteTimestamp = os.path.getmtime(tmpOut) os.rename(tmpOut, self.path) - iolog.info("rewrote %s in %.1f ms", self.path, serializeTime * 1000) + iolog.info("%s rewrote in %.1f ms", self.path, serializeTime * 1000)