diff --git a/.gitignore b/.gitignore index 7b8da97..a54ed2e 100755 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ IGNORE ORIG SUBMIT doc/_build/ +node_modules +package-lock.json \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..19689db --- /dev/null +++ b/.pylintrc @@ -0,0 +1,570 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Specify a configuration file. +#rcfile= + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# Format style used to check logging format string. `old` means using % +# formatting, while `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it working +# install python-enchant package.. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to indicated private dictionary in +# --spelling-private-dict-file option instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis. It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules=json + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )?<?https?://\S+>?$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether the implicit-str-concat-in-sequence should +# generate a warning on implicit string concatenation in sequences defined over +# several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement. +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.travis.yml b/.travis.yml index 67c4269..3b6764b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,14 @@ language: python -python: - - "3.6" - - "3.5" - - "3.4" - - "2.7" + +matrix: + include: + - python: 3.7 + dist: xenial + sudo: true # Install dependencies install: - - pip install tornado ptyprocess + - pip install tornado ptyprocess python-interface msgpack # command to run tests script: py.test diff --git a/README.rst b/README.rst index e2c1a92..bec2637 100644 --- a/README.rst +++ b/README.rst @@ -16,11 +16,15 @@ Modules: a terminal. * ``terminado.uimodule``: Provides a ``Terminal`` Tornado `UI Module <http://www.tornadoweb.org/en/stable/guide/templates.html#ui-modules>`_. +* ``terminado.formats``: Provides message format implementations for JSON, LightPayload (a custom message format) and + MessagePack JS: * ``terminado/_static/terminado.js``: A lightweight wrapper to set up a term.js terminal with a websocket. +* ``terminado_static/terminad-xtermjs.bundle.js``: An addon for Xterm.js enabling support for terminado supporting all + message formats. Usage example: diff --git a/appveyor.yml b/appveyor.yml index 11e596c..96ec577 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,9 +4,7 @@ skip_branch_with_pr: true # environment variables environment: matrix: - - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python35-x64" - - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python37-x64" build: off @@ -24,6 +22,6 @@ install: # Install dependencies # update path to use installed pip and py.test - set PATH=%PYTHON%\\scripts;%PATH% - - 'pip install tornado pywinpty pytest' + - 'pip install tornado pywinpty pytest python-interface msgpack' test_script: - 'py.test terminado/tests/basic_test.py::CommonTests::test_basic' diff --git a/doc/conf.py b/doc/conf.py index 6f2ae16..81e5c49 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -54,7 +54,7 @@ # built documents. # # The short X.Y version. -version = '0.7' +version = '0.9' # The full version, including alpha/beta/rc tags. release = version diff --git a/doc/releasenotes.rst b/doc/releasenotes.rst index 023d244..1850f49 100644 --- a/doc/releasenotes.rst +++ b/doc/releasenotes.rst @@ -1,6 +1,15 @@ Release notes ============= +0.9 +--- + +- Added support for message formats. The following message formats are supported: JSON, LightPayload (a custom + message format) and MessagePack. The default message format is JSON, which is fully backwards-compatible. The + message format can be switched at runtime. +- Added Xterm.js addon supporting all the message formats supported on the server-side. +- Added a command "switch_format" for switching the message format on the fly. + 0.7 --- diff --git a/doc/requirements.txt b/doc/requirements.txt index 09154a4..02be0a1 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,2 +1,4 @@ ptyprocess tornado +python-interface +msgpack \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..2695f68 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "terminado-xtermjs", + "version": "1.0.0", + "description": "An addon for Xterm.js enabling terminado to be used as backend.", + "keywords": [ + "xtermjs", + "terminado", + "json", + "lightpayload", + "messagepack" + ], + "homepage": "https://github.com/jupyter/terminado", + "bugs": "https://github.com/jupyter/terminado/issues", + "license": "MIT", + "browser": "terminado/_static/terminado-xterm.js", + "repository": { + "type": "git", + "url": "https://github.com/jupyter/terminado.git" + }, + "devDependencies": { + "browserify": "^16.2.3" + }, + "dependencies": { + "messagepack": "^1.1.8" + }, + "files": [ + "terminado/_static/terminado-xtermjs.js", + "terminado/_static/terminado-xtermjs.bundle.js" + ], + "scripts": { + "install": "cd terminado/_static && browserify -r ./terminado-xtermjs -o ./terminado-xtermjs.bundle.js" + } +} diff --git a/pyproject.toml b/pyproject.toml index f24d611..3b1b918 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,10 @@ requires = [ "ptyprocess;os_name!='nt'", "pywinpty (>=0.5);os_name=='nt'", "tornado (>=4)", + "python-interface", + "msgpack" ] -requires-python=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +requires-python=">=3.7" classifiers=[ "Environment :: Web Environment", "License :: OSI Approved :: BSD License", diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..393355d --- /dev/null +++ b/setup.py @@ -0,0 +1,22 @@ +import setuptools + +setuptools.setup( + name="terminado", + version="0.9.2", + author="Jupyter Development Team", + author_email="jupyter@googlegroups.com", + description="A websocket backend for the Xterm.js JavaScript terminal emulator library.", + url="https://github.com/jupyter/terminado", + packages=setuptools.find_packages(exclude=["doc", "demos", "terminado/_static"]), + classifiers=[ + "Programming Language :: Python :: 3.7" + ], + license="MIT", + install_requires=[ + "ptyprocess;os_name!='nt'", + "pywinpty (>=0.5);os_name=='nt'", + "tornado (>=4)", + "python-interface", + "msgpack" + ] +) diff --git a/terminado/__init__.py b/terminado/__init__.py index 643ebfd..48ec12a 100644 --- a/terminado/__init__.py +++ b/terminado/__init__.py @@ -12,4 +12,4 @@ # Prevent a warning about no attached handlers in Python 2 logging.getLogger(__name__).addHandler(logging.NullHandler()) -__version__ = '0.8.2' +__version__ = '0.9.2' diff --git a/terminado/_static/terminado-xtermjs.bundle.js b/terminado/_static/terminado-xtermjs.bundle.js new file mode 100644 index 0000000..9fc6bd7 --- /dev/null +++ b/terminado/_static/terminado-xtermjs.bundle.js @@ -0,0 +1,1059 @@ +require=(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function typeError(tag, expected) { + throw new TypeError(`unexpected tag 0x${tag.toString(16)} (${expected} expected)`); +} + +// positive fixint: 0xxx xxxx +function posFixintTag(i) { + return i & 0x7f; +} +function isPosFixintTag(tag) { + return (tag & 0x80) === 0; +} +function readPosFixint(tag) { + return tag & 0x7f; +} +// negative fixint: 111x xxxx +function negFixintTag(i) { + return 0xe0 | (i & 0x1f); +} +function isNegFixintTag(tag) { + return (tag & 0xe0) == 0xe0; +} +function readNegFixint(tag) { + return tag - 0x100; +} +// fixstr: 101x xxxx +function fixstrTag(length) { + return 0xa0 | (length & 0x1f); +} +function isFixstrTag(tag) { + return (tag & 0xe0) == 0xa0; +} +function readFixstr(tag) { + return tag & 0x1f; +} +// fixarray: 1001 xxxx +function fixarrayTag(length) { + return 0x90 | (length & 0x0f); +} +function isFixarrayTag(tag) { + return (tag & 0xf0) == 0x90; +} +function readFixarray(tag) { + return tag & 0x0f; +} +// fixmap: 1000 xxxx +function fixmapTag(length) { + return 0x80 | (length & 0x0f); +} +function isFixmapTag(tag) { + return (tag & 0xf0) == 0x80; +} +function readFixmap(tag) { + return tag & 0x0f; +} + +function createWriteBuffer() { + let view = new DataView(new ArrayBuffer(64)); + let n = 0; + function need(x) { + if (n + x > view.byteLength) { + const arr = new Uint8Array(Math.max(n + x, view.byteLength + 64)); + arr.set(new Uint8Array(view.buffer.slice(0, n))); + view = new DataView(arr.buffer); + } + } + return { + put(v) { + need(v.byteLength); + (new Uint8Array(view.buffer)).set(new Uint8Array(v), n); + n += v.byteLength; + }, + putI8(v) { + need(1); + view.setInt8(n, v); + ++n; + }, + putI16(v) { + need(2); + view.setInt16(n, v); + n += 2; + }, + putI32(v) { + need(4); + view.setInt32(n, v); + n += 4; + }, + putI64(v) { + need(8); + const neg = v < 0; + if (neg) { + v = -v; + } + let hi = (v / 0x100000000) | 0; + let lo = (v % 0x100000000) | 0; + if (neg) { + // 2s complement + lo = (~lo + 1) | 0; + hi = lo === 0 ? (~hi + 1) | 0 : ~hi; + } + view.setUint32(n, hi); + view.setUint32(n + 4, lo); + n += 8; + }, + putUi8(v) { + need(1); + view.setUint8(n, v); + ++n; + }, + putUi16(v) { + need(2); + view.setUint16(n, v); + n += 2; + }, + putUi32(v) { + need(4); + view.setUint32(n, v); + n += 4; + }, + putUi64(v) { + need(8); + view.setUint32(n, (v / 0x100000000) | 0); + view.setUint32(n + 4, v % 0x100000000); + n += 8; + }, + putF(v) { + need(8); + view.setFloat64(n, v); + n += 8; + }, + ui8array() { + return new Uint8Array(view.buffer.slice(0, n)); + }, + }; +} +function createReadBuffer(buf) { + let view = new DataView(ArrayBuffer.isView(buf) ? buf.buffer : buf); + let n = 0; + return { + peek() { + return view.getUint8(n); + }, + get(len) { + n += len; + return view.buffer.slice(n - len, n); + }, + getI8() { + return view.getInt8(n++); + }, + getI16() { + n += 2; + return view.getInt16(n - 2); + }, + getI32() { + n += 4; + return view.getInt32(n - 4); + }, + getI64() { + n += 8; + const hi = view.getInt32(n - 8); + const lo = view.getUint32(n - 4); + return hi * 0x100000000 + lo; + }, + getUi8() { + return view.getUint8(n++); + }, + getUi16() { + n += 2; + return view.getUint16(n - 2); + }, + getUi32() { + n += 4; + return view.getUint32(n - 4); + }, + getUi64() { + n += 8; + const hi = view.getUint32(n - 8); + const lo = view.getUint32(n - 4); + return hi * 0x100000000 + lo; + }, + getF32() { + n += 4; + return view.getFloat32(n - 4); + }, + getF64() { + n += 8; + return view.getFloat64(n - 8); + }, + }; +} +function putBlob(buf, blob, baseTag) { + const n = blob.byteLength; + if (n <= 255) { + buf.putUi8(baseTag); + buf.putUi8(n); + } + else if (n <= 65535) { + buf.putUi8(baseTag + 1); + buf.putUi16(n); + } + else if (n <= 4294967295) { + buf.putUi8(baseTag + 2); + buf.putUi32(n); + } + else { + throw new RangeError("length limit exceeded"); + } + buf.put(blob); +} +function getBlob(buf) { + const tag = buf.getUi8(); + let n; + switch (tag) { + case 192 /* Nil */: + n = 0; + break; + case 196 /* Bin8 */: + case 217 /* Str8 */: + n = buf.getUi8(); + break; + case 197 /* Bin16 */: + case 218 /* Str16 */: + n = buf.getUi16(); + break; + case 198 /* Bin32 */: + case 219 /* Str32 */: + n = buf.getUi32(); + break; + default: + if (!isFixstrTag(tag)) { + typeError(tag, "bytes or string"); + } + n = readFixstr(tag); + } + return buf.get(n); +} +function putArrHeader(buf, n) { + if (n < 16) { + buf.putUi8(fixarrayTag(n)); + } + else { + putCollectionHeader(buf, 220 /* Array16 */, n); + } +} +function getArrHeader(buf, expect) { + const tag = buf.getUi8(); + const n = isFixarrayTag(tag) + ? readFixarray(tag) + : getCollectionHeader(buf, tag, 220 /* Array16 */, "array"); + if (expect != null && n !== expect) { + throw new Error(`invalid array header size ${n}`); + } + return n; +} +function putMapHeader(buf, n) { + if (n < 16) { + buf.putUi8(fixmapTag(n)); + } + else { + putCollectionHeader(buf, 222 /* Map16 */, n); + } +} +function getMapHeader(buf, expect) { + const tag = buf.getUi8(); + const n = isFixmapTag(tag) + ? readFixmap(tag) + : getCollectionHeader(buf, tag, 222 /* Map16 */, "map"); + if (expect != null && n !== expect) { + throw new Error(`invalid map header size ${n}`); + } + return n; +} +function putCollectionHeader(buf, baseTag, n) { + if (n <= 65535) { + buf.putUi8(baseTag); + buf.putUi16(n); + } + else if (n <= 4294967295) { + buf.putUi8(baseTag + 1); + buf.putUi32(n); + } + else { + throw new RangeError("length limit exceeded"); + } +} +function getCollectionHeader(buf, tag, baseTag, typename) { + switch (tag) { + case 192 /* Nil */: + return 0; + case baseTag: // 16 bit + return buf.getUi16(); + case baseTag + 1: // 32 bit + return buf.getUi32(); + default: + typeError(tag, typename); + } +} + +const Any = { + enc(buf, v) { + typeOf(v).enc(buf, v); + }, + dec(buf) { + return tagType(buf.peek()).dec(buf); + }, +}; +const Nil = { + enc(buf, v) { + buf.putUi8(192 /* Nil */); + }, + dec(buf) { + const tag = buf.getUi8(); + if (tag !== 192 /* Nil */) { + typeError(tag, "nil"); + } + return null; + }, +}; +const Bool = { + enc(buf, v) { + buf.putUi8(v ? 195 /* True */ : 194 /* False */); + }, + dec(buf) { + const tag = buf.getUi8(); + switch (tag) { + case 192 /* Nil */: + case 194 /* False */: + return false; + case 195 /* True */: + return true; + default: + typeError(tag, "bool"); + } + }, +}; +const Int = { + enc(buf, v) { + if (-128 <= v && v <= 127) { + if (v >= 0) { + buf.putUi8(posFixintTag(v)); + } + else if (v > -32) { + buf.putUi8(negFixintTag(v)); + } + else { + buf.putUi8(208 /* Int8 */); + buf.putUi8(v); + } + } + else if (-32768 <= v && v <= 32767) { + buf.putI8(209 /* Int16 */); + buf.putI16(v); + } + else if (-2147483648 <= v && v <= 2147483647) { + buf.putI8(210 /* Int32 */); + buf.putI32(v); + } + else { + buf.putI8(211 /* Int64 */); + buf.putI64(v); + } + }, + dec(buf) { + const tag = buf.getUi8(); + if (isPosFixintTag(tag)) { + return readPosFixint(tag); + } + else if (isNegFixintTag(tag)) { + return readNegFixint(tag); + } + switch (tag) { + case 192 /* Nil */: + return 0; + // signed int types + case 208 /* Int8 */: + return buf.getI8(); + case 209 /* Int16 */: + return buf.getI16(); + case 210 /* Int32 */: + return buf.getI32(); + case 211 /* Int64 */: + return buf.getI64(); + // unsigned int types + case 204 /* Uint8 */: + return buf.getUi8(); + case 205 /* Uint16 */: + return buf.getUi16(); + case 206 /* Uint32 */: + return buf.getUi32(); + case 207 /* Uint64 */: + return buf.getUi64(); + default: + typeError(tag, "int"); + } + }, +}; +const Uint = { + enc(buf, v) { + if (v < 0) { + throw new Error(`not an uint: ${v}`); + } + else if (v <= 127) { + buf.putUi8(posFixintTag(v)); + } + else if (v <= 255) { + buf.putUi8(204 /* Uint8 */); + buf.putUi8(v); + } + else if (v <= 65535) { + buf.putUi8(205 /* Uint16 */); + buf.putUi16(v); + } + else if (v <= 4294967295) { + buf.putUi8(206 /* Uint32 */); + buf.putUi32(v); + } + else { + buf.putUi8(207 /* Uint64 */); + buf.putUi64(v); + } + }, + dec(buf) { + const v = Int.dec(buf); + if (v < 0) { + throw new RangeError("uint underflow"); + } + return v; + }, +}; +const Float = { + enc(buf, v) { + buf.putUi8(203 /* Float64 */); + buf.putF(v); + }, + dec(buf) { + const tag = buf.getUi8(); + switch (tag) { + case 192 /* Nil */: + return 0; + case 202 /* Float32 */: + return buf.getF32(); + case 203 /* Float64 */: + return buf.getF64(); + default: + typeError(tag, "float"); + } + }, +}; +const Bytes = { + enc(buf, v) { + putBlob(buf, v, 196 /* Bin8 */); + }, + dec: getBlob, +}; +const Str = { + enc(buf, v) { + const utf8 = toUTF8(v); + if (utf8.byteLength < 32) { + buf.putUi8(fixstrTag(utf8.byteLength)); + buf.put(utf8); + } + else { + putBlob(buf, utf8, 217 /* Str8 */); + } + }, + dec(buf) { + return fromUTF8(getBlob(buf)); + }, +}; +const Time = { + enc(buf, v) { + const ms = v.getTime(); + buf.putUi8(199 /* Ext8 */); + buf.putUi8(12); + buf.putI8(-1); + buf.putUi32((ms % 1000) * 1000000); + buf.putI64(ms / 1000); + }, + dec(buf) { + const tag = buf.getUi8(); + switch (tag) { + case 214 /* FixExt4 */: // 32-bit seconds + if (buf.getI8() === -1) { + return new Date(buf.getUi32() * 1000); + } + break; + case 215 /* FixExt8 */: // 34-bit seconds + 30-bit nanoseconds + if (buf.getI8() === -1) { + const lo = buf.getUi32(); + const hi = buf.getUi32(); + // seconds: hi + (lo&0x3)*0x100000000 + // nanoseconds: lo>>2 == lo/4 + return new Date((hi + (lo & 0x3) * 0x100000000) * 1000 + lo / 4000000); + } + break; + case 199 /* Ext8 */: // 64-bit seconds + 32-bit nanoseconds + if (buf.getUi8() === 12 && buf.getI8() === -1) { + const ns = buf.getUi32(); + const s = buf.getI64(); + return new Date(s * 1000 + ns / 1000000); + } + break; + } + typeError(tag, "time"); + }, +}; +const Arr = TypedArr(Any); +const Map = TypedMap(Any, Any); +function TypedArr(valueT) { + return { + encHeader: putArrHeader, + decHeader: getArrHeader, + enc(buf, v) { + putArrHeader(buf, v.length); + v.forEach(x => valueT.enc(buf, x)); + }, + dec(buf) { + const res = []; + for (let n = getArrHeader(buf); n > 0; --n) { + res.push(valueT.dec(buf)); + } + return res; + }, + }; +} +function TypedMap(keyT, valueT) { + return { + encHeader: putMapHeader, + decHeader: getMapHeader, + enc(buf, v) { + const props = Object.keys(v); + putMapHeader(buf, props.length); + props.forEach(p => { + keyT.enc(buf, p); + valueT.enc(buf, v[p]); + }); + }, + dec(buf) { + const res = {}; + for (let n = getMapHeader(buf); n > 0; --n) { + const k = keyT.dec(buf); + res[k] = valueT.dec(buf); + } + return res; + }, + }; +} +function structEncoder(fields) { + const ordinals = Object.keys(fields); + return (buf, v) => { + putMapHeader(buf, ordinals.length); + ordinals.forEach(ord => { + const f = fields[ord]; + Int.enc(buf, Number(ord)); + f[1].enc(buf, v[f[0]]); + }); + }; +} +function structDecoder(fields) { + return (buf) => { + const res = {}; + for (let n = getMapHeader(buf); n > 0; --n) { + const f = fields[Int.dec(buf)]; + if (f) { + res[f[0]] = f[1].dec(buf); + } + else { + Any.dec(buf); + } + } + return res; + }; +} +function Struct(fields) { + return { + enc: structEncoder(fields), + dec: structDecoder(fields), + }; +} +function unionEncoder(branches) { + return (buf, v) => { + putArrHeader(buf, 2); + const ord = branches.ordinalOf(v); + Int.enc(buf, ord); + branches[ord].enc(buf, v); + }; +} +function unionDecoder(branches) { + return (buf) => { + getArrHeader(buf, 2); + const t = branches[Int.dec(buf)]; + if (!t) { + throw new TypeError("invalid union type"); + } + return t.dec(buf); + }; +} +function Union(branches) { + return { + enc: unionEncoder(branches), + dec: unionDecoder(branches), + }; +} +function toUTF8(v) { + const n = v.length; + const bin = new Uint8Array(4 * n); + let pos = 0, i = 0, c; + while (i < n) { + c = v.charCodeAt(i++); + if ((c & 0xfc00) === 0xd800) { + c = (c << 10) + v.charCodeAt(i++) - 0x35fdc00; + } + if (c < 0x80) { + bin[pos++] = c; + } + else if (c < 0x800) { + bin[pos++] = 0xc0 + (c >> 6); + bin[pos++] = 0x80 + (c & 0x3f); + } + else if (c < 0x10000) { + bin[pos++] = 0xe0 + (c >> 12); + bin[pos++] = 0x80 + ((c >> 6) & 0x3f); + bin[pos++] = 0x80 + (c & 0x3f); + } + else { + bin[pos++] = 0xf0 + (c >> 18); + bin[pos++] = 0x80 + ((c >> 12) & 0x3f); + bin[pos++] = 0x80 + ((c >> 6) & 0x3f); + bin[pos++] = 0x80 + (c & 0x3f); + } + } + return bin.buffer.slice(0, pos); +} +function fromUTF8(buf) { + const bin = new Uint8Array(buf); + let n, c, codepoints = []; + for (let i = 0; i < bin.length;) { + c = bin[i++]; + n = 0; + switch (c & 0xf0) { + case 0xf0: + n = 3; + break; + case 0xe0: + n = 2; + break; + case 0xd0: + case 0xc0: + n = 1; + break; + } + if (n !== 0) { + c &= (1 << (6 - n)) - 1; + for (let k = 0; k < n; ++k) { + c = (c << 6) + (bin[i++] & 0x3f); + } + } + codepoints.push(c); + } + return String.fromCodePoint.apply(null, codepoints); +} +function typeOf(v) { + switch (typeof v) { + case "undefined": + return Nil; + case "boolean": + return Bool; + case "number": + return !isFinite(v) || Math.floor(v) !== v ? Float + : v < 0 ? Int + : Uint; + case "string": + return Str; + case "object": + return v === null ? Nil + : Array.isArray(v) ? Arr + : v instanceof Uint8Array || v instanceof ArrayBuffer ? Bytes + : v instanceof Date ? Time + : Map; + default: + throw new TypeError(`unsupported type ${typeof v}`); + } +} +function tagType(tag) { + switch (tag) { + case 192 /* Nil */: + return Nil; + case 194 /* False */: + case 195 /* True */: + return Bool; + case 208 /* Int8 */: + case 209 /* Int16 */: + case 210 /* Int32 */: + case 211 /* Int64 */: + return Int; + case 204 /* Uint8 */: + case 205 /* Uint16 */: + case 206 /* Uint32 */: + case 207 /* Uint64 */: + return Uint; + case 202 /* Float32 */: + case 203 /* Float64 */: + return Float; + case 196 /* Bin8 */: + case 197 /* Bin16 */: + case 198 /* Bin32 */: + return Bytes; + case 217 /* Str8 */: + case 218 /* Str16 */: + case 219 /* Str32 */: + return Str; + case 220 /* Array16 */: + case 221 /* Array32 */: + return Arr; + case 222 /* Map16 */: + case 223 /* Map32 */: + return Map; + case 214 /* FixExt4 */: + case 215 /* FixExt8 */: + case 199 /* Ext8 */: + return Time; + default: + if (isPosFixintTag(tag) || isNegFixintTag(tag)) { + return Int; + } + if (isFixstrTag(tag)) { + return Str; + } + if (isFixarrayTag(tag)) { + return Arr; + } + if (isFixmapTag(tag)) { + return Map; + } + throw new TypeError(`unsupported tag ${tag}`); + } +} + +function encode(v, typ) { + const buf = createWriteBuffer(); + (typ || Any).enc(buf, v); + return buf.ui8array(); +} +function decode(buf, typ) { + return (typ || Any).dec(createReadBuffer(buf)); +} + +exports.Nil = Nil; +exports.Bool = Bool; +exports.Int = Int; +exports.Uint = Uint; +exports.Float = Float; +exports.Bytes = Bytes; +exports.Str = Str; +exports.TypedArr = TypedArr; +exports.TypedMap = TypedMap; +exports.Time = Time; +exports.Any = Any; +exports.Arr = Arr; +exports.Map = Map; +exports.Struct = Struct; +exports.Union = Union; +exports.structEncoder = structEncoder; +exports.structDecoder = structDecoder; +exports.unionEncoder = unionEncoder; +exports.unionDecoder = unionDecoder; +exports.encode = encode; +exports.decode = decode; + + +},{}],"/terminado-xtermjs":[function(require,module,exports){ +/** + * Swaps keys and values in the given object. + * Non-string values will be converted to string in order to be used as key. + * + * @param {Object} object + * The keys and values to swap. + * @return {Object} + * A new object with keys and values swapped. + */ +function swap(object){ + // the new object + var swappedObject = {}; + // loop all keys + for (var key in object) { + // get the value to be used as key, converting it to string, if needed + var value = typeof object[key] == "string" ? object[key] : object[key].toString(); + // add the swapped key/value pair + swappedObject[value] = key; + } + // return the new object + return swappedObject; +} + +// define the message formats +var formats = { + JSON: { + /** + * Packs the given type and data as JSON-serialised string. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {String} + * The JSON-serialised pack. + */ + pack: function pack(type, message) { + // init the pack with the type + var pack = [type]; + + // check if the message is an array + if (message instanceof Array) { + // add the message's elements to the pack + pack = pack.concat(message); + } else { + // add the message to the pack + pack.push(message); + } + + // return the JSON-stringyfied pack + return JSON.stringify(pack); + }, + + /** + * Unpacks the given JSON-serialised string. + * + * @param {String} data + * A JSON-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // return the unpacked type and message (parts) + return JSON.parse(data); + } + }, + + LightPayload: { + // forward map mapping terminado types to LightPayload types + TYPES: { + stdin: "I", + stdout: "O", + set_size: "S", + setup: "C", + disconnect: "D", + switch_format: "F" + }, + + /** + * Packs the given type and data as LightPayload-serialised string. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {String} + * The LightPayload-serialised pack + */ + pack: function pack(type, message) { + // return the LightPayload-serialised string + return this.TYPES[type] + "|" + (message instanceof Array ? message.join(",") : message); + }, + + /** + * Unpacks the given LightPayload-serialised string. + * + * @param {String} data + * A LightPayload-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // return the unpacked type and message + return [this.RTYPES[data[0]], data.substring(2)]; + } + }, + + // forward map mapping terminado types to MessagePack types + MessagePack: { + TYPES: { + stdin: 1, + stdout: 2, + set_size: 3, + setup: 4, + disconnect: 5, + switch_format: 6 + }, + + /** + * Packs the given type and data as MessagePack-serialised binary data. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {ByteArray} + * The MessagePack-serialised pack. + */ + pack: function pack(type, message) { + // init the pack with the type mapped to the corresponding MessagePack type + var pack = [this.TYPES[type]]; + + // check if the message is an array + if (message instanceof Array) { + // add the message's elements to the pack + pack = pack.concat(message); + } else { + // add the message to the pack + pack.push(message); + } + + // return the MessagePack-serialised pack + return require("messagepack").encode(pack); + }, + + /** + * Unpacks the given MessagePack-serialised binary data. + * + * @param {Blob} data + * A LightPayload-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // a blob can only be read async, return a promise + return new Promise(function(resolve, reject) { + // create a file reader + var fileReader = new FileReader(); + // when the blob is read + fileReader.onload = function(event) { + // unpack the MessagePack-serialised binary data + var message = require("messagepack").decode(event.target.result); + // map the MessagePack type to the corresponding terminado type + message[0] = swap(formats.MessagePack.TYPES)[message[0].toString()]; + // resolve the promise + resolve(message); + }; + // on error reject the promise + fileReader.onerror = reject; + // on abort reject the promise + fileReader.onabort = reject; + // read the blob + fileReader.readAsArrayBuffer(data); + }); + } + } +}; + +// reverse map mapping MessagePack types to terminado types +formats.LightPayload.RTYPES = swap(formats.LightPayload.TYPES); +// reverse map mapping LightPayload types to terminado types +formats.MessagePack.RTYPES = swap(formats.MessagePack.TYPES); + +// define the terminado addon +var terminado = { + // define the default message format + DEFAULT_MESSAGE_FORMAT: "JSON", + + apply: function apply(terminalConstructor, messageFormat, switchMessageFormat) { + // default to the default message format, if no message format is given + messageFormat = messageFormat || this.DEFAULT_MESSAGE_FORMAT; + // default to switching message format, if not given and message format is not the default message format + switchMessageFormat = switchMessageFormat !== undefined ? switchMessageFormat : + (messageFormat != this.DEFAULT_MESSAGE_FORMAT ? true : false); + + // closure cache the message format and if to switch the message format + terminalConstructor.prototype.terminadoAttach = (function(messageFormat) { + return function (socket, bidirectional, buffered) { + return terminado.terminadoAttach(this, socket, bidirectional, buffered, messageFormat, switchMessageFormat); + }; + })(messageFormat, switchMessageFormat); + + terminalConstructor.prototype.terminadoDetach = function (socket) { + return terminado.terminadoDetach(this, socket); + }; + }, + + terminadoAttach: function terminadoAttach(term, socket, bidirectional, buffered, messageFormat, switchMessageFormat) { + // check if to switch the message format + if (switchMessageFormat) { + // tell terminado which message format to use from now on + socket.send(formats[this.DEFAULT_MESSAGE_FORMAT].pack("switch_format", messageFormat)); + } + + var addonTerminal = term; + bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; + addonTerminal.__socket = socket; + addonTerminal.__flushBuffer = function () { + addonTerminal.write(addonTerminal.__attachSocketBuffer); + addonTerminal.__attachSocketBuffer = null; + }; + addonTerminal.__pushToBuffer = function (data) { + if (addonTerminal.__attachSocketBuffer) { + addonTerminal.__attachSocketBuffer += data; + } + else { + addonTerminal.__attachSocketBuffer = data; + setTimeout(addonTerminal.__flushBuffer, 10); + } + }; + addonTerminal.__getMessage = function (ev) { + function processMessage(message) { + if (message[0] === 'stdout') { + if (buffered) { + addonTerminal.__pushToBuffer(message[1]); + } + else { + addonTerminal.write(message[1]); + } + } + } + + // unpack the data + var data = formats[messageFormat].unpack(ev.data); + // check if data is still unpacking + if (data instanceof Promise) { + // wait for the data to be unpacked and process it once unpacked + data.then(processMessage); + } else { + // process the data + processMessage(data); + } + }; + addonTerminal.__sendData = function (data) { + // pack and send the data + socket.send(formats[messageFormat].pack("stdin", data)); + }; + addonTerminal.__setSize = function (size) { + // pack and set the "set_size" data + socket.send(formats[messageFormat].pack("set_size", [size.rows, size.cols])); + }; + socket.addEventListener('message', addonTerminal.__getMessage); + if (bidirectional) { + addonTerminal.on('data', addonTerminal.__sendData); + } + addonTerminal.on('resize', addonTerminal.__setSize); + socket.addEventListener('close', function () { return terminado.terminadoDetach(addonTerminal, socket); }); + socket.addEventListener('error', function () { return terminado.terminadoDetach(addonTerminal, socket); }); + }, + + terminadoDetach: function terminadoDetach(term, socket) { + var addonTerminal = term; + addonTerminal.off('data', addonTerminal.__sendData); + socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; + if (socket) { + socket.removeEventListener('message', addonTerminal.__getMessage); + } + delete addonTerminal.__socket; + } +}; + +// export the terminando addon +module.exports = terminado; +},{"messagepack":1}]},{},[]); diff --git a/terminado/_static/terminado-xtermjs.js b/terminado/_static/terminado-xtermjs.js new file mode 100644 index 0000000..8457ea1 --- /dev/null +++ b/terminado/_static/terminado-xtermjs.js @@ -0,0 +1,283 @@ +/** + * Swaps keys and values in the given object. + * Non-string values will be converted to string in order to be used as key. + * + * @param {Object} object + * The keys and values to swap. + * @return {Object} + * A new object with keys and values swapped. + */ +function swap(object){ + // the new object + var swappedObject = {}; + // loop all keys + for (var key in object) { + // get the value to be used as key, converting it to string, if needed + var value = typeof object[key] == "string" ? object[key] : object[key].toString(); + // add the swapped key/value pair + swappedObject[value] = key; + } + // return the new object + return swappedObject; +} + +// define the message formats +var formats = { + JSON: { + /** + * Packs the given type and data as JSON-serialised string. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {String} + * The JSON-serialised pack. + */ + pack: function pack(type, message) { + // init the pack with the type + var pack = [type]; + + // check if the message is an array + if (message instanceof Array) { + // add the message's elements to the pack + pack = pack.concat(message); + } else { + // add the message to the pack + pack.push(message); + } + + // return the JSON-stringyfied pack + return JSON.stringify(pack); + }, + + /** + * Unpacks the given JSON-serialised string. + * + * @param {String} data + * A JSON-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // return the unpacked type and message (parts) + return JSON.parse(data); + } + }, + + LightPayload: { + // forward map mapping terminado types to LightPayload types + TYPES: { + stdin: "I", + stdout: "O", + set_size: "S", + setup: "C", + disconnect: "D", + switch_format: "F" + }, + + /** + * Packs the given type and data as LightPayload-serialised string. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {String} + * The LightPayload-serialised pack + */ + pack: function pack(type, message) { + // return the LightPayload-serialised string + return this.TYPES[type] + "|" + (message instanceof Array ? message.join(",") : message); + }, + + /** + * Unpacks the given LightPayload-serialised string. + * + * @param {String} data + * A LightPayload-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // return the unpacked type and message + return [this.RTYPES[data[0]], data.substring(2)]; + } + }, + + // forward map mapping terminado types to MessagePack types + MessagePack: { + TYPES: { + stdin: 1, + stdout: 2, + set_size: 3, + setup: 4, + disconnect: 5, + switch_format: 6 + }, + + /** + * Packs the given type and data as MessagePack-serialised binary data. + * + * @param {String} type + * A tornado message type. + * @param {String|Array} message + * The message to pack. + * @return {ByteArray} + * The MessagePack-serialised pack. + */ + pack: function pack(type, message) { + // init the pack with the type mapped to the corresponding MessagePack type + var pack = [this.TYPES[type]]; + + // check if the message is an array + if (message instanceof Array) { + // add the message's elements to the pack + pack = pack.concat(message); + } else { + // add the message to the pack + pack.push(message); + } + + // return the MessagePack-serialised pack + return require("messagepack").encode(pack); + }, + + /** + * Unpacks the given MessagePack-serialised binary data. + * + * @param {Blob} data + * A LightPayload-serialised string. + * @return {Array} + * A type and the message (parts). + */ + unpack: function unpack(data) { + // a blob can only be read async, return a promise + return new Promise(function(resolve, reject) { + // create a file reader + var fileReader = new FileReader(); + // when the blob is read + fileReader.onload = function(event) { + // unpack the MessagePack-serialised binary data + var message = require("messagepack").decode(event.target.result); + // map the MessagePack type to the corresponding terminado type + message[0] = swap(formats.MessagePack.TYPES)[message[0].toString()]; + // resolve the promise + resolve(message); + }; + // on error reject the promise + fileReader.onerror = reject; + // on abort reject the promise + fileReader.onabort = reject; + // read the blob + fileReader.readAsArrayBuffer(data); + }); + } + } +}; + +// reverse map mapping MessagePack types to terminado types +formats.LightPayload.RTYPES = swap(formats.LightPayload.TYPES); +// reverse map mapping LightPayload types to terminado types +formats.MessagePack.RTYPES = swap(formats.MessagePack.TYPES); + +// define the terminado addon +var terminado = { + // define the default message format + DEFAULT_MESSAGE_FORMAT: "JSON", + + apply: function apply(terminalConstructor, messageFormat, switchMessageFormat) { + // default to the default message format, if no message format is given + messageFormat = messageFormat || this.DEFAULT_MESSAGE_FORMAT; + // default to switching message format, if not given and message format is not the default message format + switchMessageFormat = switchMessageFormat !== undefined ? switchMessageFormat : + (messageFormat != this.DEFAULT_MESSAGE_FORMAT ? true : false); + + // closure cache the message format and if to switch the message format + terminalConstructor.prototype.terminadoAttach = (function(messageFormat) { + return function (socket, bidirectional, buffered) { + return terminado.terminadoAttach(this, socket, bidirectional, buffered, messageFormat, switchMessageFormat); + }; + })(messageFormat, switchMessageFormat); + + terminalConstructor.prototype.terminadoDetach = function (socket) { + return terminado.terminadoDetach(this, socket); + }; + }, + + terminadoAttach: function terminadoAttach(term, socket, bidirectional, buffered, messageFormat, switchMessageFormat) { + // check if to switch the message format + if (switchMessageFormat) { + // tell terminado which message format to use from now on + socket.send(formats[this.DEFAULT_MESSAGE_FORMAT].pack("switch_format", messageFormat)); + } + + var addonTerminal = term; + bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; + addonTerminal.__socket = socket; + addonTerminal.__flushBuffer = function () { + addonTerminal.write(addonTerminal.__attachSocketBuffer); + addonTerminal.__attachSocketBuffer = null; + }; + addonTerminal.__pushToBuffer = function (data) { + if (addonTerminal.__attachSocketBuffer) { + addonTerminal.__attachSocketBuffer += data; + } + else { + addonTerminal.__attachSocketBuffer = data; + setTimeout(addonTerminal.__flushBuffer, 10); + } + }; + addonTerminal.__getMessage = function (ev) { + function processMessage(message) { + if (message[0] === 'stdout') { + if (buffered) { + addonTerminal.__pushToBuffer(message[1]); + } + else { + addonTerminal.write(message[1]); + } + } + } + + // unpack the data + var data = formats[messageFormat].unpack(ev.data); + // check if data is still unpacking + if (data instanceof Promise) { + // wait for the data to be unpacked and process it once unpacked + data.then(processMessage); + } else { + // process the data + processMessage(data); + } + }; + addonTerminal.__sendData = function (data) { + // pack and send the data + socket.send(formats[messageFormat].pack("stdin", data)); + }; + addonTerminal.__setSize = function (size) { + // pack and set the "set_size" data + socket.send(formats[messageFormat].pack("set_size", [size.rows, size.cols])); + }; + socket.addEventListener('message', addonTerminal.__getMessage); + if (bidirectional) { + addonTerminal.on('data', addonTerminal.__sendData); + } + addonTerminal.on('resize', addonTerminal.__setSize); + socket.addEventListener('close', function () { return terminado.terminadoDetach(addonTerminal, socket); }); + socket.addEventListener('error', function () { return terminado.terminadoDetach(addonTerminal, socket); }); + }, + + terminadoDetach: function terminadoDetach(term, socket) { + var addonTerminal = term; + addonTerminal.off('data', addonTerminal.__sendData); + socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; + if (socket) { + socket.removeEventListener('message', addonTerminal.__getMessage); + } + delete addonTerminal.__socket; + } +}; + +// export the terminando addon +module.exports = terminado; \ No newline at end of file diff --git a/terminado/formats/__init__.py b/terminado/formats/__init__.py new file mode 100644 index 0000000..1fb6d6e --- /dev/null +++ b/terminado/formats/__init__.py @@ -0,0 +1 @@ +"""Message formats for reading and writing to the WebSocket.""" diff --git a/terminado/formats/format.py b/terminado/formats/format.py new file mode 100644 index 0000000..17fec5f --- /dev/null +++ b/terminado/formats/format.py @@ -0,0 +1,16 @@ +"""Interface for classes wanting to implement a message format.""" + +from interface import Interface + +class MessageFormat(Interface): + """Interface for message formats.""" + + @staticmethod + def pack(command: str, message): + """Pack the given command and message for writing to the socket.""" + pass + + @staticmethod + def unpack(data) -> list: + """Unpack the data read from the socket.""" + pass diff --git a/terminado/formats/json.py b/terminado/formats/json.py new file mode 100644 index 0000000..99bec4a --- /dev/null +++ b/terminado/formats/json.py @@ -0,0 +1,29 @@ +"""Message format implementation writing and reading data as JSON. + See http://json.org for JSON. +""" + +import json +from interface import implements +from .format import MessageFormat + +class JSONMessageFormat(implements(MessageFormat)): + """Message format implementation writing and reading data as JSON. + See http://json.org for JSON. + """ + + @staticmethod + def pack(command: str, message): + """Pack the given command and message for writing to the socket.""" + pack = [command] + + if isinstance(message, list): + pack = pack + message + else: + pack.append(message) + + return json.dumps(pack) + + @staticmethod + def unpack(data) -> list: + """Unpack the data read from the socket.""" + return json.loads(data) diff --git a/terminado/formats/lightpayload.py b/terminado/formats/lightpayload.py new file mode 100644 index 0000000..dd0a913 --- /dev/null +++ b/terminado/formats/lightpayload.py @@ -0,0 +1,60 @@ +"""Message format implementation writing and reading data as custom string-serialized format. + LightPayload has a smaller message size than JSON or even MessagePack, because it is optimised for the data send + and received by tornado rather than being a generic data format. Packing and unpacking uses string operations + mostly, making it fast and small. +""" + +from interface import implements +from .format import MessageFormat + +class LightPayloadMessageFormat(implements(MessageFormat)): + """Message format implementation writing and reading data as custom string-serialized format. + LightPayload has a smaller message size than JSON or even MessagePack, because it is optimised for the data send + and received by tornado rather than being a generic data format. Packing and unpacking uses string operations + mostly, making it fast and small. + """ + + # forward map mapping terminado types to LightPayload types + TYPES = { + "stdin": "I", # (I)nput + "stdout": "O", # (O)utput + "set_size": "S", # set (S)ize + "setup": "C", # (C)onnect + "disconnect": "D", # (D)isconnect + "switch_format": "F" # switch (F)ormat + } + + # reverse map mapping LightPayload types to terminado types + RTYPES = {value:key for key, value in TYPES.items()} + + @staticmethod + def pack(command: str, message): + """Pack the given command and message for writing to the socket.""" + # map the terminado type to the corresponding LightPayload type + command = LightPayloadMessageFormat.TYPES[command] + + pack = command + "|" + + if isinstance(message, list): + pack += ",".join(message) + else: + pack += message or "" + + return pack + + @staticmethod + def unpack(data) -> list: + """Unpack the data read from the socket.""" + # map the LightPayload type to the corresponding terminado type + command = LightPayloadMessageFormat.RTYPES[data[0]] + + message = data[2:] + + # the message is always a string, except for "set_size" for which it is a stringyfied list of (two) ints + if command == "set_size": + message = [int(x) for x in message.split(',')] + message = [command] + message + else: + message = [command, message] + + return message diff --git a/terminado/formats/messagepack.py b/terminado/formats/messagepack.py new file mode 100644 index 0000000..223c9cd --- /dev/null +++ b/terminado/formats/messagepack.py @@ -0,0 +1,54 @@ +"""Message format implementation writing and reading data as MessagePack. + See https://msgpack.org for MessagePack. + It's like JSON. but fast and small. +""" + +import msgpack +from interface import implements +from .format import MessageFormat + +class MessagePackMessageFormat(implements(MessageFormat)): + """Message format implementation writing and reading data as MessagePack. + See https://msgpack.org for MessagePack. + It's like JSON. but fast and small. + """ + + # forward map mapping terminado types to LightPayload types + TYPES = { + "stdin": 1, + "stdout": 2, + "set_size": 3, + "setup": 4, + "disconnect": 5, + "switch_format": 6 + } + + # reverse map mapping LightPayload types to terminado types + RTYPES = {str(value):key for key, value in TYPES.items()} + + @staticmethod + def pack(command: str, message): + """Pack the given command and message for writing to the socket.""" + # map the terminado type to the corresponding MessagePack type + command = MessagePackMessageFormat.TYPES[command] + + pack = [command] + + if isinstance(message, list): + pack = pack + message + else: + pack.append(message) + + # use an UTF-8 encoded string instead of bytes + return msgpack.dumps(pack, use_bin_type=False) + + @staticmethod + def unpack(data) -> list: + """Unpack the data read from the socket.""" + pack = msgpack.loads(data, raw=False) + + # map the MessagePack type to the corresponding terminado type + command = MessagePackMessageFormat.RTYPES[str(pack[0])] + pack[0] = command + + return pack diff --git a/terminado/websocket.py b/terminado/websocket.py index 3d3fe39..e2745af 100644 --- a/terminado/websocket.py +++ b/terminado/websocket.py @@ -12,8 +12,8 @@ except ImportError: from urlparse import urlparse -import json import logging +import importlib import tornado.web import tornado.websocket @@ -24,12 +24,26 @@ def _cast_unicode(s): return s class TermSocket(tornado.websocket.WebSocketHandler): + # map mapping message format identifiers to their implementing classes + MESSAGE_FORMATS = { + "JSON": "JSONMessageFormat", + "LightPayload": "LightPayloadMessageFormat", + "MessagePack": "MessagePackMessageFormat" + } + + def get_compression_options(self): + """Use the WebSocket's permessage-deflate extension.""" + return {} + """Handler for a terminal websocket""" - def initialize(self, term_manager): + def initialize(self, term_manager, message_format = "JSON"): self.term_manager = term_manager self.term_name = "" self.size = (None, None) self.terminal = None + # load the class implementing the message format + self.message_format = getattr(importlib.import_module("terminado.formats." + message_format.lower()), + self.MESSAGE_FORMATS[message_format]) self._logger = logging.getLogger(__name__) @@ -56,31 +70,41 @@ def open(self, url_component=None): self.on_pty_read(s) self.terminal.clients.append(self) - self.send_json_message(["setup", {}]) + self.send_message("setup", {}) self._logger.info("TermSocket.open: Opened %s", self.term_name) + def send_message(self, command, message): + """Sends a typed message packed by the current message format implementation.""" + + pack = self.message_format.pack(command, message) + + # make sure binary packs are send as binary + if hasattr(pack, "decode"): + self.write_message(pack, binary=True) + else: + self.write_message(pack) + def on_pty_read(self, text): """Data read from pty; send to frontend""" - self.send_json_message(['stdout', text]) + self.send_message("stdout", text) - def send_json_message(self, content): - json_msg = json.dumps(content) - self.write_message(json_msg) - - def on_message(self, message): + def on_message(self, pack): """Handle incoming websocket message - - We send JSON arrays, where the first element is a string indicating - what kind of message this is. Data associated with the message follows. """ ##logging.info("TermSocket.on_message: %s - (%s) %s", self.term_name, type(message), len(message) if isinstance(message, bytes) else message[:250]) - command = json.loads(message) - msg_type = command[0] - - if msg_type == "stdin": - self.terminal.ptyproc.write(command[1]) - elif msg_type == "set_size": - self.size = command[1:3] + message = self.message_format.unpack(pack) + + if message[0] == "switch_format": + # load the class implementing the message format + self.message_format = getattr(importlib.import_module("terminado.formats." + message[1].lower()), + self.MESSAGE_FORMATS[message[1]]) + + for s in self.terminal.read_buffer: + self.on_pty_read(s) + elif message[0] == "stdin": + self.terminal.ptyproc.write(message[1]) + elif message[0] == "set_size": + self.size = message[1:3] self.terminal.resize_to_smallest() def on_close(self): @@ -98,6 +122,7 @@ def on_close(self): def on_pty_died(self): """Terminal closed: tell the frontend, and close the socket. """ - self.send_json_message(['disconnect', 1]) + self.send_message("disconnect", 1) + self.close() self.terminal = None