From: Andrew Lorimer Date: Fri, 7 May 2021 13:26:00 +0000 (+1000) Subject: remove lint X-Git-Tag: v0.0.1 X-Git-Url: https://git.lorimer.id.au/ppt-control.git/diff_plain/8b3313fa3b36ea31bc2f486b3fa3d98013ccc9ad remove lint --- diff --git a/black.jpg b/black.jpg deleted file mode 100755 index 7b05935..0000000 Binary files a/black.jpg and /dev/null differ diff --git a/http_server_39.py b/http_server_39.py deleted file mode 100755 index def05f4..0000000 --- a/http_server_39.py +++ /dev/null @@ -1,1294 +0,0 @@ -"""HTTP server classes. - -Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see -SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, -and CGIHTTPRequestHandler for CGI scripts. - -It does, however, optionally implement HTTP/1.1 persistent connections, -as of version 0.3. - -Notes on CGIHTTPRequestHandler ------------------------------- - -This class implements GET and POST requests to cgi-bin scripts. - -If the os.fork() function is not present (e.g. on Windows), -subprocess.Popen() is used as a fallback, with slightly altered semantics. - -In all cases, the implementation is intentionally naive -- all -requests are executed synchronously. - -SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL --- it may execute arbitrary Python code or external programs. - -Note that status code 200 is sent prior to execution of a CGI script, so -scripts cannot send other status codes such as 302 (redirect). - -XXX To do: - -- log requests even later (to capture byte count) -- log user-agent header and other interesting goodies -- send error log to separate file -""" - - -# See also: -# -# HTTP Working Group T. Berners-Lee -# INTERNET-DRAFT R. T. Fielding -# H. Frystyk Nielsen -# Expires September 8, 1995 March 8, 1995 -# -# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt -# -# and -# -# Network Working Group R. Fielding -# Request for Comments: 2616 et al -# Obsoletes: 2068 June 1999 -# Category: Standards Track -# -# URL: http://www.faqs.org/rfcs/rfc2616.html - -# Log files -# --------- -# -# Here's a quote from the NCSA httpd docs about log file format. -# -# | The logfile format is as follows. Each line consists of: -# | -# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb -# | -# | host: Either the DNS name or the IP number of the remote client -# | rfc931: Any information returned by identd for this person, -# | - otherwise. -# | authuser: If user sent a userid for authentication, the user name, -# | - otherwise. -# | DD: Day -# | Mon: Month (calendar name) -# | YYYY: Year -# | hh: hour (24-hour format, the machine's timezone) -# | mm: minutes -# | ss: seconds -# | request: The first line of the HTTP request as sent by the client. -# | ddd: the status code returned by the server, - if not available. -# | bbbb: the total number of bytes sent, -# | *not including the HTTP/1.0 header*, - if not available -# | -# | You can determine the name of the file accessed through request. -# -# (Actually, the latter is only true if you know the server configuration -# at the time the request was made!) - -__version__ = "0.6" - -__all__ = [ - "HTTPServer", "ThreadingHTTPServer", "BaseHTTPRequestHandler", - "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler", -] - -import copy -import datetime -import email.utils -import html -import http.client -import io -import mimetypes -import os -import posixpath -import select -import shutil -import socket # For gethostbyaddr() -import socketserver -import sys -import time -import urllib.parse -import contextlib -from functools import partial - -from http import HTTPStatus - - -# Default error message template -DEFAULT_ERROR_MESSAGE = """\ - - - - - Error response - - -

Error response

-

Error code: %(code)d

-

Message: %(message)s.

-

Error code explanation: %(code)s - %(explain)s.

- - -""" - -DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" - -class HTTPServer(socketserver.TCPServer): - - allow_reuse_address = 1 # Seems to make sense in testing environment - - def server_bind(self): - """Override server_bind to store the server name.""" - socketserver.TCPServer.server_bind(self) - host, port = self.server_address[:2] - self.server_name = socket.getfqdn(host) - self.server_port = port - - -class ThreadingHTTPServer(socketserver.ThreadingMixIn, HTTPServer): - daemon_threads = True - - -class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): - - """HTTP request handler base class. - - The following explanation of HTTP serves to guide you through the - code as well as to expose any misunderstandings I may have about - HTTP (so you don't need to read the code to figure out I'm wrong - :-). - - HTTP (HyperText Transfer Protocol) is an extensible protocol on - top of a reliable stream transport (e.g. TCP/IP). The protocol - recognizes three parts to a request: - - 1. One line identifying the request type and path - 2. An optional set of RFC-822-style headers - 3. An optional data part - - The headers and data are separated by a blank line. - - The first line of the request has the form - - - - where is a (case-sensitive) keyword such as GET or POST, - is a string containing path information for the request, - and should be the string "HTTP/1.0" or "HTTP/1.1". - is encoded using the URL encoding scheme (using %xx to signify - the ASCII character with hex code xx). - - The specification specifies that lines are separated by CRLF but - for compatibility with the widest range of clients recommends - servers also handle LF. Similarly, whitespace in the request line - is treated sensibly (allowing multiple spaces between components - and allowing trailing whitespace). - - Similarly, for output, lines ought to be separated by CRLF pairs - but most clients grok LF characters just fine. - - If the first line of the request has the form - - - - (i.e. is left out) then this is assumed to be an HTTP - 0.9 request; this form has no optional headers and data part and - the reply consists of just the data. - - The reply form of the HTTP 1.x protocol again has three parts: - - 1. One line giving the response code - 2. An optional set of RFC-822-style headers - 3. The data - - Again, the headers and data are separated by a blank line. - - The response code line has the form - - - - where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), - is a 3-digit response code indicating success or - failure of the request, and is an optional - human-readable string explaining what the response code means. - - This server parses the request and the headers, and then calls a - function specific to the request type (). Specifically, - a request SPAM will be handled by a method do_SPAM(). If no - such method exists the server sends an error response to the - client. If it exists, it is called with no arguments: - - do_SPAM() - - Note that the request name is case sensitive (i.e. SPAM and spam - are different requests). - - The various request details are stored in instance variables: - - - client_address is the client IP address in the form (host, - port); - - - command, path and version are the broken-down request line; - - - headers is an instance of email.message.Message (or a derived - class) containing the header information; - - - rfile is a file object open for reading positioned at the - start of the optional input data part; - - - wfile is a file object open for writing. - - IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! - - The first thing to be written must be the response line. Then - follow 0 or more header lines, then a blank line, and then the - actual data (if any). The meaning of the header lines depends on - the command executed by the server; in most cases, when data is - returned, there should be at least one header line of the form - - Content-type: / - - where and should be registered MIME types, - e.g. "text/html" or "text/plain". - - """ - - # The Python system version, truncated to its first component. - sys_version = "Python/" + sys.version.split()[0] - - # The server software version. You may want to override this. - # The format is multiple whitespace-separated strings, - # where each string is of the form name[/version]. - server_version = "BaseHTTP/" + __version__ - - error_message_format = DEFAULT_ERROR_MESSAGE - error_content_type = DEFAULT_ERROR_CONTENT_TYPE - - # The default request version. This only affects responses up until - # the point where the request line is parsed, so it mainly decides what - # the client gets back when sending a malformed request line. - # Most web servers default to HTTP 0.9, i.e. don't send a status line. - default_request_version = "HTTP/0.9" - - def parse_request(self): - """Parse a request (internal). - - The request should be stored in self.raw_requestline; the results - are in self.command, self.path, self.request_version and - self.headers. - - Return True for success, False for failure; on failure, any relevant - error response has already been sent back. - - """ - self.command = None # set in case of error on the first line - self.request_version = version = self.default_request_version - self.close_connection = True - requestline = str(self.raw_requestline, 'iso-8859-1') - requestline = requestline.rstrip('\r\n') - self.requestline = requestline - words = requestline.split() - if len(words) == 0: - return False - - if len(words) >= 3: # Enough to determine protocol version - version = words[-1] - try: - if not version.startswith('HTTP/'): - raise ValueError - base_version_number = version.split('/', 1)[1] - version_number = base_version_number.split(".") - # RFC 2145 section 3.1 says there can be only one "." and - # - major and minor numbers MUST be treated as - # separate integers; - # - HTTP/2.4 is a lower version than HTTP/2.13, which in - # turn is lower than HTTP/12.3; - # - Leading zeros MUST be ignored by recipients. - if len(version_number) != 2: - raise ValueError - version_number = int(version_number[0]), int(version_number[1]) - except (ValueError, IndexError): - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad request version (%r)" % version) - return False - if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": - self.close_connection = False - if version_number >= (2, 0): - self.send_error( - HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, - "Invalid HTTP version (%s)" % base_version_number) - return False - self.request_version = version - - if not 2 <= len(words) <= 3: - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad request syntax (%r)" % requestline) - return False - command, path = words[:2] - if len(words) == 2: - self.close_connection = True - if command != 'GET': - self.send_error( - HTTPStatus.BAD_REQUEST, - "Bad HTTP/0.9 request type (%r)" % command) - return False - self.command, self.path = command, path - - # Examine the headers and look for a Connection directive. - try: - self.headers = http.client.parse_headers(self.rfile, - _class=self.MessageClass) - except http.client.LineTooLong as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Line too long", - str(err)) - return False - except http.client.HTTPException as err: - self.send_error( - HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, - "Too many headers", - str(err) - ) - return False - - conntype = self.headers.get('Connection', "") - if conntype.lower() == 'close': - self.close_connection = True - elif (conntype.lower() == 'keep-alive' and - self.protocol_version >= "HTTP/1.1"): - self.close_connection = False - # Examine the headers and look for an Expect directive - expect = self.headers.get('Expect', "") - if (expect.lower() == "100-continue" and - self.protocol_version >= "HTTP/1.1" and - self.request_version >= "HTTP/1.1"): - if not self.handle_expect_100(): - return False - return True - - def handle_expect_100(self): - """Decide what to do with an "Expect: 100-continue" header. - - If the client is expecting a 100 Continue response, we must - respond with either a 100 Continue or a final response before - waiting for the request body. The default is to always respond - with a 100 Continue. You can behave differently (for example, - reject unauthorized requests) by overriding this method. - - This method should either return True (possibly after sending - a 100 Continue response) or send an error response and return - False. - - """ - self.send_response_only(HTTPStatus.CONTINUE) - self.end_headers() - return True - - def handle_one_request(self): - """Handle a single HTTP request. - - You normally don't need to override this method; see the class - __doc__ string for information on how to handle specific HTTP - commands such as GET and POST. - - """ - try: - self.raw_requestline = self.rfile.readline(65537) - if len(self.raw_requestline) > 65536: - self.requestline = '' - self.request_version = '' - self.command = '' - self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) - return - if not self.raw_requestline: - self.close_connection = True - return - if not self.parse_request(): - # An error code has been sent, just exit - return - mname = 'do_' + self.command - if not hasattr(self, mname): - self.send_error( - HTTPStatus.NOT_IMPLEMENTED, - "Unsupported method (%r)" % self.command) - return - method = getattr(self, mname) - method() - self.wfile.flush() #actually send the response if not already done. - except socket.timeout as e: - #a read or a write timed out. Discard this connection - self.log_error("Request timed out: %r", e) - self.close_connection = True - return - - def handle(self): - """Handle multiple requests if necessary.""" - self.close_connection = True - - self.handle_one_request() - while not self.close_connection: - self.handle_one_request() - - def send_error(self, code, message=None, explain=None): - """Send and log an error reply. - - Arguments are - * code: an HTTP error code - 3 digits - * message: a simple optional 1 line reason phrase. - *( HTAB / SP / VCHAR / %x80-FF ) - defaults to short entry matching the response code - * explain: a detailed message defaults to the long entry - matching the response code. - - This sends an error response (so it must be called before any - output has been generated), logs the error, and finally sends - a piece of HTML explaining the error to the user. - - """ - - try: - shortmsg, longmsg = self.responses[code] - except KeyError: - shortmsg, longmsg = '???', '???' - if message is None: - message = shortmsg - if explain is None: - explain = longmsg - self.log_error("code %d, message %s", code, message) - self.send_response(code, message) - self.send_header('Connection', 'close') - - # Message body is omitted for cases described in: - # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) - # - RFC7231: 6.3.6. 205(Reset Content) - body = None - if (code >= 200 and - code not in (HTTPStatus.NO_CONTENT, - HTTPStatus.RESET_CONTENT, - HTTPStatus.NOT_MODIFIED)): - # HTML encode to prevent Cross Site Scripting attacks - # (see bug #1100201) - content = (self.error_message_format % { - 'code': code, - 'message': html.escape(message, quote=False), - 'explain': html.escape(explain, quote=False) - }) - body = content.encode('UTF-8', 'replace') - self.send_header("Content-Type", self.error_content_type) - self.send_header('Content-Length', str(len(body))) - self.end_headers() - - if self.command != 'HEAD' and body: - self.wfile.write(body) - - def send_response(self, code, message=None): - """Add the response header to the headers buffer and log the - response code. - - Also send two standard headers with the server software - version and the current date. - - """ - self.log_request(code) - self.send_response_only(code, message) - self.send_header('Server', self.version_string()) - self.send_header('Date', self.date_time_string()) - - def send_response_only(self, code, message=None): - """Send the response header only.""" - if self.request_version != 'HTTP/0.9': - if message is None: - if code in self.responses: - message = self.responses[code][0] - else: - message = '' - if not hasattr(self, '_headers_buffer'): - self._headers_buffer = [] - self._headers_buffer.append(("%s %d %s\r\n" % - (self.protocol_version, code, message)).encode( - 'latin-1', 'strict')) - - def send_header(self, keyword, value): - """Send a MIME header to the headers buffer.""" - if self.request_version != 'HTTP/0.9': - if not hasattr(self, '_headers_buffer'): - self._headers_buffer = [] - self._headers_buffer.append( - ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) - - if keyword.lower() == 'connection': - if value.lower() == 'close': - self.close_connection = True - elif value.lower() == 'keep-alive': - self.close_connection = False - - def end_headers(self): - """Send the blank line ending the MIME headers.""" - if self.request_version != 'HTTP/0.9': - self._headers_buffer.append(b"\r\n") - self.flush_headers() - - def flush_headers(self): - if hasattr(self, '_headers_buffer'): - self.wfile.write(b"".join(self._headers_buffer)) - self._headers_buffer = [] - - def log_request(self, code='-', size='-'): - """Log an accepted request. - - This is called by send_response(). - - """ - if isinstance(code, HTTPStatus): - code = code.value - self.log_message('"%s" %s %s', - self.requestline, str(code), str(size)) - - def log_error(self, format, *args): - """Log an error. - - This is called when a request cannot be fulfilled. By - default it passes the message on to log_message(). - - Arguments are the same as for log_message(). - - XXX This should go to the separate error log. - - """ - - self.log_message(format, *args) - - def log_message(self, format, *args): - """Log an arbitrary message. - - This is used by all other logging functions. Override - it if you have specific logging wishes. - - The first argument, FORMAT, is a format string for the - message to be logged. If the format string contains - any % escapes requiring parameters, they should be - specified as subsequent arguments (it's just like - printf!). - - The client ip and current date/time are prefixed to - every message. - - """ - - sys.stderr.write("%s - - [%s] %s\n" % - (self.address_string(), - self.log_date_time_string(), - format%args)) - - def version_string(self): - """Return the server software version string.""" - return self.server_version + ' ' + self.sys_version - - def date_time_string(self, timestamp=None): - """Return the current date and time formatted for a message header.""" - if timestamp is None: - timestamp = time.time() - return email.utils.formatdate(timestamp, usegmt=True) - - def log_date_time_string(self): - """Return the current time formatted for logging.""" - now = time.time() - year, month, day, hh, mm, ss, x, y, z = time.localtime(now) - s = "%02d/%3s/%04d %02d:%02d:%02d" % ( - day, self.monthname[month], year, hh, mm, ss) - return s - - weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] - - monthname = [None, - 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - - def address_string(self): - """Return the client address.""" - - return self.client_address[0] - - # Essentially static class variables - - # The version of the HTTP protocol we support. - # Set this to HTTP/1.1 to enable automatic keepalive - protocol_version = "HTTP/1.0" - - # MessageClass used to parse headers - MessageClass = http.client.HTTPMessage - - # hack to maintain backwards compatibility - responses = { - v: (v.phrase, v.description) - for v in HTTPStatus.__members__.values() - } - - -class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): - - """Simple HTTP request handler with GET and HEAD commands. - - This serves files from the current directory and any of its - subdirectories. The MIME type for files is determined by - calling the .guess_type() method. - - The GET and HEAD requests are identical except that the HEAD - request omits the actual contents of the file. - - """ - - server_version = "SimpleHTTP/" + __version__ - extensions_map = _encodings_map_default = { - '.gz': 'application/gzip', - '.Z': 'application/octet-stream', - '.bz2': 'application/x-bzip2', - '.xz': 'application/x-xz', - } - - def __init__(self, *args, directory=None, **kwargs): - if directory is None: - directory = os.getcwd() - self.directory = os.fspath(directory) - super().__init__(*args, **kwargs) - - def do_GET(self): - """Serve a GET request.""" - f = self.send_head() - if f: - try: - self.copyfile(f, self.wfile) - finally: - f.close() - - def do_HEAD(self): - """Serve a HEAD request.""" - f = self.send_head() - if f: - f.close() - - def send_head(self): - """Common code for GET and HEAD commands. - - This sends the response code and MIME headers. - - Return value is either a file object (which has to be copied - to the outputfile by the caller unless the command was HEAD, - and must be closed by the caller under all circumstances), or - None, in which case the caller has nothing further to do. - - """ - path = self.translate_path(self.path) - f = None - if os.path.isdir(path): - parts = urllib.parse.urlsplit(self.path) - if not parts.path.endswith('/'): - # redirect browser - doing basically what apache does - self.send_response(HTTPStatus.MOVED_PERMANENTLY) - new_parts = (parts[0], parts[1], parts[2] + '/', - parts[3], parts[4]) - new_url = urllib.parse.urlunsplit(new_parts) - self.send_header("Location", new_url) - self.end_headers() - return None - for index in "index.html", "index.htm": - index = os.path.join(path, index) - if os.path.exists(index): - path = index - break - else: - return self.list_directory(path) - ctype = self.guess_type(path) - # check for trailing "/" which should return 404. See Issue17324 - # The test for this was added in test_httpserver.py - # However, some OS platforms accept a trailingSlash as a filename - # See discussion on python-dev and Issue34711 regarding - # parseing and rejection of filenames with a trailing slash - if path.endswith("/"): - self.send_error(HTTPStatus.NOT_FOUND, "File not found") - return None - try: - f = open(path, 'rb') - except OSError: - self.send_error(HTTPStatus.NOT_FOUND, "File not found") - return None - - try: - fs = os.fstat(f.fileno()) - # Use browser cache if possible - if ("If-Modified-Since" in self.headers - and "If-None-Match" not in self.headers): - # compare If-Modified-Since and time of last file modification - try: - ims = email.utils.parsedate_to_datetime( - self.headers["If-Modified-Since"]) - except (TypeError, IndexError, OverflowError, ValueError): - # ignore ill-formed values - pass - else: - if ims.tzinfo is None: - # obsolete format with no timezone, cf. - # https://tools.ietf.org/html/rfc7231#section-7.1.1.1 - ims = ims.replace(tzinfo=datetime.timezone.utc) - if ims.tzinfo is datetime.timezone.utc: - # compare to UTC datetime of last modification - last_modif = datetime.datetime.fromtimestamp( - fs.st_mtime, datetime.timezone.utc) - # remove microseconds, like in If-Modified-Since - last_modif = last_modif.replace(microsecond=0) - - if last_modif <= ims: - self.send_response(HTTPStatus.NOT_MODIFIED) - self.end_headers() - f.close() - return None - - self.send_response(HTTPStatus.OK) - self.send_header("Content-type", ctype) - self.send_header("Content-Length", str(fs[6])) - self.send_header("Last-Modified", - self.date_time_string(fs.st_mtime)) - self.end_headers() - return f - except: - f.close() - raise - - def list_directory(self, path): - """Helper to produce a directory listing (absent index.html). - - Return value is either a file object, or None (indicating an - error). In either case, the headers are sent, making the - interface the same as for send_head(). - - """ - try: - list = os.listdir(path) - except OSError: - self.send_error( - HTTPStatus.NOT_FOUND, - "No permission to list directory") - return None - list.sort(key=lambda a: a.lower()) - r = [] - try: - displaypath = urllib.parse.unquote(self.path, - errors='surrogatepass') - except UnicodeDecodeError: - displaypath = urllib.parse.unquote(path) - displaypath = html.escape(displaypath, quote=False) - enc = sys.getfilesystemencoding() - title = 'Directory listing for %s' % displaypath - r.append('') - r.append('\n') - r.append('' % enc) - r.append('%s\n' % title) - r.append('\n

%s

' % title) - r.append('
\n
    ') - for name in list: - fullname = os.path.join(path, name) - displayname = linkname = name - # Append / for directories or @ for symbolic links - if os.path.isdir(fullname): - displayname = name + "/" - linkname = name + "/" - if os.path.islink(fullname): - displayname = name + "@" - # Note: a link to a directory displays with @ and links with / - r.append('
  • %s
  • ' - % (urllib.parse.quote(linkname, - errors='surrogatepass'), - html.escape(displayname, quote=False))) - r.append('
\n
\n\n\n') - encoded = '\n'.join(r).encode(enc, 'surrogateescape') - f = io.BytesIO() - f.write(encoded) - f.seek(0) - self.send_response(HTTPStatus.OK) - self.send_header("Content-type", "text/html; charset=%s" % enc) - self.send_header("Content-Length", str(len(encoded))) - self.end_headers() - return f - - def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - - """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - # Don't forget explicit trailing slash when normalizing. Issue17324 - trailing_slash = path.rstrip().endswith('/') - try: - path = urllib.parse.unquote(path, errors='surrogatepass') - except UnicodeDecodeError: - path = urllib.parse.unquote(path) - path = posixpath.normpath(path) - words = path.split('/') - words = filter(None, words) - path = self.directory - for word in words: - if os.path.dirname(word) or word in (os.curdir, os.pardir): - # Ignore components that are not a simple file/directory name - continue - path = os.path.join(path, word) - if trailing_slash: - path += '/' - return path - - def copyfile(self, source, outputfile): - """Copy all data between two file objects. - - The SOURCE argument is a file object open for reading - (or anything with a read() method) and the DESTINATION - argument is a file object open for writing (or - anything with a write() method). - - The only reason for overriding this would be to change - the block size or perhaps to replace newlines by CRLF - -- note however that this the default server uses this - to copy binary data as well. - - """ - shutil.copyfileobj(source, outputfile) - - def guess_type(self, path): - """Guess the type of a file. - - Argument is a PATH (a filename). - - Return value is a string of the form type/subtype, - usable for a MIME Content-type header. - - The default implementation looks the file's extension - up in the table self.extensions_map, using application/octet-stream - as a default; however it would be permissible (if - slow) to look inside the data to make a better guess. - - """ - base, ext = posixpath.splitext(path) - if ext in self.extensions_map: - return self.extensions_map[ext] - ext = ext.lower() - if ext in self.extensions_map: - return self.extensions_map[ext] - guess, _ = mimetypes.guess_type(path) - if guess: - return guess - return 'application/octet-stream' - - -# Utilities for CGIHTTPRequestHandler - -def _url_collapse_path(path): - """ - Given a URL path, remove extra '/'s and '.' path elements and collapse - any '..' references and returns a collapsed path. - - Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. - The utility of this function is limited to is_cgi method and helps - preventing some security attacks. - - Returns: The reconstituted URL, which will always start with a '/'. - - Raises: IndexError if too many '..' occur within the path. - - """ - # Query component should not be involved. - path, _, query = path.partition('?') - path = urllib.parse.unquote(path) - - # Similar to os.path.split(os.path.normpath(path)) but specific to URL - # path semantics rather than local operating system semantics. - path_parts = path.split('/') - head_parts = [] - for part in path_parts[:-1]: - if part == '..': - head_parts.pop() # IndexError if more '..' than prior parts - elif part and part != '.': - head_parts.append( part ) - if path_parts: - tail_part = path_parts.pop() - if tail_part: - if tail_part == '..': - head_parts.pop() - tail_part = '' - elif tail_part == '.': - tail_part = '' - else: - tail_part = '' - - if query: - tail_part = '?'.join((tail_part, query)) - - splitpath = ('/' + '/'.join(head_parts), tail_part) - collapsed_path = "/".join(splitpath) - - return collapsed_path - - - -nobody = None - -def nobody_uid(): - """Internal routine to get nobody's uid""" - global nobody - if nobody: - return nobody - try: - import pwd - except ImportError: - return -1 - try: - nobody = pwd.getpwnam('nobody')[2] - except KeyError: - nobody = 1 + max(x[2] for x in pwd.getpwall()) - return nobody - - -def executable(path): - """Test for executable file.""" - return os.access(path, os.X_OK) - - -class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): - - """Complete HTTP server with GET, HEAD and POST commands. - - GET and HEAD also support running CGI scripts. - - The POST command is *only* implemented for CGI scripts. - - """ - - # Determine platform specifics - have_fork = hasattr(os, 'fork') - - # Make rfile unbuffered -- we need to read one line and then pass - # the rest to a subprocess, so we can't use buffered input. - rbufsize = 0 - - def do_POST(self): - """Serve a POST request. - - This is only implemented for CGI scripts. - - """ - - if self.is_cgi(): - self.run_cgi() - else: - self.send_error( - HTTPStatus.NOT_IMPLEMENTED, - "Can only POST to CGI scripts") - - def send_head(self): - """Version of send_head that support CGI scripts""" - if self.is_cgi(): - return self.run_cgi() - else: - return SimpleHTTPRequestHandler.send_head(self) - - def is_cgi(self): - """Test whether self.path corresponds to a CGI script. - - Returns True and updates the cgi_info attribute to the tuple - (dir, rest) if self.path requires running a CGI script. - Returns False otherwise. - - If any exception is raised, the caller should assume that - self.path was rejected as invalid and act accordingly. - - The default implementation tests whether the normalized url - path begins with one of the strings in self.cgi_directories - (and the next character is a '/' or the end of the string). - - """ - collapsed_path = _url_collapse_path(self.path) - dir_sep = collapsed_path.find('/', 1) - while dir_sep > 0 and not collapsed_path[:dir_sep] in self.cgi_directories: - dir_sep = collapsed_path.find('/', dir_sep+1) - if dir_sep > 0: - head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] - self.cgi_info = head, tail - return True - return False - - - cgi_directories = ['/cgi-bin', '/htbin'] - - def is_executable(self, path): - """Test whether argument path is an executable file.""" - return executable(path) - - def is_python(self, path): - """Test whether argument path is a Python script.""" - head, tail = os.path.splitext(path) - return tail.lower() in (".py", ".pyw") - - def run_cgi(self): - """Execute a CGI script.""" - dir, rest = self.cgi_info - path = dir + '/' + rest - i = path.find('/', len(dir)+1) - while i >= 0: - nextdir = path[:i] - nextrest = path[i+1:] - - scriptdir = self.translate_path(nextdir) - if os.path.isdir(scriptdir): - dir, rest = nextdir, nextrest - i = path.find('/', len(dir)+1) - else: - break - - # find an explicit query string, if present. - rest, _, query = rest.partition('?') - - # dissect the part after the directory name into a script name & - # a possible additional path, to be stored in PATH_INFO. - i = rest.find('/') - if i >= 0: - script, rest = rest[:i], rest[i:] - else: - script, rest = rest, '' - - scriptname = dir + '/' + script - scriptfile = self.translate_path(scriptname) - if not os.path.exists(scriptfile): - self.send_error( - HTTPStatus.NOT_FOUND, - "No such CGI script (%r)" % scriptname) - return - if not os.path.isfile(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not a plain file (%r)" % scriptname) - return - ispy = self.is_python(scriptname) - if self.have_fork or not ispy: - if not self.is_executable(scriptfile): - self.send_error( - HTTPStatus.FORBIDDEN, - "CGI script is not executable (%r)" % scriptname) - return - - # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html - # XXX Much of the following could be prepared ahead of time! - env = copy.deepcopy(os.environ) - env['SERVER_SOFTWARE'] = self.version_string() - env['SERVER_NAME'] = self.server.server_name - env['GATEWAY_INTERFACE'] = 'CGI/1.1' - env['SERVER_PROTOCOL'] = self.protocol_version - env['SERVER_PORT'] = str(self.server.server_port) - env['REQUEST_METHOD'] = self.command - uqrest = urllib.parse.unquote(rest) - env['PATH_INFO'] = uqrest - env['PATH_TRANSLATED'] = self.translate_path(uqrest) - env['SCRIPT_NAME'] = scriptname - if query: - env['QUERY_STRING'] = query - env['REMOTE_ADDR'] = self.client_address[0] - authorization = self.headers.get("authorization") - if authorization: - authorization = authorization.split() - if len(authorization) == 2: - import base64, binascii - env['AUTH_TYPE'] = authorization[0] - if authorization[0].lower() == "basic": - try: - authorization = authorization[1].encode('ascii') - authorization = base64.decodebytes(authorization).\ - decode('ascii') - except (binascii.Error, UnicodeError): - pass - else: - authorization = authorization.split(':') - if len(authorization) == 2: - env['REMOTE_USER'] = authorization[0] - # XXX REMOTE_IDENT - if self.headers.get('content-type') is None: - env['CONTENT_TYPE'] = self.headers.get_content_type() - else: - env['CONTENT_TYPE'] = self.headers['content-type'] - length = self.headers.get('content-length') - if length: - env['CONTENT_LENGTH'] = length - referer = self.headers.get('referer') - if referer: - env['HTTP_REFERER'] = referer - accept = self.headers.get_all('accept', ()) - env['HTTP_ACCEPT'] = ','.join(accept) - ua = self.headers.get('user-agent') - if ua: - env['HTTP_USER_AGENT'] = ua - co = filter(None, self.headers.get_all('cookie', [])) - cookie_str = ', '.join(co) - if cookie_str: - env['HTTP_COOKIE'] = cookie_str - # XXX Other HTTP_* headers - # Since we're setting the env in the parent, provide empty - # values to override previously set values - for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', - 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): - env.setdefault(k, "") - - self.send_response(HTTPStatus.OK, "Script output follows") - self.flush_headers() - - decoded_query = query.replace('+', ' ') - - if self.have_fork: - # Unix -- fork as we should - args = [script] - if '=' not in decoded_query: - args.append(decoded_query) - nobody = nobody_uid() - self.wfile.flush() # Always flush before forking - pid = os.fork() - if pid != 0: - # Parent - pid, sts = os.waitpid(pid, 0) - # throw away additional data [see bug #427345] - while select.select([self.rfile], [], [], 0)[0]: - if not self.rfile.read(1): - break - exitcode = os.waitstatus_to_exitcode(sts) - if exitcode: - self.log_error(f"CGI script exit code {exitcode}") - return - # Child - try: - try: - os.setuid(nobody) - except OSError: - pass - os.dup2(self.rfile.fileno(), 0) - os.dup2(self.wfile.fileno(), 1) - os.execve(scriptfile, args, env) - except: - self.server.handle_error(self.request, self.client_address) - os._exit(127) - - else: - # Non-Unix -- use subprocess - import subprocess - cmdline = [scriptfile] - if self.is_python(scriptfile): - interp = sys.executable - if interp.lower().endswith("w.exe"): - # On Windows, use python.exe, not pythonw.exe - interp = interp[:-5] + interp[-4:] - cmdline = [interp, '-u'] + cmdline - if '=' not in query: - cmdline.append(query) - self.log_message("command: %s", subprocess.list2cmdline(cmdline)) - try: - nbytes = int(length) - except (TypeError, ValueError): - nbytes = 0 - p = subprocess.Popen(cmdline, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env = env - ) - if self.command.lower() == "post" and nbytes > 0: - data = self.rfile.read(nbytes) - else: - data = None - # throw away additional data [see bug #427345] - while select.select([self.rfile._sock], [], [], 0)[0]: - if not self.rfile._sock.recv(1): - break - stdout, stderr = p.communicate(data) - self.wfile.write(stdout) - if stderr: - self.log_error('%s', stderr) - p.stderr.close() - p.stdout.close() - status = p.returncode - if status: - self.log_error("CGI script exit status %#x", status) - else: - self.log_message("CGI script exited OK") - - -def _get_best_family(*address): - infos = socket.getaddrinfo( - *address, - type=socket.SOCK_STREAM, - flags=socket.AI_PASSIVE, - ) - family, type, proto, canonname, sockaddr = next(iter(infos)) - return family, sockaddr - - -def test(HandlerClass=BaseHTTPRequestHandler, - ServerClass=ThreadingHTTPServer, - protocol="HTTP/1.0", port=8000, bind=None): - """Test the HTTP request handler class. - - This runs an HTTP server on port 8000 (or the port argument). - - """ - ServerClass.address_family, addr = _get_best_family(bind, port) - - HandlerClass.protocol_version = protocol - with ServerClass(addr, HandlerClass) as httpd: - host, port = httpd.socket.getsockname()[:2] - url_host = f'[{host}]' if ':' in host else host - print( - f"Serving HTTP on {host} port {port} " - f"(http://{url_host}:{port}/) ..." - ) - try: - httpd.serve_forever() - except KeyboardInterrupt: - print("\nKeyboard interrupt received, exiting.") - sys.exit(0) - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser() - parser.add_argument('--cgi', action='store_true', - help='Run as CGI Server') - parser.add_argument('--bind', '-b', metavar='ADDRESS', - help='Specify alternate bind address ' - '[default: all interfaces]') - parser.add_argument('--directory', '-d', default=os.getcwd(), - help='Specify alternative directory ' - '[default:current directory]') - parser.add_argument('port', action='store', - default=8000, type=int, - nargs='?', - help='Specify alternate port [default: 8000]') - args = parser.parse_args() - if args.cgi: - handler_class = CGIHTTPRequestHandler - else: - handler_class = partial(SimpleHTTPRequestHandler, - directory=args.directory) - - # ensure dual-stack is not disabled; ref #38907 - class DualStackServer(ThreadingHTTPServer): - def server_bind(self): - # suppress exception when protocol is IPv4 - with contextlib.suppress(Exception): - self.socket.setsockopt( - socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) - return super().server_bind() - - test( - HandlerClass=handler_class, - ServerClass=DualStackServer, - port=args.port, - bind=args.bind, - ) diff --git a/icons/first.svg b/icons/first.svg deleted file mode 100644 index 8d0f155..0000000 --- a/icons/first.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/last.svg b/icons/last.svg deleted file mode 100644 index 7064515..0000000 --- a/icons/last.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/left.svg b/icons/left.svg deleted file mode 100644 index acb94c1..0000000 --- a/icons/left.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/icons/ppt.ico b/icons/ppt.ico deleted file mode 100755 index e3fb47b..0000000 Binary files a/icons/ppt.ico and /dev/null differ diff --git a/icons/ppt.png b/icons/ppt.png deleted file mode 100755 index 841b67b..0000000 Binary files a/icons/ppt.png and /dev/null differ diff --git a/icons/right.svg b/icons/right.svg deleted file mode 100644 index a9e5aa7..0000000 --- a/icons/right.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/index.html b/index.html deleted file mode 100755 index b19415f..0000000 --- a/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - ppt-control - - - -
- -
-

Current slide

- -
- -
-

Next slide

- -
- -
- -
-
-

- - - - - Current: /? - - -

- - Show current slide - Show next slide - Keyboard shortcuts - -

Not connected

-
-
- - - - - - diff --git a/ppt-control.js b/ppt-control.js deleted file mode 100644 index e1b8841..0000000 --- a/ppt-control.js +++ /dev/null @@ -1,240 +0,0 @@ -var DEFAULT_TITLE = "ppt-control" -var preloaded = false; -var preload = []; - -function imageRefresh(id) { - img = document.getElementById(id); - var d = new Date; - var http = img.src; - if (http.indexOf("?t=") != -1) { http = http.split("?t=")[0]; } - img.src = http + '?t=' + d.getTime(); -} - -function startWebsocket() { - ws = new WebSocket("ws://" + window.location.host + ":5678/"); - ws.onclose = function(){ - //websocket = null; - setTimeout(function(){startWebsocket()}, 10000); - } - return ws; -} - -var websocket = startWebsocket(); - -var prev = document.querySelector('#prev'), - next = document.querySelector('#next'), - first = document.querySelector('#first'), - last = document.querySelector('#last'), - black = document.querySelector('#black'), - white = document.querySelector('#white'), - slide_label = document.querySelector('#slide_label'), - current = document.querySelector('#current'), - total = document.querySelector('#total'), - users = document.querySelector('.users'), - current_img = document.querySelector('#current_img'), - next_img = document.querySelector('#next_img'), - current_div = document.querySelector('#current_div'), - next_div = document.querySelector('#next_div'), - controls_container = document.querySelector('#controls_container'), - controls_container_inner = document.querySelector('#controls_container_inner'), - show_current = document.querySelector('#show_current'), - show_next = document.querySelector('#show_next'), - shortcuts = document.querySelector('#shortcuts'); - -prev.onclick = function (event) { - websocket.send(JSON.stringify({action: 'prev'})); -} - -next.onclick = function (event) { - websocket.send(JSON.stringify({action: 'next'})); -} - -first.onclick = function (event) { - websocket.send(JSON.stringify({action: 'first'})); -} - -last.onclick = function (event) { - websocket.send(JSON.stringify({action: 'last'})); -} - -black.onclick = function (event) { - websocket.send(JSON.stringify({action: 'black'})); -} - -white.onclick = function (event) { - websocket.send(JSON.stringify({action: 'white'})); -} - -current.onblur = function (event) { - websocket.send(JSON.stringify({action: 'goto', value: current.value})); -} - -current.addEventListener('keyup',function(e){ - if (e.which == 13) this.blur(); -}); - -current_img.onclick = function (event) { - next.click() -} - -next_img.onclick = function (event) { - next.click() -} - - -function sync_current() { - if (show_current.checked) { - current_div.style.display = "block"; - slide_label.style.display = "none"; - next_div.style.width = "25%"; - } else { - current_div.style.display = "none"; - slide_label.style.display = "inline"; - next_div.style.width = "calc(100% - 20px)"; - } - set_control_width(); - saveSettings(); -} -show_current.onclick = sync_current; - -function sync_next() { - if (show_next.checked) { - next_div.style.display = "block"; - current_div.style.width = "70%"; - } else { - next_div.style.display = "none"; - current_div.style.width = "calc(100% - 20px)"; - } - set_control_width(); - saveSettings(); -} -show_next.onclick = sync_next; - -function sync_shortcuts() { - saveSettings(); -} -shortcuts.onclick = sync_shortcuts; - -function set_control_width() { - var width = window.innerWidth - || document.documentElement.clientWidth - || document.body.clientWidth; - if (show_current.checked && show_next.checked && width > 800) { - controls_container_inner.style.width = "70%" - } else { - controls_container_inner.style.width = "100%" - } -} - - -document.addEventListener('keydown', function (e) { - if (shortcuts.checked) { - switch (e.key) { - case "Left": - case "ArrowLeft": - case "Up": - case "ArrowUp": - case "k": - case "K": - prev.click(); - break; - case " ": - case "Spacebar": - case "Enter": - case "Right": - case "ArrowRight": - case "Down": - case "ArrowDown": - case "j": - case "J": - next.click(); - break; - case "b": - case "B": - black.click(); - case "w": - case "W": - white.click(); - default: - return - } - } -}); - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -function disconnect() { - document.title = DEFAULT_TITLE; - current_img.src = "/black.jpg"; - next_img.src = "/black.jpg"; - users.textContent = "Connection to PowerPoint failed"; -} - -websocket.onmessage = function (event) { - console.log("Received data"); - data = JSON.parse(event.data); - switch (data.type) { - case 'state': - if (data.connected == "0" || data.connected == 0) { - console.log("Disconnected"); - disconnect(); - break; - } - var d = new Date; - switch (data.visible) { - case 3: - current_img.src = "/black.jpg"; - break; - case 4: - current_img.src = "/white.jpg"; - break; - default: - //current_img.src = "/cache/" + data.current + ".jpg?t=" + d.getTime(); - current_img.src = "/cache/" + data.current + ".jpg"; - break; - } - if (data.current == data.total + 1) { - //next_img.src = "/cache/" + (data.total + 1) + ".jpg?t=" + d.getTime(); - next_img.src = "/cache/" + (data.total + 1) + ".jpg"; - } else { - //next_img.src = "/cache/" + (data.current + 1) + ".jpg?t=" + d.getTime(); - next_img.src = "/cache/" + (data.current + 1) + ".jpg"; - } - - if (document.activeElement != current) { - current.value = data.current; - } - total.textContent = data.total; - document.title = data.name; - break; - case 'users': - users.textContent = ( - data.count.toString() + " client" + - (data.count == 1 ? "" : "s")); - break; - default: - console.error( - "unsupported event", data); - } - if (preloaded == false && ! isNaN(total.textContent)) { - image = document.getElementById("preload_img"); - for (let i=1; i<=Number(total.textContent); i++) { - image.src = "/cache/" + i + ".jpg"; - preload.push(image); - console.log("Preloaded " + total.textContent); - //sleep(0.5) - } - preloaded = true; - } - -}; - -var interval = setInterval(refresh, 5000); - -function refresh() { - console.log("Refreshing") - websocket.send(JSON.stringify({action: 'refresh'})); -} - diff --git a/ppt_control.py b/ppt_control.py deleted file mode 100755 index 5e8bdfa..0000000 --- a/ppt_control.py +++ /dev/null @@ -1,431 +0,0 @@ -import sys -sys.coinit_flags= 0 -import win32com.client -import pywintypes -import os -import shutil -import http_server_39 as server -#import http.server as server -import socketserver -import threading -import asyncio -import websockets -import logging, json -import urllib -import posixpath -import time -import pythoncom -import pystray -import tkinter as tk -from tkinter import ttk -from PIL import Image, ImageDraw - -logging.basicConfig() - -global STATE -global STATE_DEFAULT -global current_slideshow -current_slideshow = None -CACHEDIR = r'''C:\Windows\Temp\ppt-cache''' -CACHE_TIMEOUT = 2*60*60 - -class Handler(server.SimpleHTTPRequestHandler): - def __init__(self, *args, **kwargs): - super().__init__(*args, directory=os.path.dirname(os.path.realpath(__file__))) - - def translate_path(self, path): - """Translate a /-separated PATH to the local filename syntax. - - Components that mean special things to the local file system - (e.g. drive or directory names) are ignored. (XXX They should - probably be diagnosed.) - - """ - # abandon query parameters - path = path.split('?',1)[0] - path = path.split('#',1)[0] - # Don't forget explicit trailing slash when normalizing. Issue17324 - trailing_slash = path.rstrip().endswith('/') - try: - path = urllib.parse.unquote(path, errors='surrogatepass') - except UnicodeDecodeError: - path = urllib.parse.unquote(path) - path = posixpath.normpath(path) - words = path.split('/') - words = list(filter(None, words)) - if len(words) > 0 and words[0] == "cache": - if current_slideshow: - path = CACHEDIR + "\\" + current_slideshow.name() - else: - path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "black.jpg") + '/' - return path - words.pop(0) - else: - path = self.directory - for word in words: - if os.path.dirname(word) or word in (os.curdir, os.pardir): - # Ignore components that are not a simple file/directory name - continue - path = os.path.join(path, word) - if trailing_slash: - path += '/' - return path - - -def run_http(): - http_server = server.HTTPServer(("", 80), Handler) - http_server.serve_forever() - -STATE_DEFAULT = {"connected": 0, "current": 0, "total": 0, "visible": 0, "name": ""} -STATE = STATE_DEFAULT -USERS = set() - - -def state_event(): - print("Running state event") - return json.dumps({"type": "state", **STATE}) - - -def users_event(): - return json.dumps({"type": "users", "count": len(USERS)}) - - -async def notify_state(): - print("Notifying state to " + str(len(USERS)) + " users") - global STATE - if current_slideshow and STATE["connected"] == 1: - STATE["current"] = current_slideshow.current_slide() - STATE["total"] = current_slideshow.total_slides() - STATE["visible"] = current_slideshow.visible() - STATE["name"] = current_slideshow.name() - else: - STATE = STATE_DEFAULT - if USERS: # asyncio.wait doesn't accept an empty list - message = state_event() - await asyncio.wait([user.send(message) for user in USERS]) - - -async def notify_users(): - if USERS: # asyncio.wait doesn't accept an empty list - message = users_event() - await asyncio.wait([user.send(message) for user in USERS]) - - -async def register(websocket): - USERS.add(websocket) - await notify_users() - - -async def unregister(websocket): - USERS.remove(websocket) - await notify_users() - - -async def ws_handle(websocket, path): - print("Received command") - global current_slideshow - # register(websocket) sends user_event() to websocket - await register(websocket) - try: - await websocket.send(state_event()) - async for message in websocket: - data = json.loads(message) - if data["action"] == "prev": - if current_slideshow: - current_slideshow.prev() - await notify_state() - elif data["action"] == "next": - if current_slideshow: - current_slideshow.next() - await notify_state() - elif data["action"] == "first": - if current_slideshow: - current_slideshow.first() - await notify_state() - elif data["action"] == "last": - if current_slideshow: - current_slideshow.last() - await notify_state() - elif data["action"] == "black": - if current_slideshow: - if current_slideshow.visible() == 3: - current_slideshow.normal() - else: - current_slideshow.black() - await notify_state() - elif data["action"] == "white": - if current_slideshow: - if current_slideshow.visible() == 4: - current_slideshow.normal() - else: - current_slideshow.white() - await notify_state() - elif data["action"] == "goto": - if current_slideshow: - current_slideshow.goto(int(data["value"])) - await notify_state() - elif data["action"] == "refresh": - print("Received refresh command") - await notify_state() - if current_slideshow: - current_slideshow.export_current_next() - current_slideshow.refresh() - else: - logging.error("unsupported event: {}", data) - finally: - await unregister(websocket) - -def run_ws(): - # https://stackoverflow.com/questions/21141217/how-to-launch-win32-applications-in-separate-threads-in-python/22619084#22619084 - # https://www.reddit.com/r/learnpython/comments/mwt4qi/pywintypescom_error_2147417842_the_application/ - pythoncom.CoInitializeEx(pythoncom.COINIT_MULTITHREADED) - asyncio.set_event_loop(asyncio.new_event_loop()) - start_server = websockets.serve(ws_handle, "0.0.0.0", 5678, ping_interval=None) - asyncio.get_event_loop().run_until_complete(start_server) - asyncio.get_event_loop().run_forever() - -def start_server(): - http_daemon = threading.Thread(name="http_daemon", target=run_http) - http_daemon.setDaemon(True) - http_daemon.start() - print("Started HTTP server") - - ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) - ws_daemon.setDaemon(True) - ws_daemon.start() - print("Started websocket server") - -class Slideshow: - def __init__(self, instance): - self.instance = instance - if self.instance is None: - raise ValueError("PPT instance cannot be None") - - if self.instance.SlideShowWindows.Count == 0: - raise ValueError("PPT instance has no slideshow windows") - self.view = self.instance.SlideShowWindows[0].View - - if self.instance.ActivePresentation is None: - raise ValueError("PPT instance has no active presentation") - self.presentation = self.instance.ActivePresentation - - def unload(self): - connect_ppt() - - def refresh(self): - try: - if self.instance is None: - raise ValueError("PPT instance cannot be None") - - if self.instance.SlideShowWindows.Count == 0: - raise ValueError("PPT instance has no slideshow windows") - self.view = self.instance.SlideShowWindows[0].View - - if self.instance.ActivePresentation is None: - raise ValueError("PPT instance has no active presentation") - except: - self.unload() - - def total_slides(self): - try: - self.refresh() - return len(self.presentation.Slides) - except ValueError or pywintypes.com_error: - self.unload() - - def current_slide(self): - try: - self.refresh() - return self.view.CurrentShowPosition - except ValueError or pywintypes.com_error: - self.unload() - - def visible(self): - try: - self.refresh() - return self.view.State - except ValueError or pywintypes.com_error: - self.unload() - - def prev(self): - try: - self.refresh() - self.view.Previous() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def next(self): - try: - self.refresh() - self.view.Next() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def first(self): - try: - self.refresh() - self.view.First() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def last(self): - try: - self.refresh() - self.view.Last() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def goto(self, slide): - try: - self.refresh() - if slide <= self.total_slides(): - self.view.GotoSlide(slide) - else: - self.last() - self.next() - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def black(self): - try: - self.refresh() - self.view.State = 3 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def white(self): - try: - self.refresh() - self.view.State = 4 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def normal(self): - try: - self.refresh() - self.view.State = 1 - self.export_current_next() - except ValueError or pywintypes.com_error: - self.unload() - - def name(self): - try: - self.refresh() - return self.presentation.Name - except ValueError or pywintypes.com_error: - self.unload() - - - def export_current_next(self): - self.export(self.current_slide()) - self.export(self.current_slide() + 1) - - def export(self, slide): - destination = CACHEDIR + "\\" + self.name() + "\\" + str(slide) + ".jpg" - os.makedirs(os.path.dirname(destination), exist_ok=True) - if not os.path.exists(destination) or time.time() - os.path.getmtime(destination) > CACHE_TIMEOUT: - if slide <= self.total_slides(): - attempts = 0 - while attempts < 3: - try: - self.presentation.Slides(slide).Export(destination, "JPG") - break - except: - pass - attempts += 1 - elif slide == self.total_slides() + 1: - shutil.copyfileobj(open(os.path.dirname(os.path.realpath(__file__)) + r'''\black.jpg''', 'rb'), open(destination, 'wb')) - else: - pass - - def export_all(self): - for i in range(1, self.total_slides()): - self.export(i) - -def get_ppt_instance(): - instance = win32com.client.Dispatch('Powerpoint.Application') - if instance is None or instance.SlideShowWindows.Count == 0: - return None - return instance - -def get_current_slideshow(): - return current_slideshow - -def connect_ppt(): - global STATE - if STATE["connected"] == 1: - print("Disconnected from PowerPoint instance") - STATE = STATE_DEFAULT - while True: - try: - instance = get_ppt_instance() - global current_slideshow - current_slideshow = Slideshow(instance) - STATE["connected"] = 1 - STATE["current"] = current_slideshow.current_slide() - STATE["total"] = current_slideshow.total_slides() - print("Connected to PowerPoint instance") - current_slideshow.export_all() - break - except ValueError as e: - current_slideshow = None - pass - time.sleep(1) - -def start(_=None): - #root = tk.Tk() - #root.iconphoto(False, tk.PhotoImage(file="icons/ppt.png")) - #root.geometry("250x150+300+300") - #app = Interface(root) - #interface_thread = threading.Thread(target=root.mainloop()) - #interface_thread.setDaemon(True) - #interface_thread.start() - start_server() - connect_ppt() - - -def null_action(): - pass - -class Interface(ttk.Frame): - - def __init__(self, parent): - ttk.Frame.__init__(self, parent) - - self.parent = parent - - self.initUI() - - def initUI(self): - - self.parent.title("ppt-control") - self.style = ttk.Style() - #self.style.theme_use("default") - - self.pack(fill=tk.BOTH, expand=1) - - quitButton = ttk.Button(self, text="Close", - command=self.quit) - quitButton.place(x=50, y=50) - status_label = ttk.Label(self, text="PowerPoint status: not detected") - status_label.place(x=10,y=10) - - - -def show_icon(): - menu = (pystray.MenuItem("Status", lambda: null_action(), enabled=False), - pystray.MenuItem("Restart", lambda: start()), - pystray.MenuItem("Settings", lambda: open_settings())) - icon = pystray.Icon("ppt-control", Image.open("icons/ppt.ico"), "ppt-control", menu) - icon.visible = True - icon.run(setup=start) - -if __name__ == "__main__": - show_icon() diff --git a/ppt_control_obs.py b/ppt_control_obs.py deleted file mode 100755 index e36957e..0000000 --- a/ppt_control_obs.py +++ /dev/null @@ -1,175 +0,0 @@ -# -*- coding: utf-8 -*- - -import obspython as obs -import asyncio -import websockets -import threading -from time import sleep - -PORT_DEFAULT = 5678 -HOSTNAME_DEFAULT = "localhost" - -hotkey_id_first = None -hotkey_id_prev = None -hotkey_id_next = None -hotkey_id_last = None -hotkey_id_black = None -hotkey_id_white = None - -HOTKEY_NAME_FIRST = 'powerpoint_slides.first' -HOTKEY_NAME_PREV = 'powerpoint_slides.previous' -HOTKEY_NAME_NEXT = 'powerpoint_slides.next' -HOTKEY_NAME_LAST = 'powerpoint_slides.last' -HOTKEY_NAME_BLACK = 'powerpoint_slides.black' -HOTKEY_NAME_WHITE = 'powerpoint_slides.white' - -HOTKEY_DESC_FIRST = 'First PowerPoint slide' -HOTKEY_DESC_PREV = 'Previous PowerPoint slide' -HOTKEY_DESC_NEXT = 'Next PowerPoint slide' -HOTKEY_DESC_LAST = 'Last PowerPoint slide' -HOTKEY_DESC_BLACK = 'Black PowerPoint slide' -HOTKEY_DESC_WHITE = 'White PowerPoint slide' - -global cmd -global attempts -cmd = "" -hostname = HOSTNAME_DEFAULT -port = PORT_DEFAULT -attempts = 0 - -async def communicate(): - async with websockets.connect("ws://%s:%s" % (hostname, port), ping_interval=None) as websocket: - global cmd - global attempts - while True: - if cmd: - try: - await websocket.send('{"action": "%s"}' % cmd) - cmd = "" - except websockets.ConnectionClosed as exc: - attempts += 1 - if attempts == 4: - print("Failed to send command after {} attempts - aborting connection".format(attempts)) - attempts = 0 - cmd = "" - raise websockets.exceptions.ConnectionClosedError(1006, "Sending command failed after {} attempts".format(attempts)) - await asyncio.sleep(0.05 + 0.5*attempts**2) - -def run_ws(): - while True: - try: - asyncio.set_event_loop(asyncio.new_event_loop()) - asyncio.get_event_loop().run_until_complete(communicate()) - except (OSError, websockets.exceptions.ConnectionClosedError): - # No server available - just keep trying - pass - except Exception as e: - print("Failed to connect to websocket: {} - {}".format(type(e), e)) - finally: - sleep(1) - -#------------------------------------------------------------ -# global functions for script plugins - -def script_load(settings): - global hotkey_id_first - global hotkey_id_prev - global hotkey_id_next - global hotkey_id_last - global hotkey_id_black - global hotkey_id_white - - hotkey_id_first = register_and_load_hotkey(settings, HOTKEY_NAME_FIRST, HOTKEY_DESC_FIRST, first_slide) - hotkey_id_prev = register_and_load_hotkey(settings, HOTKEY_NAME_PREV, HOTKEY_DESC_PREV, prev_slide) - hotkey_id_next = register_and_load_hotkey(settings, HOTKEY_NAME_NEXT, HOTKEY_DESC_NEXT, next_slide) - hotkey_id_last = register_and_load_hotkey(settings, HOTKEY_NAME_LAST, HOTKEY_DESC_LAST, last_slide) - hotkey_id_black = register_and_load_hotkey(settings, HOTKEY_NAME_BLACK, HOTKEY_DESC_BLACK, black) - hotkey_id_white = register_and_load_hotkey(settings, HOTKEY_NAME_WHITE, HOTKEY_DESC_WHITE, white) - - ws_daemon = threading.Thread(name="ws_daemon", target=run_ws) - ws_daemon.setDaemon(True) - ws_daemon.start() - print("Started websocket client") - -def script_unload(): - obs.obs_hotkey_unregister(first_slide) - obs.obs_hotkey_unregister(prev_slide) - obs.obs_hotkey_unregister(next_slide) - obs.obs_hotkey_unregister(last_slide) - obs.obs_hotkey_unregister(black) - obs.obs_hotkey_unregister(white) - -def script_save(settings): - save_hotkey(settings, HOTKEY_NAME_FIRST, hotkey_id_first) - save_hotkey(settings, HOTKEY_NAME_PREV, hotkey_id_prev) - save_hotkey(settings, HOTKEY_NAME_NEXT, hotkey_id_next) - save_hotkey(settings, HOTKEY_NAME_LAST, hotkey_id_last) - save_hotkey(settings, HOTKEY_NAME_BLACK, hotkey_id_black) - save_hotkey(settings, HOTKEY_NAME_WHITE, hotkey_id_white) - -def script_description(): - return """ppt-control client - - Provides hotkeys for controlling PowerPoint slides using websockets. - Go to OBS settings -> Hotkeys to change hotkeys (none set by default).""" - -def script_defaults(settings): - obs.obs_data_set_default_string(settings, 'hostname', HOSTNAME_DEFAULT) - obs.obs_data_set_default_int(settings, 'port', PORT_DEFAULT) - -def script_properties(): - props = obs.obs_properties_create() - - obs.obs_properties_add_text(props, "hostname", "Hostname: ", obs.OBS_TEXT_DEFAULT) - obs.obs_properties_add_int(props, "port", "Port: ", 0, 9999, 1) - return props - -def script_update(settings): - global port - port = obs.obs_data_get_int(settings, "port") - hostname = obs.obs_data_get_string(settings, "hostname") - -def register_and_load_hotkey(settings, name, description, callback): - hotkey_id = obs.obs_hotkey_register_frontend(name, description, callback) - hotkey_save_array = obs.obs_data_get_array(settings, name) - obs.obs_hotkey_load(hotkey_id, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - - return hotkey_id - -def save_hotkey(settings, name, hotkey_id): - hotkey_save_array = obs.obs_hotkey_save(hotkey_id) - obs.obs_data_set_array(settings, name, hotkey_save_array) - obs.obs_data_array_release(hotkey_save_array) - -#------------------------------------- - -def first_slide(pressed): - if pressed: - global cmd - cmd = "first" - -def prev_slide(pressed): - if pressed: - global cmd - cmd = "prev" - -def next_slide(pressed): - if pressed: - global cmd - cmd = "next" - -def last_slide(pressed): - if pressed: - global cmd - cmd = "last" - -def black(pressed): - if pressed: - global cmd - cmd = "black" - -def white(pressed): - if pressed: - global cmd - cmd = "white" diff --git a/settings.js b/settings.js deleted file mode 100644 index 901af0b..0000000 --- a/settings.js +++ /dev/null @@ -1,50 +0,0 @@ -const COOKIENAME = "settings"; -const COOKIEEXP = 365; - -function setCookie(cname, cvalue, exdays) { - var d = new Date(); - d.setTime(d.getTime() + (exdays*24*60*60*1000)); - var expires = "expires="+ d.toUTCString(); - document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; -} - -function getCookie(cname) { - var name = cname + "="; - var decodedCookie = decodeURIComponent(document.cookie); - var ca = decodedCookie.split(';'); - for(var i = 0; i