[Zope-CVS] CVS: Products/ExternalEditor - CHANGES.txt:1.8 ExternalEditor.py:1.6 zopeedit.py:1.9
Casey Duncan
casey@zope.com
Mon, 10 Jun 2002 01:19:41 -0400
Update of /cvs-repository/Products/ExternalEditor
In directory cvs.zope.org:/tmp/cvs-serv25943
Modified Files:
CHANGES.txt ExternalEditor.py zopeedit.py
Log Message:
Added Windows support. Much refactoring of process control. Fixed cache
headers to work correctly with IE.
=== Products/ExternalEditor/CHANGES.txt 1.7 => 1.8 ===
+ - Now works on Windows (applause) using Pythonwin. Much overall
+ refactoring to abstract process control.
+
+ - Added "temp_dir" configuration option for specifying a different
+ temp file directory then the OS default.
+
- Added support for domain specific configuration options.
- Fixed trailing newline bug in encoded auth data coming from
=== Products/ExternalEditor/ExternalEditor.py 1.5 => 1.6 ===
import Acquisition
from AccessControl.SecurityManagement import getSecurityManager
+from webdav.common import rfc1123_date
import base64
class ExternalEditor(Acquisition.Implicit):
@@ -74,9 +75,6 @@
raise 'BadRequest', 'Object does not support external editing'
RESPONSE.setHeader('Content-Type', 'application/x-zope-edit')
- RESPONSE.setHeader('Cache-Control', 'no-cache')
RESPONSE.setHeader('Pragma', 'no-cache')
return '\n'.join(r)
-
-
=== Products/ExternalEditor/zopeedit.py 1.8 => 1.9 ===
import sys, os, stat
-from time import sleep
from ConfigParser import ConfigParser
from httplib import HTTPConnection, HTTPSConnection
from urlparse import urlparse
+win32 = sys.platform == 'win32'
+
class Configuration:
def __init__(self, path):
@@ -75,11 +76,9 @@
for section in sections:
if self.config.has_section(section):
- print 'found section:', section
for option in self.config.options(section):
opt[option] = self.config.get(section, option)
return opt
-
class ExternalEditor:
@@ -92,7 +91,7 @@
self.config = Configuration(config_path)
# Open the input file and read the metadata headers
- in_f = open(input_file, 'rb')
+ in_f = open(input_file)
metadata = {}
while 1:
@@ -115,14 +114,23 @@
self.host)
# Write the body of the input file to a separate file
- body_file = (self.host + self.path).replace('/', ',')
- body_file = '%s-%s' % (os.tmpnam(), body_file)
+ content_file = (self.host + self.path)\
+ .replace('/', ',').replace(':',',')
+
+ if self.options.has_key('temp_dir'):
+ temp = os.tempnam(self.options['temp_dir'])
+ elif win32:
+ temp = os.tempnam()
+ else:
+ temp = os.tmpnam()
+
+ content_file = '%s-%s' % (temp, content_file)
ext = self.options.get('extension')
- if ext and not body_file.endswith(ext):
- body_file = body_file + ext
- body_f = open(body_file, 'wb')
+ if ext and not content_file.endswith(ext):
+ content_file = content_file + ext
+ body_f = open(content_file, 'wb')
body_f.write(in_f.read())
- self.body_file = body_file
+ self.content_file = content_file
in_f.close()
body_f.close()
self.clean_up = int(self.options.get('cleanup_files', 1))
@@ -132,18 +140,23 @@
# a fatal error occurs, unless explicitly stated otherwise
# in the config file
if getattr(self, 'clean_up', 1):
- os.remove(input_file)
+ try:
+ exc, exc_data = sys.exc_info()[:2]
+ os.remove(input_file)
+ except OSError:
+ # Sometimes we aren't allowed to delete it
+ raise exc, exc_data
raise
def __del__(self):
# for security we always delete the files by default
- if getattr(self, 'clean_up', 1) and hasattr(self, 'body_file'):
- os.remove(self.body_file)
+ if getattr(self, 'clean_up', 1) and hasattr(self, 'content_file'):
+ os.remove(self.content_file)
- def getEditor(self):
+ def getEditorCommand(self):
"""Return the editor command"""
editor = self.options.get('editor')
-
+ """
if not editor and has_tk():
from tkSimpleDialog import askstring
editor = askstring('Zope External Editor',
@@ -151,7 +164,7 @@
if not editor: sys.exit(0)
self.config.set('general', 'editor', path)
self.config.save()
-
+ """
if editor is not None:
return editor
else:
@@ -159,66 +172,54 @@
def launch(self):
"""Launch external editor"""
- editor = self.getEditor().split()
- file = self.body_file
- editor.append(file)
- last_fstat = os.stat(file)
- pid = os.spawnvp(os.P_NOWAIT, editor[0], editor) # Note: Unix only
+ save_interval = float(self.options.get('save_interval'))
use_locks = int(self.options.get('use_locks'))
+ launch_success = 0
+ last_fstat = os.stat(self.content_file)
+ command = '%s %s' % (self.getEditorCommand(), self.content_file)
+ editor = EditorProcess(command)
if use_locks:
self.lock()
- exit_pid = 0
- save_interval = self.config.getfloat('general', 'save_interval')
- success = 0
-
- while exit_pid != pid:
- sleep(save_interval or 2)
-
- try:
- exit_pid, exit_status = os.waitpid(pid, os.WNOHANG)
- if exit_pid != pid: success = 1
- except OSError:
- exit_pid = pid
+ while 1:
+ editor.wait(save_interval or 2)
+ fstat = os.stat(self.content_file)
- fstat = os.stat(file)
- if (exit_pid == pid or save_interval) \
+ if (save_interval or not editor.isAlive()) \
and fstat[stat.ST_MTIME] != last_fstat[stat.ST_MTIME]:
# File was modified
- success = 1 # handle very short editing sessions
+ launch_success = 1 # handle very short editing sessions
self.saved = self.putChanges()
last_fstat = fstat
+
+ if editor.isAlive():
+ launch_success = 1
+ else:
+ break
- if not success:
- fatalError(('Editor "%s" did not launch properly.\n'
- 'External editor lost connection '
- 'to editor process.') % editor[0])
-
+ if not launch_success:
+ fatalError('Editor did not launch properly.\n'
+ 'External editor lost connection '
+ 'to editor process.')
+
if use_locks:
self.unlock()
- if not self.saved and has_tk():
- from tkMessageBox import askyesno
- if askyesno('Zope External Editor',
- 'File not saved to Zope.\nReopen local copy?'):
- has_tk() # ugh, keeps tk happy
- self.launch()
- else:
- self.clean_up = 0 # Keep temp file
- has_tk() # ugh
+ if not self.saved \
+ and askYesNo('File not saved to Zope.\nReopen local copy?'):
+ self.launch()
def putChanges(self):
"""Save changes to the file back to Zope"""
- f = open(self.body_file, 'rb')
+ f = open(self.content_file, 'rb')
body = f.read()
f.close()
headers = {'Content-Type':
self.metadata.get('content_type', 'text/plain')}
if hasattr(self, 'lock_token'):
- headers['If'] = '<%s> (<%s>)' % (self.path,
- self.lock_token)
+ headers['If'] = '<%s> (<%s>)' % (self.path, self.lock_token)
response = self.zope_request('PUT', headers, body)
del body # Don't keep the body around longer then we need to
@@ -228,20 +229,14 @@
sys.stderr.write('Error occurred during HTTP put:\n%d %s\n' \
% (response.status, response.reason))
sys.stderr.write('\n----\n%s\n----\n' % response.read())
-
message = response.getheader('Bobo-Exception-Type')
- if has_tk():
- from tkMessageBox import askretrycancel
- if askretrycancel('Zope External Editor',
- ('Could not save to Zope.\nError '
- 'occurred during HTTP put:\n%d %s\n%s') \
- % (response.status, response.reason,
- message)):
- has_tk() # ugh, keeps tk happy
- self.putChanges()
- else:
- has_tk() # ugh
- return 0
+
+ if askRetryCancel('Could not save to Zope.\nError '
+ 'occurred during HTTP put:\n%d %s\n%s' \
+ % (response.status, response.reason, message)):
+ return self.putChanges()
+ else:
+ return 0
return 1
def lock(self):
@@ -281,16 +276,12 @@
sys.stderr.write('Error occurred during lock request:\n%d %s\n' \
% (response.status, response.reason))
sys.stderr.write('\n----\n%s\n----\n' % response.read())
- if has_tk():
- from tkMessageBox import askretrycancel
- if askretrycancel('Zope External Editor',
- ('Lock request failed:\n%d %s\n%s') \
- % (response.status, response.reason, message)):
- has_tk() # ugh, keeps tk happy
- return self.lock()
- else:
- has_tk() # ugh
- return 0
+
+ if askRetryCancel('Lock request failed:\n%d %s\n%s' \
+ % (response.status, response.reason, message)):
+ return self.lock()
+ else:
+ return 0
return 1
def unlock(self):
@@ -299,25 +290,20 @@
return 0
headers = {'Lock-Token':self.lock_token}
-
response = self.zope_request('UNLOCK', headers)
if response.status / 100 != 2:
# Captain, she's still locked!
message = response.getheader('Bobo-Exception-Type')
- sys.stderr.write('Error occurred during unlock request:\n%d %s\n%s\n' \
+ sys.stderr.write('Error occurred during unlock request:'
+ '\n%d %s\n%s\n' \
% (response.status, response.reason, message))
sys.stderr.write('\n----\n%s\n----\n' % response.read())
- if has_tk():
- from tkMessageBox import askretrycancel
- if askretrycancel('Zope External Editor',
- ('Unlock request failed:\n%d %s') \
- % (response.status, response.reason)):
- has_tk() # ugh, keeps tk happy
- return self.unlock(token)
- else:
- has_tk() # ugh
- return 0
+ if askRetryCancel('Unlock request failed:\n%d %s'
+ % (response.status, response.reason)):
+ return self.unlock(token)
+ else:
+ return 0
return 1
def zope_request(self, method, headers={}, body=''):
@@ -361,33 +347,110 @@
response.status = 0
return response
-def has_tk():
- """Sets up a suitable tk root window if one has not
- already been setup. Returns true if tk is happy,
- false if tk throws an error (like its not available)"""
- if not hasattr(globals(), 'tk_root'):
- # create a hidden root window to make Tk happy
+title = 'Zope External Editor'
+
+## Platform specific declarations ##
+
+if win32:
+ from win32ui import MessageBox
+ from win32process import CreateProcess, GetExitCodeProcess, STARTUPINFO
+ from win32event import WaitForSingleObject
+ import pywintypes
+
+ def errorDialog(message):
+ MessageBox(message, title, 16)
+
+ def askRetryCancel(message):
+ return MessageBox(message, title, 53) == 4
+
+ def askYesNo(message):
+ return MessageBox(message, title, 52) == 6
+
+ class EditorProcess:
+ def __init__(self, command):
+ """Launch editor process"""
+ try:
+ self.handle, ht, pid, tid = CreateProcess(None, command, None,
+ None, 1, 0, None,
+ None, STARTUPINFO())
+ except pywintypes.error, e:
+ fatalError('Error launching editor process\n'
+ '(%s):\n%s' % (command, e[2]))
+ def wait(self, timeout):
+ """Wait for editor to exit or until timeout"""
+ WaitForSingleObject(self.handle, int(timeout * 1000.0))
+
+ def isAlive(self):
+ """Returns true if the editor process is still alive"""
+ return GetExitCodeProcess(self.handle) == 259
+
+else: # Posix platform
+ from time import sleep
+
+ def has_tk():
+ """Sets up a suitable tk root window if one has not
+ already been setup. Returns true if tk is happy,
+ false if tk throws an error (like its not available)"""
+ if not hasattr(globals(), 'tk_root'):
+ # create a hidden root window to make Tk happy
+ try:
+ global tk_root
+ from Tkinter import Tk
+ tk_root = Tk()
+ tk_root.withdraw()
+ return 1
+ except:
+ return 0
+ return 1
+
+ def errorDialog(message):
+ """Error dialog box"""
try:
- global tk_root
- from Tkinter import Tk
- tk_root = Tk()
- tk_root.withdraw()
- return 1
- except:
- return 0
- return 1
-
+ if has_tk():
+ from tkMessageBox import showerror
+ showerror(title, message)
+ has_tk()
+ finally:
+ sys.stderr.write(message)
+
+ def askRetryCancel(message):
+ if has_tk():
+ from tkMessageBox import askretrycancel
+ r = askretrycancel(title, message)
+ has_tk() # ugh, keeps tk happy
+ return r
+
+ def askYesNo(message):
+ if has_tk:
+ from tkMessageBox import askyesno
+ r = askyesno(title, message)
+ has_tk() # must...make...tk...happy
+ return r
+
+ class EditorProcess:
+ def __init__(self, command):
+ """Launch editor process"""
+ command = command.split()
+ self.pid = os.spawnvp(os.P_NOWAIT, command[0], command)
+
+ def wait(self, timeout):
+ """Wait for editor to exit or until timeout"""
+ sleep(timeout)
+
+ def isAlive(self):
+ """Returns true if the editor process is still alive"""
+ try:
+ exit_pid, exit_status = os.waitpid(self.pid, os.WNOHANG)
+ except OSError:
+ return 0
+ else:
+ return exit_pid != self.pid
+
def fatalError(message, exit=1):
"""Show error message and exit"""
- message = 'FATAL ERROR:\n%s\n' % message
- try:
- if has_tk():
- from tkMessageBox import showerror
- showerror('Zope External Editor', message)
- has_tk()
- finally:
- sys.stderr.write(message)
- if exit: sys.exit(0)
+ errorDialog('FATAL ERROR: %s' % message)
+ if exit:
+ sys.exit(0)
def whereIs(filename):
"""Given a filename, returns the full path to it based
@@ -426,7 +489,7 @@
cleanup_files = 1
# Use WebDAV locking to prevent concurrent editing by
-# different users. Disable for single user use for
+# different users. Disable for single user use or for
# better performance
use_locks = 1